diff --git a/web/callback.php b/web/callback.php
new file mode 100644
index 0000000..b6b1534
--- /dev/null
+++ b/web/callback.php
@@ -0,0 +1,59 @@
+exchangeCode($code);
+
+ // Validate ID token (minimal).
+ $idInfo = $oidc->validateIDToken($tokens['id_token'], $nonce);
+
+ // Get userinfo (includes groups because of scope).
+ $userInfo = $oidc->getUserInfo($tokens['access_token']);
+
+ // Store everything we need in the session.
+ $_SESSION['user'] = [
+ 'id_token' => $tokens['id_token'],
+ 'access_token'=> $tokens['access_token'],
+ 'refresh_token'=> $tokens['refresh_token'] ?? null,
+ 'expires_in' => $tokens['expires_in'] ?? null,
+ 'id_info' => $idInfo,
+ 'user_info' => $userInfo,
+ ];
+} catch (\Throwable $e) {
+ http_response_code(500);
+ exit('Error during OIDC exchange: ' . $e->getMessage());
+}
+
+// Clean up the query string (code, state) by redirecting to the home page.
+header('Location: /');
+exit;
\ No newline at end of file
diff --git a/web/composer.json b/web/composer.json
new file mode 100644
index 0000000..365b572
--- /dev/null
+++ b/web/composer.json
@@ -0,0 +1,6 @@
+{
+ "require": {
+ "guzzlehttp/guzzle": "^7.0",
+ "firebase/php-jwt": "^6.0"
+ }
+}
\ No newline at end of file
diff --git a/web/config.php b/web/config.php
new file mode 100644
index 0000000..7495633
--- /dev/null
+++ b/web/config.php
@@ -0,0 +1,23 @@
+load();
+
+/**
+ * Basic OIDC configuration – keep all secrets out of source‑control.
+ */
+define('OIDC_CLIENT_ID', getenv('OIDC_CLIENT_ID'));
+define('OIDC_CLIENT_SECRET', getenv('OIDC_CLIENT_SECRET'));
+define('OIDC_REDIRECT_URI', getenv('OIDC_REDIRECT_URI'));
+define('OIDC_AUTH_ENDPOINT', getenv('OIDC_AUTH_ENDPOINT'));
+define('OIDC_TOKEN_ENDPOINT', getenv('OIDC_TOKEN_ENDPOINT'));
+define('OIDC_USERINFO_ENDPOINT', getenv('OIDC_USERINFO_ENDPOINT'));
+define('OIDC_LOGOUT_ENDPOINT', getenv('OIDC_LOGOUT_ENDPOINT'));
+define('OIDC_POST_LOGOUT_REDIRECT_URI', getenv('OIDC_POST_LOGOUT_REDIRECT_URI'));
\ No newline at end of file
diff --git a/web/index.php b/web/index.php
index e69de29..1e1c6eb 100644
--- a/web/index.php
+++ b/web/index.php
@@ -0,0 +1,38 @@
+
+
+
+
+
+ Fitness Site – OIDC Demo
+
+
+
+ Fitness Site – OIDC Demo
+
+
+ You are not logged in.
+ Login with Authelia
+
+ Logged in as: =htmlspecialchars($_SESSION['user']['user_info']['name'] ?? 'N/A')?>
+
+
+
User Information
+
=htmlspecialchars(json_encode($_SESSION['user']['user_info'], JSON_PRETTY_PRINT))?>
+
+
+
+
ID Token Payload (claims)
+
=htmlspecialchars(json_encode($_SESSION['user']['id_info'], JSON_PRETTY_PRINT))?>
+
+
+ Logout
+
+
+
\ No newline at end of file
diff --git a/web/login.php b/web/login.php
new file mode 100644
index 0000000..0f22b2b
--- /dev/null
+++ b/web/login.php
@@ -0,0 +1,18 @@
+buildAuthUrl($state, $nonce));
+exit;
\ No newline at end of file
diff --git a/web/logout.php b/web/logout.php
new file mode 100644
index 0000000..b134ec8
--- /dev/null
+++ b/web/logout.php
@@ -0,0 +1,15 @@
+
+
+
+ Drag photo here or click to choose
+
+
+
\ No newline at end of file
diff --git a/web/photos/photos.php b/web/photos/photos.php
new file mode 100644
index 0000000..9829887
--- /dev/null
+++ b/web/photos/photos.php
@@ -0,0 +1,314 @@
+getMessage(), E_USER_WARNING);
+}
+
+// Check upload permissions
+$has_upload_perm = true; // Add authorization logic here
+
+// Generate unique token for upload (security)
+$upload_token = bin2hex(random_bytes(16));
+?>
+
+
+
+
+
+
+ Photos for Service Provider #
+
+
+
+ Photos for Service Provider #
+
+
+
+
+
+
+
+
+
+
+ Upload Photos
+
+
+
+ Upload permissions disabled. Contact administrator.
+
+
+
+ Loading photos...
+
+
+
+
+
+
No photos uploaded yet
+
Upload images to display them here
+
+
+
+
+
class="main-radio">
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/OIDC.php b/web/src/OIDC.php
new file mode 100644
index 0000000..c1e47b2
--- /dev/null
+++ b/web/src/OIDC.php
@@ -0,0 +1,91 @@
+http = new Client(['http_errors' => false]);
+ }
+
+ /**
+ * Build the Auth URL that redirects the user to Authelia.
+ */
+ public function buildAuthUrl(string $state, string $nonce): string
+ {
+ $params = [
+ 'response_type' => 'code',
+ 'client_id' => OIDC_CLIENT_ID,
+ 'redirect_uri' => OIDC_REDIRECT_URI,
+ 'scope' => 'openid profile email groups',
+ 'state' => $state,
+ 'nonce' => $nonce,
+ ];
+
+ return OIDC_AUTH_ENDPOINT . '?' . http_build_query($params);
+ }
+
+ /**
+ * Exchange the Authorization Code for tokens.
+ */
+ public function exchangeCode(string $code): array
+ {
+ $response = $this->http->post(OIDC_TOKEN_ENDPOINT, [
+ 'auth' => [OIDC_CLIENT_ID, OIDC_CLIENT_SECRET],
+ 'form_params' => [
+ 'grant_type' => 'authorization_code',
+ 'code' => $code,
+ 'redirect_uri' => OIDC_REDIRECT_URI,
+ ],
+ ]);
+
+ if ($response->getStatusCode() !== 200) {
+ throw new \RuntimeException('Token endpoint returned ' . $response->getStatusCode());
+ }
+
+ return json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR);
+ }
+
+ /**
+ * Call /userinfo with the access token.
+ */
+ public function getUserInfo(string $accessToken): array
+ {
+ $response = $this->http->get(OIDC_USERINFO_ENDPOINT, [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $accessToken,
+ ],
+ ]);
+
+ if ($response->getStatusCode() !== 200) {
+ throw new \RuntimeException('Userinfo endpoint returned ' . $response->getStatusCode());
+ }
+
+ return json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR);
+ }
+
+ /**
+ * Validate the ID Token (minimal validation for demo purposes).
+ */
+ public function validateIDToken(string $idToken, string $nonce): array
+ {
+ // Authelia uses RS256 by default, but your config says "none".
+ // We'll decode without verification if alg is "none".
+ $decoded = JWT::decode($idToken, new Key('', ''), ['none', 'RS256']);
+
+ // Check nonce matches what we stored in session.
+ if ($decoded->nonce !== $nonce) {
+ throw new \RuntimeException('Invalid nonce');
+ }
+
+ return (array)$decoded;
+ }
+}
\ No newline at end of file