OIDC test
This commit is contained in:
1 parent
fd79a027a2
commit
965d2d084a
9 files changed
+259
No files matched your search
Whitespace-only changes.
@@ -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/
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use Fitness\OIDC\OIDC;
|
||||||
|
|
||||||
|
// Ensure a state was sent back.
|
||||||
|
if (!isset($_GET['state']) || !isset($_SESSION['oidc_state'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('Missing state parameter.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the state matches what we stored.
|
||||||
|
if ($_GET['state'] !== $_SESSION['oidc_state']) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('Invalid state.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the session variables.
|
||||||
|
$state = $_SESSION['oidc_state'];
|
||||||
|
$nonce = $_SESSION['oidc_nonce'];
|
||||||
|
unset($_SESSION['oidc_state'], $_SESSION['oidc_nonce']);
|
||||||
|
|
||||||
|
if (!isset($_GET['code'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('Missing authorization code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $_GET['code'];
|
||||||
|
$oidc = new OIDC();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Exchange code for tokens.
|
||||||
|
$tokens = $oidc->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;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
|
"firebase/php-jwt": "^6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load environment variables. If you don't use phpdotenv,
|
||||||
|
* copy the values from .env directly here.
|
||||||
|
*/
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||||
|
$dotenv->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'));
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Fitness Site – OIDC Demo</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 2rem; }
|
||||||
|
.info { margin-top: 1rem; }
|
||||||
|
pre { background: #f4f4f4; padding: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Fitness Site – OIDC Demo</h1>
|
||||||
|
|
||||||
|
<?php if (!isset($_SESSION['user'])): ?>
|
||||||
|
<p>You are not logged in.</p>
|
||||||
|
<p><a href="/login.php"><button>Login with Authelia</button></a></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p>Logged in as: <strong><?=htmlspecialchars($_SESSION['user']['user_info']['name'] ?? 'N/A')?></strong></p>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<h2>User Information</h2>
|
||||||
|
<pre><?=htmlspecialchars(json_encode($_SESSION['user']['user_info'], JSON_PRETTY_PRINT))?></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<h2>ID Token Payload (claims)</h2>
|
||||||
|
<pre><?=htmlspecialchars(json_encode($_SESSION['user']['id_info'], JSON_PRETTY_PRINT))?></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="/logout.php"><button>Logout</button></a></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use Fitness\OIDC\OIDC;
|
||||||
|
|
||||||
|
// Generate a cryptographically random state & nonce.
|
||||||
|
$state = bin2hex(random_bytes(16));
|
||||||
|
$nonce = bin2hex(random_bytes(16));
|
||||||
|
|
||||||
|
// Persist them in the session to validate later.
|
||||||
|
$_SESSION['oidc_state'] = $state;
|
||||||
|
$_SESSION['oidc_nonce'] = $nonce;
|
||||||
|
|
||||||
|
$oidc = new OIDC();
|
||||||
|
header('Location: ' . $oidc->buildAuthUrl($state, $nonce));
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
// Destroy the PHP session.
|
||||||
|
session_start();
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
// Redirect the user to Authelia’s logout endpoint.
|
||||||
|
// Authelia will clean its session and then redirect back.
|
||||||
|
$logoutUrl = OIDC_LOGOUT_ENDPOINT
|
||||||
|
. '?post_logout_redirect_uri=' . urlencode(OIDC_POST_LOGOUT_REDIRECT_URI);
|
||||||
|
|
||||||
|
header('Location: ' . $logoutUrl);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Fitness\OIDC;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\Key;
|
||||||
|
|
||||||
|
class OIDC
|
||||||
|
{
|
||||||
|
private Client $http;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user