Files
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- 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>
2026-07-08 02:27:02 +02:00

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;
}