24bb244352
Contexte : sur abonnel.fr, session_start() était appelé sur chaque
requête PHP (y compris bots), créant ~17 000 fichiers de session/jour
dans un répertoire custom non nettoyé par le cron Debian. Les workers
PHP-FPM grossissaient en mémoire et le pool saturait (1 188 erreurs
503 en 30 minutes).
Changements :
public/index.php
- session_start() uniquement si le cookie de session existe déjà ou si
la requête est POST. Les bots (GET sans cookie) ne créent plus de
session.
- CSRF commentaires migré de $_SESSION['comment_csrf'] vers un double-
submit cookie (_csrf_c, SameSite=Strict, HttpOnly). La session n'est
plus requise pour les visiteurs anonymes qui postent un commentaire.
templates/comments_section.php
- Génère le token CSRF et le pose en cookie (_csrf_c) au lieu de
l'écrire en session.
public/.htaccess
- Règle Apache 410 Gone pour toute URL contenant un paramètre ?do=
(anciens paramètres DokuWiki : do=media, do=export_pdf…). Traité par
Apache en 2ms sans toucher PHP-FPM.
public/oidc/{start,callback,me}.php
- Correction du bug introduit par 0b8077e : config.php (qui utilise
BASE_PATH) était chargé avant bootstrap.php (qui définit BASE_PATH).
Fix : define('BASE_PATH', …) ajouté avant le require config.php.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
198 lines
6.8 KiB
PHP
198 lines
6.8 KiB
PHP
<?php
|
||
// projet : mug.a5l.fr
|
||
// fichier : pages/oidc/me.php
|
||
// version : 20251005
|
||
declare(strict_types=1);
|
||
|
||
if (!defined('BASE_PATH')) {
|
||
define('BASE_PATH', dirname(__DIR__, 2));
|
||
}
|
||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||
require_once dirname(__DIR__, 2) . '/bootstrap.php';
|
||
|
||
function maskToken(?string $t): string
|
||
{
|
||
if (!$t) {
|
||
return '';
|
||
}
|
||
$len = strlen($t);
|
||
if ($len <= 12) {
|
||
return str_repeat('•', $len);
|
||
}
|
||
return substr($t, 0, 6) . str_repeat('•', max(0, $len - 12)) . substr($t, -6);
|
||
}
|
||
function b64url_decode_str(string $s): string|false
|
||
{
|
||
$s = strtr($s, '-_', '+/');
|
||
$pad = strlen($s) % 4;
|
||
if ($pad) {
|
||
$s .= str_repeat('=', 4 - $pad);
|
||
}
|
||
return base64_decode($s, true);
|
||
}
|
||
function decode_jwt(string $jwt): array
|
||
{
|
||
if (substr_count($jwt, '.') !== 2) {
|
||
return [];
|
||
}
|
||
[, $payload, ] = explode('.', $jwt, 3);
|
||
$json = b64url_decode_str($payload);
|
||
if ($json === false) {
|
||
return [];
|
||
}
|
||
$arr = json_decode($json, true);
|
||
return is_array($arr) ? $arr : [];
|
||
}
|
||
|
||
$env = static function (string $k, ?string $d = null): ?string {
|
||
if (array_key_exists($k, $_ENV) && $_ENV[$k] !== '') {
|
||
return (string)$_ENV[$k];
|
||
}
|
||
$v = getenv($k);
|
||
if ($v !== false && $v !== '') {
|
||
return (string)$v;
|
||
}
|
||
return $d;
|
||
};
|
||
|
||
$debugEnabled = ($env('DEBUG_OIDC') === 'true') || (isset($_GET['debug']) && $_GET['debug'] === '1');
|
||
|
||
$oidc = $_SESSION['oidc'] ?? [];
|
||
$claims = $_SESSION['oidc_userinfo'] ?? [];
|
||
$issuer = (string)($oidc['issuer'] ?? '');
|
||
$sub = (string)($oidc['sub'] ?? '');
|
||
$idToken = (string)($oidc['id_token'] ?? '');
|
||
$accTok = (string)($oidc['access_token'] ?? '');
|
||
$expAt = (int) ($oidc['expires_at'] ?? 0);
|
||
|
||
$now = time();
|
||
$left = $expAt ? max(0, $expAt - $now) : null;
|
||
|
||
// Fallback 1 : si pas de claims userinfo, essayer de les lire dans l'id_token
|
||
if (!$claims && $idToken) {
|
||
$claims = decode_jwt($idToken);
|
||
}
|
||
|
||
// Fallback 2 (debug) : tenter un appel live au UserInfo si access_token présent
|
||
if ($debugEnabled && $claims === [] && $accTok && $issuer) {
|
||
$userinfoEndpoint = rtrim($issuer, '/') . '/protocol/openid-connect/userinfo';
|
||
$ch = curl_init($userinfoEndpoint);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accTok],
|
||
CURLOPT_TIMEOUT => 6,
|
||
]);
|
||
$resp = curl_exec($ch);
|
||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
if ($resp !== false && $code === 200) {
|
||
$tmp = json_decode((string)$resp, true);
|
||
if (is_array($tmp)) {
|
||
$claims = $tmp;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Extraire rôles groupés (Keycloak)
|
||
$roles = [];
|
||
if (isset($claims['realm_access']['roles']) && is_array($claims['realm_access']['roles'])) {
|
||
$roles = array_merge($roles, $claims['realm_access']['roles']);
|
||
}
|
||
if (isset($claims['resource_access']) && is_array($claims['resource_access'])) {
|
||
foreach ($claims['resource_access'] as $clientId => $data) {
|
||
if (!empty($data['roles']) && is_array($data['roles'])) {
|
||
foreach ($data['roles'] as $r) {
|
||
$roles[] = $clientId . ':' . $r;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
$roles = array_values(array_unique($roles));
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>OIDC • Profil</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link href="/assets/bootstrap/bootstrap.min.css" rel="stylesheet">
|
||
<style>
|
||
.kv { display:grid; grid-template-columns: 220px 1fr; gap:.5rem 1rem; }
|
||
.kv dt { font-weight: 600; color: #555; }
|
||
pre { background: #f8f9fa; padding: .75rem; border-radius: .5rem; overflow:auto; }
|
||
</style>
|
||
</head>
|
||
<body class="container py-4">
|
||
<h1 class="mb-4">Profil A5L</h1>
|
||
|
||
<?php if (!$oidc): ?>
|
||
<div class="alert alert-warning">Aucune session A5L. Connecte-toi via A5L d'abord.</div>
|
||
<?php else: ?>
|
||
<div class="card mb-4">
|
||
<div class="card-header">Session / Jetons</div>
|
||
<div class="card-body">
|
||
<dl class="kv">
|
||
<dt>Issuer</dt><dd><?= htmlspecialchars($issuer) ?></dd>
|
||
<dt>Subject (sub)</dt><dd><?= htmlspecialchars($sub) ?></dd>
|
||
<dt>ID Token</dt><dd><code><?= htmlspecialchars(maskToken($idToken)) ?></code></dd>
|
||
<dt>Access Token</dt><dd><code><?= htmlspecialchars(maskToken($accTok)) ?></code></dd>
|
||
<dt>Expire à</dt><dd><?= $expAt ? date('Y-m-d H:i:s', $expAt) : '—' ?></dd>
|
||
<dt>Temps restant</dt><dd><?= $left !== null ? ($left . ' s') : '—' ?></dd>
|
||
</dl>
|
||
<?php if ($debugEnabled): ?>
|
||
<details class="mt-3">
|
||
<summary>Voir jetons non masqués (danger)</summary>
|
||
<div class="mt-2">
|
||
<div><strong>ID Token</strong></div>
|
||
<pre><?= htmlspecialchars($idToken) ?></pre>
|
||
<div><strong>Access Token</strong></div>
|
||
<pre><?= htmlspecialchars($accTok) ?></pre>
|
||
</div>
|
||
</details>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-4">
|
||
<div class="card-header">Claims</div>
|
||
<div class="card-body">
|
||
<dl class="kv">
|
||
<dt>Email</dt><dd><?= htmlspecialchars((string)($claims['email'] ?? '')) ?></dd>
|
||
<dt>Preferred username</dt><dd><?= htmlspecialchars((string)($claims['preferred_username'] ?? '')) ?></dd>
|
||
<dt>Given name</dt><dd><?= htmlspecialchars((string)($claims['given_name'] ?? '')) ?></dd>
|
||
<dt>Family name</dt><dd><?= htmlspecialchars((string)($claims['family_name'] ?? '')) ?></dd>
|
||
<dt>Name</dt><dd><?= htmlspecialchars((string)($claims['name'] ?? '')) ?></dd>
|
||
<dt>Locale</dt><dd><?= htmlspecialchars((string)($claims['locale'] ?? '')) ?></dd>
|
||
<dt>Rôles</dt>
|
||
<dd>
|
||
<?php if ($roles): ?>
|
||
<ul class="mb-0">
|
||
<?php foreach ($roles as $r): ?>
|
||
<li><?= htmlspecialchars((string)$r) ?></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php else: ?>
|
||
—
|
||
<?php endif; ?>
|
||
</dd>
|
||
</dl>
|
||
|
||
<?php if ($debugEnabled): ?>
|
||
<h6 class="mt-3">Claims (JSON complet)</h6>
|
||
<pre><?= htmlspecialchars(json_encode($claims, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ?></pre>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!$claims): ?>
|
||
<div class="alert alert-info mt-3">
|
||
Aucun claim reçu. Vérifie que ton <code>callback</code> remplit bien <code>$_SESSION['oidc_userinfo']</code> ou que l’<code>ID Token</code> contient les champs.
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<a class="btn btn-secondary" href="<?= htmlspecialchars(url('')) ?>">Retour</a>
|
||
</body>
|
||
</html>
|