From 965d2d084aa6ffc04d489215cc12ee1164ea4092 Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 23 Oct 2026 03:22:30 -0700 Subject: [PATCH] OIDC test --- web/index.php | 0 web/login/.env | 9 ++++ web/login/callback.php | 59 ++++++++++++++++++++++++++ web/login/composer.json | 6 +++ web/login/config.php | 23 +++++++++++ web/login/index.php | 38 +++++++++++++++++ web/login/login.php | 18 ++++++++ web/login/logout.php | 15 +++++++ web/login/src/OIDC.php | 91 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 259 insertions(+) create mode 100644 web/index.php create mode 100644 web/login/.env create mode 100644 web/login/callback.php create mode 100644 web/login/composer.json create mode 100644 web/login/config.php create mode 100644 web/login/index.php create mode 100644 web/login/login.php create mode 100644 web/login/logout.php create mode 100644 web/login/src/OIDC.php diff --git a/web/index.php b/web/index.php new file mode 100644 index 0000000..e69de29 diff --git a/web/login/.env b/web/login/.env new file mode 100644 index 0000000..675657e --- /dev/null +++ b/web/login/.env @@ -0,0 +1,9 @@ +OIDC_CLIENT_ID=fitness-oidc +OIDC_CLIENT_SECRET=egebuqzZfVnjjBG9JDQ8tjW3TzV9gJurvT7qfB6c2SX2nPBNeaGycDfx4788QzJd +OIDC_REDIRECT_URI=https://fitness.matt-cloud.com/pgapi/auth/oidc/callback +OIDC_AUTH_ENDPOINT=https://auth.matt-cloud.com/oidc/auth +OIDC_TOKEN_ENDPOINT=https://auth.matt-cloud.com/oidc/token +OIDC_USERINFO_ENDPOINT=https://auth.matt-cloud.com/oidc/userinfo +OIDC_LOGOUT_ENDPOINT=https://auth.matt-cloud.com/logout +# Optional: the default redirect after logout +OIDC_POST_LOGOUT_REDIRECT_URI=https://fitness.matt-cloud.com/ \ No newline at end of file diff --git a/web/login/callback.php b/web/login/callback.php new file mode 100644 index 0000000..b6b1534 --- /dev/null +++ b/web/login/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/login/composer.json b/web/login/composer.json new file mode 100644 index 0000000..365b572 --- /dev/null +++ b/web/login/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/login/config.php b/web/login/config.php new file mode 100644 index 0000000..7495633 --- /dev/null +++ b/web/login/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/login/index.php b/web/login/index.php new file mode 100644 index 0000000..1e1c6eb --- /dev/null +++ b/web/login/index.php @@ -0,0 +1,38 @@ + + + + + + Fitness Site – OIDC Demo + + + +

Fitness Site – OIDC Demo

+ + +

You are not logged in.

+

+ +

Logged in as:

+ +
+

User Information

+
+
+ +
+

ID Token Payload (claims)

+
+
+ +

+ + + \ No newline at end of file diff --git a/web/login/login.php b/web/login/login.php new file mode 100644 index 0000000..0f22b2b --- /dev/null +++ b/web/login/login.php @@ -0,0 +1,18 @@ +buildAuthUrl($state, $nonce)); +exit; \ No newline at end of file diff --git a/web/login/logout.php b/web/login/logout.php new file mode 100644 index 0000000..b134ec8 --- /dev/null +++ b/web/login/logout.php @@ -0,0 +1,15 @@ +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