fix #29 : envoyer le lien magique par email (envoyer_mail_smtp)

This commit is contained in:
Cedric Abonnel
2026-05-13 23:41:58 +02:00
commit 8a85c15372
129 changed files with 22818 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
<?php
// projet : mug.a5l.fr
// fichier : pages/oidc/me.php
// version : 20251005
declare(strict_types=1);
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__DIR__, 2) . '/bootstrap.php';
require_once dirname(__DIR__, 2) . '/config/config.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>