1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
108 lines
3.5 KiB
PHP
108 lines
3.5 KiB
PHP
<?php
|
|
/**
|
|
* V2-Plattform — Auth
|
|
*
|
|
* Stateless JWT-light: Header.Payload.Signature mit HMAC-SHA256.
|
|
* Nicht voller JWT-Standard (keine RS256), reicht für interne Plattform.
|
|
*
|
|
* Token-Payload:
|
|
* {
|
|
* "sub": <student_id|teacher_id>,
|
|
* "role": "student" | "teacher",
|
|
* "class_id": <int|null>, // bei student
|
|
* "exp": <unix-timestamp>
|
|
* }
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
function v2_auth_b64url(string $bin): string {
|
|
return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
|
|
}
|
|
|
|
function v2_auth_b64url_decode(string $s): string {
|
|
$pad = strlen($s) % 4;
|
|
if ($pad) $s .= str_repeat('=', 4 - $pad);
|
|
return base64_decode(strtr($s, '-_', '+/')) ?: '';
|
|
}
|
|
|
|
function v2_auth_token_create(array $payload): string {
|
|
$payload['iat'] = time();
|
|
$payload['exp'] = time() + V2_JWT_TTL;
|
|
|
|
$header = ['alg' => 'HS256', 'typ' => 'JWT'];
|
|
$h = v2_auth_b64url(json_encode($header, JSON_UNESCAPED_UNICODE));
|
|
$p = v2_auth_b64url(json_encode($payload, JSON_UNESCAPED_UNICODE));
|
|
$sig = v2_auth_b64url(hash_hmac('sha256', "$h.$p", V2_JWT_SECRET, true));
|
|
return "$h.$p.$sig";
|
|
}
|
|
|
|
function v2_auth_token_verify(string $token): ?array {
|
|
$parts = explode('.', $token);
|
|
if (count($parts) !== 3) return null;
|
|
[$h, $p, $sig] = $parts;
|
|
$expected = v2_auth_b64url(hash_hmac('sha256', "$h.$p", V2_JWT_SECRET, true));
|
|
if (!hash_equals($expected, $sig)) return null;
|
|
$payload = json_decode(v2_auth_b64url_decode($p), true);
|
|
if (!is_array($payload)) return null;
|
|
if (($payload['exp'] ?? 0) < time()) return null;
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Aktuellen Token aus Authorization-Header oder ?session_token=...
|
|
* holen und validieren. Gibt Payload oder null zurück.
|
|
*
|
|
* Hinweis: Apache reicht den Authorization-Header je nach Setup nicht
|
|
* an PHP weiter. Wir probieren mehrere Quellen.
|
|
*/
|
|
function v2_auth_current(): ?array {
|
|
// 1) Standard
|
|
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
|
// 2) Über mod_rewrite weitergereicht (siehe .htaccess)
|
|
if (!$auth) $auth = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
|
|
// 3) Apache getallheaders Fallback (case-insensitive)
|
|
if (!$auth && function_exists('getallheaders')) {
|
|
foreach (getallheaders() as $name => $value) {
|
|
if (strcasecmp($name, 'Authorization') === 0) {
|
|
$auth = $value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (str_starts_with($auth, 'Bearer ')) {
|
|
$token = substr($auth, 7);
|
|
} else {
|
|
$token = $_GET['session_token'] ?? $_POST['session_token'] ?? '';
|
|
}
|
|
if (!$token) return null;
|
|
return v2_auth_token_verify($token);
|
|
}
|
|
|
|
/**
|
|
* Schutz für Endpoints, die Auth verlangen.
|
|
* Bei fehlendem/ungültigem Token: 401 + exit.
|
|
*
|
|
* $role kann sein:
|
|
* - "any" → jede authentifizierte Rolle
|
|
* - "student" → nur Schüler*in
|
|
* - "teacher" → Lehrperson ODER Admin (Admin ist immer auch Lehrperson)
|
|
* - "admin" → nur Admin
|
|
*/
|
|
function v2_auth_require(string $role = 'student'): array {
|
|
$payload = v2_auth_current();
|
|
if (!$payload) {
|
|
v2_response_error(401, 'unauthorized', 'Token fehlt oder ist ungültig.');
|
|
}
|
|
$actual = (string) ($payload['role'] ?? '');
|
|
$ok = match ($role) {
|
|
'any' => true,
|
|
'teacher' => in_array($actual, ['teacher', 'admin'], true),
|
|
default => $actual === $role,
|
|
};
|
|
if (!$ok) {
|
|
v2_response_error(403, 'forbidden', "Rolle '$role' erforderlich (du bist '$actual').");
|
|
}
|
|
return $payload;
|
|
}
|