Zum Inhalt

Authentifizierung & Authority

Das MoE-System unterscheidet zwei vollständig getrennte Authentifizierungsebenen:

Ebene Zugangspunkt Nutzer
Admin-Backend /login Admins (mit is_admin = 1)
User-Portal /user/login Endnutzer (alle Rollen)

Admin-Authentifizierung

Lokale Anmeldung

  1. Formular ausfüllen: Benutzername + Passwort + CSRF-Token
  2. Passwort-Prüfung: bcrypt-Vergleich via passlib
  3. is_admin = 1 muss gesetzt sein — normale User werden abgewiesen
  4. Session wird angelegt:
    session["authenticated"] = True
    session["user"] = username
    session["admin_user_id"] = user_id
    

OIDC / Authentik (optional)

Erfordert folgende Umgebungsvariablen:

Variable Beschreibung
AUTHENTIK_URL Basis-URL der Authentik-Instanz
OIDC_CLIENT_ID OAuth2-Client-ID
OIDC_CLIENT_SECRET OAuth2-Client-Secret

Flow:

Admin öffnet /login
Button "Mit Authentik anmelden (SSO)"
GET /auth/login → Redirect zu Authentik /application/o/authorize/
        │  Scopes: openid profile email groups
User authentifiziert sich bei Authentik
GET /auth/callback?code=... → Token-Exchange /application/o/token/
Admin-Check: user muss in Gruppe "moe-admins" ODER is_superuser=true
Session anlegen + OIDC-Token speichern

Admin-Prüfung bei OIDC:

is_admin = "moe-admins" in userinfo.get("groups", []) or userinfo.get("is_superuser", False)

Logout

  • Lokale Anmeldung: Session löschen
  • OIDC: Session löschen + Redirect zu Authentik end-session/ Endpunkt

User-Portal-Authentifizierung

Anmeldung

Formular unter /user/login:

  1. Benutzername + Passwort eingeben
  2. bcrypt-Passwort-Vergleich
  3. is_active = 1 wird geprüft (gesperrte User werden abgewiesen)
  4. Session:
    session["user_authenticated"] = True
    session["user_id"] = user_id
    session["username"] = username
    session["user_role"] = role
    

Passwort-Reset-Flow

/user/forgot-password
    │  E-Mail-Adresse eingeben
    Token generieren (secrets.token_urlsafe(32))
    Token-Hash in DB: password_reset_tokens (TTL 1 Stunde, single-use)
    E-Mail versenden (wenn SMTP konfiguriert)
/user/reset-password?token=...
    │  Token validieren (nicht abgelaufen, noch nicht genutzt)
    Neues Passwort eingeben (min. 8 Zeichen)
    bcrypt-Hash speichern
    Token als used markieren

Sicherheitsverhalten

Bei /user/forgot-password zeigt das System immer die Meldung „Falls ein Account existiert..." — unabhängig davon, ob die E-Mail tatsächlich registriert ist. Dadurch werden Account-Enumerations verhindert.


API-Key-Authentifizierung

Für API-Anfragen an den Orchestrator werden API-Keys verwendet. Kein Session-Cookie, kein Login — stateless.

Unterstützte Header

Authorization: Bearer moe-sk-{48 hex chars}
x-api-key: moe-sk-{48 hex chars}

Validierungsablauf

Incoming Request
Header parsen → Key extrahieren
SHA-256 berechnen: hash = sha256(key)
Redis-Lookup: GET user:apikey:{hash}
        │                              │
    Cache Hit                      Cache Miss
    TTL 5 Minuten                  SQLite-Lookup:
                                   SELECT ... FROM api_keys
                                   WHERE key_hash = ? AND is_active = 1
                                   JOIN users WHERE is_active = 1
                                   Redis-Cache befüllen (TTL 300s)
User-Objekt prüfen:
  - is_active == 1?
  - Budget nicht überschritten?
  - Permissions für angeforderte Ressource?
Request ausführen oder ablehnen (401/403/429)

Redis-Schema

user:apikey:{sha256-hash}   →   HASH
    user_id          STRING
    username         STRING
    role             STRING   (user|subscriber|expert|admin)
    is_active        STRING   (1|0)
    daily_limit      STRING   (Integer oder leer = unlimitiert)
    monthly_limit    STRING
    total_limit      STRING
    permissions      STRING   (JSON: {resource_type: [id, ...]})
    cost_factor      STRING   (Float)

TTL: 300 Sekunden

CSRF-Schutz

Alle Formulare im Admin-Backend und User-Portal sind CSRF-geschützt:

<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
  • Token generiert per Session: secrets.token_hex(16)
  • Validiert Server-seitig mit secrets.compare_digest()
  • Session-Lebensdauer: max. 8 Stunden (SESSION_MAX_AGE = 28800)

Session-Konfiguration

SESSION_MAX_AGE = 28800  # 8 Stunden
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "lax"

Admin-Impersonation

Admins können User-Sessions übernehmen:

GET /admin/users/{uid}/impersonate
  1. Admin-Session wird geprüft (is_admin = 1)
  2. User-Session wird für die laufende Sitzung gesetzt:
    session["user_authenticated"] = True
    session["user_id"] = uid
    session["admin_impersonating"] = True
    
  3. Redirect zu /user/dashboard
  4. Oranges Impersonation-Banner erscheint

Beenden:

GET /user/impersonate/exit
Setzt Admin-Session zurück, löscht User-Impersonation-Flags.


Sicherheitsübersicht

Mechanismus Implementierung
Passwort-Hashing bcrypt (passlib)
API-Key-Speicherung SHA-256 Hash, nie Klartext
CSRF-Schutz HMAC-basierter Session-Token
Session-TTL 8 Stunden
Redis-Cache-TTL 5 Minuten (API-Keys)
OIDC-Gruppe moe-admins
Password-Reset-TTL 1 Stunde, single-use
Budget-Enforcement Redis-Counter + Orchestrator-Check