Anmeldung wahlweise ueber OpenID Connect (Authentik)
Neben dem Passwort-Login gibt es jetzt einen zweiten, gleichberechtigten Weg: OpenID Connect gegen einen selbst gehosteten Anmeldedienst. Ohne Konfiguration aendert sich nichts - keine SSO-Knoepfe, Passwort-Login wie bisher. Der Anmeldedienst ist NUR fuer die Anmeldung zustaendig. Danach stellt Vorrania sein eigenes Token aus wie beim Passwort-Login, deshalb bleiben deps.py, die Rollenpruefung und die langlebigen API-Tokens unveraendert. Backend - Anbieter kommen aus der Umgebung (OIDC_PROVIDERS + OIDC_<KUERZEL>_*). Ein zweiter Anbieter ist damit reine Konfiguration, ohne Codeaenderung. - Browser-Weg (vertraulicher Client) und App-Weg (oeffentlicher Client, PKCE) enden beide im selben Token-Schema. - state/nonce/PKCE-Verifier liegen in einer kurzlebigen Tabelle statt in einem Cookie: Starlettes CORS-Schicht spiegelt bei allow_origins=["*"] zusammen mit allow_credentials die Herkunft zurueck, sobald ein Request ein Cookie traegt. Ohne Cookie entsteht diese Wechselwirkung erst gar nicht - und der Ablauf bleibt mit der db-Fixture pruefbar. - Zuordnung ueber die sub des Anbieters; die Erstverknuepfung laeuft ueber die BESTAETIGTE E-Mail-Adresse. Rollen kommen nie aus dem Anbieter, und ein gleichlautender preferred_username uebernimmt nie ein bestehendes Konto. - users.password_hash darf jetzt leer sein (reine SSO-Konten). verify_password faengt das ab, sonst antwortete der Login mit 500 statt 401. Verknuepfen bestehender Konten - Neue Seite "Mein Konto" (fuer alle Benutzer, nicht nur Admins): verknuepfen und wieder loesen. Die Berechtigung stammt aus der laufenden Sitzung, es braucht also weder eine hinterlegte Adresse noch Admin-Rechte. - Der Start laeuft ueber POST, weil eine Seitennavigation das Bearer-Token nicht mitschicken kann. - Die letzte Anmeldemoeglichkeit eines Kontos laesst sich nicht loesen. iOS - AppAuth (erste Paketabhaengigkeit) mit PKCE im System-Browser, nie in einem eingebetteten WebView, und ohne Client-Secret. - offline_access nur im App-Weg: der Browser-Weg braucht kein Refresh-Token und bekaeme sonst einen ueberfluessigen Zustimmungsdialog. - Anmeldezustand je Serverprofil im Keychain; abgelaufene Token erneuert die App still im Hintergrund. Gleichzeitige 401er teilen sich eine Erneuerung. Deployment - docker-compose reicht die ganze .env durch. Einzeln aufzuzaehlen ginge nur mit fest verdrahteten Kuerzeln - genau das soll ein weiterer Anbieter nicht erfordern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8d4954610b
commit
3d0ff54bf5
@@ -93,6 +93,7 @@ class UserOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
username: str
|
||||
email: str | None = None
|
||||
role: Role
|
||||
created_at: datetime
|
||||
|
||||
@@ -100,14 +101,69 @@ class UserOut(BaseModel):
|
||||
class UserCreate(BaseModel):
|
||||
username: str = Field(min_length=1, max_length=64)
|
||||
password: str = Field(min_length=4, max_length=255)
|
||||
email: str | None = Field(default=None, max_length=255)
|
||||
role: Role = Role.user
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
password: str | None = Field(default=None, min_length=4, max_length=255)
|
||||
# Leerer String heisst „Adresse entfernen" – deshalb hier nicht min_length.
|
||||
email: str | None = Field(default=None, max_length=255)
|
||||
role: Role | None = None
|
||||
|
||||
|
||||
# ---- Single Sign-on (OpenID Connect) ----
|
||||
class OidcProviderOut(BaseModel):
|
||||
"""Was ein Client braucht, um den Anmeldeknopf zu zeigen und den Flow zu starten.
|
||||
|
||||
Bewusst oeffentlich abrufbar: die Web-Oberflaeche kennt keine Build-Variablen,
|
||||
und die iOS-App muss die Angaben je Serverprofil vom jeweiligen Server lernen.
|
||||
Ein Client-Secret steht hier nie drin.
|
||||
"""
|
||||
|
||||
slug: str
|
||||
display_name: str
|
||||
discovery_url: str
|
||||
# Client-ID fuer die App (oeffentlicher Client, PKCE). Leer, wenn fuer diesen
|
||||
# Anbieter kein nativer Client eingerichtet ist.
|
||||
native_client_id: str | None = None
|
||||
native_redirect_uri: str
|
||||
# Scopes fuer den App-Weg (inkl. offline_access) – der Browser-Weg wird vom
|
||||
# Server selbst gebaut und braucht diese Angabe nicht.
|
||||
scopes: str
|
||||
|
||||
|
||||
class OidcIdentityOut(BaseModel):
|
||||
"""Eine bestehende Verknuepfung des angemeldeten Kontos."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
provider: str
|
||||
display_name: str
|
||||
created_at: datetime
|
||||
last_login_at: datetime | None = None
|
||||
|
||||
|
||||
class OidcLinkStart(BaseModel):
|
||||
"""Adresse, zu der die Oberflaeche fuer die Verknuepfung weiterleiten soll.
|
||||
|
||||
Der Start laeuft ueber POST und nicht als einfacher Link, weil die
|
||||
Berechtigung aus dem Bearer-Token der laufenden Sitzung kommt – und das
|
||||
laesst sich bei einer normalen Seitennavigation nicht mitschicken.
|
||||
"""
|
||||
|
||||
authorization_url: str
|
||||
|
||||
|
||||
class OidcNativeLogin(BaseModel):
|
||||
"""ID-Token aus dem App-Flow, das gegen ein Vorrania-Token getauscht wird."""
|
||||
|
||||
id_token: str = Field(min_length=1)
|
||||
# Die nonce, die die App in ihrer Anfrage verwendet hat. AppAuth prueft sie
|
||||
# zwar selbst, aber nur der Server-Vergleich schliesst aus, dass ein anderswo
|
||||
# abgegriffenes ID-Token desselben Clients hier eingereicht wird.
|
||||
nonce: str | None = None
|
||||
|
||||
|
||||
# ---- Groups ----
|
||||
class LocationMinStockIn(BaseModel):
|
||||
"""Ein Mindestbestand-Eintrag je Lagerort (Menge in Basiseinheiten).
|
||||
|
||||
Reference in New Issue
Block a user