efihub/docs/superpowers/specs/2026-04-27-auth-part1-design.md
2026-04-27 09:16:55 +02:00

5.2 KiB

Design Spec: Benutzerverwaltung Part 1 — Core Auth

Datum: 2026-04-27 Status: Genehmigt


Ziel

Lokale Benutzerverwaltung mit JWT-basierter Session (httpOnly-Cookie) und optionalem LDAP-Fallback. Part 1 implementiert das User-Datenmodell, lokale Passwort-Auth und die FastAPI-Dependencies. Part 2 (separater Spec) ergänzt LDAP-Auth und Celery-Hintergrund-Sync.


Datenmodell

Einzige Tabelle users:

Feld SQLAlchemy-Typ Details
id Integer, PK Auto-increment
username String(64), unique, not null sAMAccountName / lokaler Login
full_name String(128), not null Vor- + Nachname
email String(128), nullable optional
department String(64), nullable aus AD (Part 2)
role String(64), nullable aus AD description-Feld (Part 2)
pw_hash String(256), nullable None = kein lokaler Login möglich
is_active Boolean, default True gesperrte Accounts
is_admin Boolean, default False Admin-Rechte
account_expires DateTime, nullable aus AD; None = kein Ablauf
last_login DateTime, nullable bei jedem Login aktualisiert
created_at DateTime, server_default automatisch
updated_at DateTime, onupdate automatisch

pw_hash is not None → lokale Auth verfügbar. Kein separates auth_source-Flag.


Auth-Flow

POST /auth/login (username + password als Form-Daten)
│
├─► Nutzer in DB vorhanden und is_active?
│     └─► Nein → 401
│
├─► pw_hash gesetzt?
│     ├─► Ja → passlib bcrypt.verify(password, pw_hash)
│     │         ├─► OK → Login erfolgreich ✓
│     │         └─► Falsch → weiter zu LDAP (falls aktiviert)
│     └─► Nein → weiter zu LDAP (falls aktiviert)
│
├─► LDAP aktiviert? (settings.LDAP_ENABLED == True, gesetzt wenn APP_ENV != "development")
│     ├─► Ja → ldap3 NTLM-Bind gegen gso1.ads1.fh-nuernberg.de
│     │         ├─► OK → AD-Felder in User-Eintrag schreiben,
│     │         │         last_login setzen → Login erfolgreich ✓
│     │         └─► Fehler → 401
│     └─► Nein → 401
│
└─► Bei Erfolg: JWT erstellen → httpOnly-Cookie "access_token" setzen → Redirect /

Timing-Attack-Schutz: Wenn pw_hash gesetzt aber falsch, trotzdem LDAP probieren — kein frühes Abbrechen das Timing-Unterschiede verrät.


JWT

  • Library: python-jose[cryptography]
  • Algorithmus: HS256
  • Claims: sub (username), exp (jetzt + 8h), is_admin (bool)
  • Secret: settings.SECRET_KEY (Zufallsstring, in .env gesetzt)
  • Cookie: access_token, httpOnly=True, samesite="lax", secure=True wenn APP_ENV == "production"

Konfiguration (Ergänzungen in app/core/config.py)

SECRET_KEY: str = "changeme"
LDAP_ENABLED: bool = False          # True wenn APP_ENV != "development"
LDAP_SERVER: str = "gso1.ads1.fh-nuernberg.de"
LDAP_DOMAIN: str = "ADS1"
LDAP_SEARCH_BASE: str = "OU=users,OU=EFI,OU=Faculties,DC=ADS1,DC=fh-nuernberg,DC=de"

LDAP_ENABLED kann in .env explizit überschrieben werden.


Dateistruktur

app/
├── core/
│   ├── config.py           # Ergänzung: SECRET_KEY, LDAP_*
│   ├── database.py         # neu: Engine, SessionLocal, Base, get_db()
│   └── auth.py             # neu: JWT erstellen/lesen, Cookie setzen/löschen
├── modules/
│   └── auth/
│       ├── __init__.py
│       ├── models.py       # SQLAlchemy User-Model
│       ├── schemas.py      # Pydantic: LoginForm, UserOut
│       ├── service.py      # verify_password, authenticate_user
│       ├── router.py       # GET/POST /auth/login, GET /auth/logout
│       └── dependencies.py # get_current_user, require_admin
├── templates/
│   └── auth/
│       └── login.html      # Login-Formular (extends base.html)
alembic/
├── alembic.ini
├── env.py
└── versions/
    └── 0001_create_users.py

Endpunkte

Methode Pfad Beschreibung
GET /auth/login Login-Formular
POST /auth/login Auth-Logik, Cookie setzen, Redirect /
GET /auth/logout Cookie löschen, Redirect /auth/login
GET /auth/me Aktueller User als JSON (für API-Clients)

FastAPI-Dependencies

  • get_current_user(request: Request, db: Session = Depends(get_db)) -> User Liest JWT aus Cookie access_token, dekodiert, lädt User aus DB. Wirft RedirectResponse("/auth/login") wenn kein/ungültiges Token.

  • require_admin(user: User = Depends(get_current_user)) -> User Prüft user.is_admin, wirft HTTPException(403) wenn False.


Login-Template

Minimales Formular, extends base.html. Felder: username, password. Fehlermeldung wenn Auth fehlschlägt (kein Detail welche Methode fehlschlug — Sicherheit).


Neue Dependencies

alembic
python-jose[cryptography]
passlib[bcrypt]
python-multipart
ldap3

Out of Scope (Part 1)

  • LDAP-Hintergrund-Sync (→ Part 2)
  • Passwort-Reset / Registrierungs-UI
  • Admin-CRUD-UI für User-Verwaltung (→ sqladmin, separater Spec)
  • Refresh-Tokens