Major SKILL.md update: lessons learned from real integrations - Add IdP-initiated SSO handling (Admin-Panel sends ?code= only, no state) → state missing must NOT trigger 400, otherwise admin-panel app tiles break - Add Middleware-Reihenfolge warning (SessionMiddleware LIFO footgun) - Add Auth-Guard distinction: HTML-Navigation 307 vs JSON-API 401 - Add Frontend 401-handling pattern (hard navigation, not fetch redirect) - Add stdlib-only HTTP + JWT-decode helpers (urllib.request + base64url padding) - Add Session-Storage options table (in-memory / SessionMiddleware / Redis) - Add CLIENT_SECRET generation guidance (admin-panel vs locally) - Add Smoke-Test mit curl (5 calls catching 90% of typical bugs) - Add "replace existing auth" checklist (remove old constants/routes/HTML) - Cookie flags: explain why samesite=lax (not strict) for OIDC callback - Logout: two separate functions, FastAPI doesn't support decorator stacking - Hard restart guidance for middleware iteration - Many additional Fallstricke entries from real bugs
24 KiB
name, description
| name | description |
|---|---|
| onyva-login | Integriert den zentralen Onyva Dev Login (OIDC/SSO) in ein bestehendes Projekt. Nutze diesen Skill wenn jemand "onyva login integrieren", "auth integrieren" oder "/onyva-login" schreibt. |
Deine Aufgabe ist es, den zentralen Onyva Dev Login (OIDC/SSO-Server unter https://login.onyva.dev) in das aktuelle Projekt zu integrieren.
Vorgehen
-
Analysiere das Projekt zuerst — Lies relevante Dateien (Framework, Routing, vorhandene Auth-Logik, Env-Konfiguration), bevor du Code schreibst. Achte besonders auf bereits genutzten HTTP-Stack (
httpx/requests/stdliburllib) und vorhandene Session-Middleware. -
Frage nach fehlenden Infos — Du brauchst:
OIDC_CLIENT_ID(registrierter Slug im Admin-Panel, kein Zufallswert — z.B.mein-app-name)OIDC_CLIENT_SECRET(siehe "Wer erzeugt CLIENT_SECRET?" unten)OIDC_REDIRECT_URI(wohin der Auth-Server nach dem Login zurückleitet — muss im Admin-Panel exakt eingetragen sein, inkl. Port und Pfad, ohne trailing slash)OIDC_LOGOUT_REDIRECT_URI(wohin nach dem zentralen Logout weitergeleitet wird)- Falls noch nicht bekannt: welche Routen geschützt sein sollen und welche öffentlich bleiben
-
Bei bestehendem Auth-System: alles Alte raus, bevor das Neue rein kommt. Sonst existieren beide Systeme parallel. Typische Stellen:
- Alte Auth-Konstanten (
APP_USERNAME,APP_PASSWORD, eigene Session-Secrets) - Login-HTML/Form-Funktionen
POST /login-Route und der dahinterliegende Credential-Check- Session-Token-Helper (HMAC-Signaturen o.ä.) — außer du kannst das HMAC-Schema für den neuen Session-Cookie wiederverwenden
- Alte
.env-Variablen aus.env.exampleentfernen - Verweise im Frontend (z.B. Logo-Pfade in der Login-Page, Logout-Button-Targets)
- Alte Auth-Konstanten (
-
Implementiere die Integration nach den technischen Vorgaben unten.
-
Passe
.env.example(oder äquivalente Konfiguration) mit allen nötigen Variablen an. -
Smoke-Test mit curl (siehe Smoke-Test in einer Zeile) — die fünf Calls fangen 90% der typischen Bugs ab, bevor der User es im Browser klickt.
Technische Vorgaben
Auth-Server
- Issuer / Basis-URL:
https://login.onyva.dev - Discovery:
https://login.onyva.dev/.well-known/openid-configuration
| Endpunkt | Methode | Zweck |
|---|---|---|
/oauth/authorize |
GET | Login starten |
/oauth/token |
POST | Code gegen Tokens tauschen |
/oauth/userinfo |
GET | Nutzerinfos per Bearer Token |
/oauth/jwks.json |
GET | Public Keys (RS256) |
/logout |
GET | Zentrales Logout |
⚠ Starlette/FastAPI: Middleware-Reihenfolge (der häufigste Footgun)
app.add_middleware() macht intern ein insert(0, ...) — heißt: das zuletzt registrierte Middleware liegt im Stack außen. Wenn deine Auth-Guard request.session lesen will, muss SessionMiddleware außen um die Guard liegen → SessionMiddleware NACH dem @app.middleware("http")-Decorator registrieren.
# ❌ FALSCH — SessionMiddleware zuerst → liegt INNEN → Guard knallt mit
# AssertionError: SessionMiddleware must be installed to access request.session
app.add_middleware(SessionMiddleware, secret_key=...)
@app.middleware("http")
async def auth_guard(request, call_next):
user = request.session.get("user") # 💥
# ✅ RICHTIG — Guard zuerst, SessionMiddleware danach → liegt AUSSEN
@app.middleware("http")
async def auth_guard(request, call_next):
user = request.session.get("user") # funktioniert
...
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET, ...)
Symptom in den Logs: AssertionError: SessionMiddleware must be installed to access request.session für jeden geschützten Request. Wenn du das siehst → 99% Reihenfolge.
Login starten
Generiere state (≥16 Zeichen, zufällig) und code_verifier (PKCE, 43–128 Zeichen), speichere beide kurzlebig zwischen Login-Start und Callback (siehe Session-Storage). Dann Weiterleitung zu:
GET https://login.onyva.dev/oauth/authorize
?response_type=code
&client_id={OIDC_CLIENT_ID}
&redirect_uri={OIDC_REDIRECT_URI}
&scope=openid profile email
&state={state}
&code_challenge={BASE64URL(SHA256(code_verifier))}
&code_challenge_method=S256
Wichtig: state muss nachweislich in der URL stehen — ohne state kein state im Callback.
Konkretes Python-stdlib-Beispiel (funktioniert ohne Zusatz-Dependencies):
import secrets, hashlib, base64
from urllib.parse import urlencode
# OIDC_ISSUER defensiv trimmen — .env kann versehentlich mit '/' enden
OIDC_ISSUER = os.getenv("OIDC_ISSUER", "https://login.onyva.dev").rstrip("/")
def auth_login():
state = secrets.token_urlsafe(24)
code_verifier = secrets.token_urlsafe(64) # 86 Zeichen — innerhalb 43-128
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
save_pending(state, code_verifier) # siehe Session-Storage
params = urlencode({
"response_type": "code",
"client_id": OIDC_CLIENT_ID,
"redirect_uri": OIDC_REDIRECT_URI,
"scope": "openid profile email",
"state": state,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
})
return redirect(f"{OIDC_ISSUER}/oauth/authorize?{params}")
Session-Storage für State + PKCE-Verifier
Du musst state und code_verifier zwischen Login-Start und Callback überleben lassen. Drei realistische Optionen — wähle nach Setup:
| Option | Geeignet für | Aufwand |
|---|---|---|
In-Memory dict mit threading.Lock |
Single-Instance-Apps; geht beim Neustart verloren (egal — der User klickt einfach nochmal Login) | Keine neuen Deps |
SessionMiddleware (Starlette/FastAPI) |
Stateless, multi-instance, persistiert über Neustarts | Benötigt itsdangerous |
| Externer Store (Redis/DB) | Mehrere App-Instanzen ohne Cookie-Sessions | Infrastruktur |
Pragma: für die meisten internen Onyva-Apps reicht Option 1. Wenn du SessionMiddleware verwendest, kannst du dieselbe Session sowohl für PKCE-State als auch für die User-Session nach Login nutzen — dann brauchst du nichts extra.
Callback verarbeiten
Zwei Eintrittsformen in den Callback — beide musst du unterstützen:
- SP-initiated (Standard): User klickt in deiner App auf „Login" →
/auth/loginbaut state + PKCE → Redirect zu/oauth/authorize→ Auth-Server schickt zurück mit?code=...&state=.... State + Verifier sind in deiner Session. - IdP-initiated (vom Admin-Panel von login.onyva.dev): User klickt dort auf eine App-Kachel → Auth-Server schickt direkt mit
?code=...(ohnestate, ohne dass du/auth/logindurchlaufen hast). Session hat nichts gespeichert.
Erkennung: state fehlt im Query → IdP-initiated. In diesem Fall State-Check überspringen und beim Token-Tausch kein code_verifier mitschicken. Sicherheitsrechtfertigung: bei IdP-initiated kommt der Code aus einer vertrauenswürdigen Quelle (login.onyva.dev), die der User soeben aktiv gewählt hat — die State-CSRF-Begründung greift hier nicht. Siehe "IdP-initiated"-Fallstrick.
# Pseudocode — an das Framework anpassen
def callback(request):
# 0. Sonderfall: Auth deaktiviert (kann state UND code-frei kommen)
if "auth_disabled" in request.params:
session["user"] = {"auth_disabled": True}
return redirect("/")
# 1. code ist IMMER Pflicht — fehlt er, ist es kein gültiger Callback
code = request.params.get("code")
if not code:
return error("Kein Authorization Code", status=400)
# 2. state ist NUR bei SP-initiated da. Fehlt er → IdP-initiated (Admin-Panel-Kachel)
received_state = request.params.get("state")
idp_initiated = received_state is None
code_verifier = pop_pending(received_state) if received_state else None
# 3. State validieren — nur bei SP-initiated. Bei IdP-initiated NICHT mit
# "State-Mismatch" abbrechen, sonst kann das Admin-Panel keine User reinschicken.
if not idp_initiated and code_verifier is None:
return error("State mismatch")
# 4. Token-Tausch (application/x-www-form-urlencoded, kein JSON)
token_body = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": OIDC_REDIRECT_URI,
"client_id": OIDC_CLIENT_ID,
"client_secret": OIDC_CLIENT_SECRET,
}
# PKCE-Verifier NUR beim SP-initiated Flow mitschicken — bei IdP-initiated
# haben wir keinen Verifier und der Server erwartet ihn auch nicht.
if not idp_initiated and code_verifier:
token_body["code_verifier"] = code_verifier
resp = http_post("https://login.onyva.dev/oauth/token", data=token_body)
tokens = resp.json()
# 4. Claims aus id_token lesen
claims = decode_jwt_claims(tokens["id_token"]) # ohne Signaturprüfung ok für interne Nutzung
session["user"] = {
"sub": claims["sub"], # "user:42"
"email": claims["email"],
"name": claims["name"],
"user_type": claims["user_type"], # "employee" | "customer"
"app_roles": claims["app_roles"], # ["admin"] | ["editor"] | ["viewer"] | []
}
return redirect("/")
Nach erfolgreichem Callback niemals auf /auth/* zurückleiten — return_to immer auf / oder eine echte App-Seite normalisieren.
stdlib-only HTTP + JWT-Decode
Wenn das Projekt kein httpx/requests hat, reicht stdlib komplett aus:
import json, base64
from urllib import request as urllib_request
from urllib import error as urllib_error
from urllib.parse import urlencode
def http_post_form(url: str, data: dict) -> dict:
req = urllib_request.Request(
url,
data=urlencode(data).encode(),
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
try:
with urllib_request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
except urllib_error.HTTPError as exc:
# 4xx/5xx vom Auth-Server — body hat den eigentlichen Fehler
body = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"Token endpoint {exc.code}: {body}")
def decode_jwt_claims(token: str) -> dict:
"""Liest den Payload ohne Signaturprüfung — ok für interne Apps.
Bei extern verteilten Tokens: gegen jwks.json verifizieren."""
payload_b64 = token.split(".")[1]
payload_b64 += "=" * (-len(payload_b64) % 4) # base64url braucht Padding!
return json.loads(base64.urlsafe_b64decode(payload_b64).decode())
Auth-Guard: HTML-Navigation vs. JSON-API
Der Guard muss unauthentifizierte Requests unterschiedlich behandeln:
- HTML-Navigation (Browser-GET einer Seite) →
307 Redirectnach/auth/login - JSON-/API-Call (
fetch(), XHR) →401 Unauthorized, kein Redirect
Grund: fetch() und XMLHttpRequest folgen 3xx-Redirects transparent. Wenn du eine API mit 307 → /auth/login antwortest, navigiert der Client zur OIDC-Authorize-Page (HTML) — der Request schlägt als "JSON-Parse-Fehler" fehl, und die richtige Aktion (User zum Login schicken) bleibt aus. Mit 401 weiß der Frontend-Code, dass er selbst eine Top-Level-Navigation auslösen muss.
@app.middleware("http")
async def auth_guard(request: Request, call_next):
path = request.url.path
if path in PUBLIC_PATHS or path.startswith("/auth/"):
return await call_next(request)
if not AUTH_ENABLED:
request.state.user = DEV_USER
return await call_next(request)
user = request.session.get("user")
if not user:
accept = request.headers.get("accept", "")
# Nur echte HTML-GET-Navigation redirecten — alles andere 401
if request.method == "GET" and "text/html" in accept:
return RedirectResponse(url="/auth/login")
return JSONResponse({"detail": "Unauthorized"}, status_code=401)
request.state.user = user
return await call_next(request)
Frontend: 401 abfangen
Im API-Wrapper / XHR-Handler 401 explizit prüfen und mit harter Navigation reagieren (window.location.href, nicht fetch() oder History-API):
async function api(url, init) {
const r = await fetch(url, init);
if (r.status === 401) {
window.location.href = '/auth/login';
throw new Error('Session abgelaufen');
}
if (!r.ok) throw new Error(await r.text());
return r.json();
}
// Bei XHR analog im load-Event:
xhr.addEventListener('load', () => {
if (xhr.status === 401) {
window.location.href = '/auth/login';
return; // wichtig: nicht resolve/reject aufrufen, Seite wird neu geladen
}
// ... normaler Response-Handling
});
User-Session nach erfolgreichem Login
Die Claims müssen über mehrere Requests hinweg verfügbar bleiben. Optionen, vom einfachsten zum komplexesten:
- Signierter Cookie mit JSON-Payload (HMAC-SHA256) — stateless, ohne neue Deps. Klein genug (<4KB), wenn du nur die wichtigen Claims speicherst.
SessionMiddleware— wenn ohnehin schon eingebunden, einfachsession["user"] = {...}.- Server-side Session-Store — wenn Daten >4KB oder oft invalidiert werden müssen.
Cookie-Flags:
httponly=Trueimmersamesite="lax"— nichtstrict, sonst geht der OIDC-Callback verloren (Cross-Site-Navigation von login.onyva.dev zurück auf deine App)secure=Truein Produktion (https),Falselokal (http)max_agesetzen (z.B.60*60*8= 8h), nicht endlose Sessions
JWT Claims
{
"sub": "user:42",
"email": "nutzer@onyva.de",
"name": "Max Mustermann",
"user_type": "employee",
"app_roles": ["admin"],
"iss": "https://login.onyva.dev",
"aud": "mein-client-id",
"exp": 1700000000
}
Logout
def logout(request):
session.clear() # bzw. response.delete_cookie(AUTH_COOKIE)
return redirect(
f"https://login.onyva.dev/logout"
f"?post_logout_redirect_uri={urllib.parse.quote(OIDC_LOGOUT_REDIRECT_URI, safe='')}"
)
post_logout_redirect_uri muss absolut sein (https://...). Der Auth-Server löscht die zentrale Session — der Nutzer ist danach auf allen Onyva-Apps ausgeloggt.
Legacy-Buttons unterstützen: Hat die bestehende App noch ein <form action="/logout" method="post">, registriere zwei Routen mit demselben Body — FastAPI lässt nicht zwei Decorators auf einer Funktion stacken (der zweite überschreibt den ersten):
async def _do_logout(request: Request):
request.session.clear()
return RedirectResponse(
f"{OIDC_ISSUER}/logout"
f"?post_logout_redirect_uri={urllib.parse.quote(OIDC_LOGOUT_REDIRECT_URI, safe='')}"
)
@app.get("/auth/logout")
async def auth_logout_get(request: Request):
return await _do_logout(request)
@app.post("/logout") # Legacy
async def auth_logout_post(request: Request):
return await _do_logout(request)
Auth-Mode-Verhalten
| auth_mode | Auth-Server-Verhalten | Client-Verhalten |
|---|---|---|
required |
403 wenn keine Mitgliedschaft | Fehlermeldung anzeigen |
optional |
Code für jeden angemeldeten Nutzer; app_roles ggf. leer |
App entscheidet selbst |
disabled |
Sofort zurück mit ?auth_disabled=1 |
Kein Login-Dialog, alle Funktionen offen |
AUTH_ENABLED=false (Entwicklungsmodus)
DEV_USER = {
"sub": "user:0", "email": "dev@local", "name": "Dev",
"user_type": "employee", "app_roles": ["admin"],
}
def require_auth(request):
if not AUTH_ENABLED:
return DEV_USER
user = request.session.get("user")
if not user:
raise HTTPException(status_code=401, detail="Unauthorized")
return user
Der Guard im Middleware-Code muss das auch prüfen — sonst redirected er trotzdem zum Login:
if not AUTH_ENABLED:
request.state.user = DEV_USER
return await call_next(request)
Wer erzeugt CLIENT_SECRET?
Zwei valide Modelle, je nach Admin-Panel-Workflow:
- Admin-Panel erzeugt — du legst nur
CLIENT_ID(Slug) an, das Panel zeigt dir das Secret einmalig nach dem Anlegen. - Du erzeugst, Admin-Panel akzeptiert — du generierst lokal:
und trägst denselben String im Admin-Panel als Secret ein.
python -c "import secrets; print(secrets.token_urlsafe(48))"
Beide funktionieren. Wichtig nur: .env und Admin-Panel müssen exakt denselben Wert haben.
Frage den User explizit, welcher der beiden Wege gilt, bevor du Werte erzeugst. Im Zweifel: in .env mit Platzhalter (OIDC_CLIENT_SECRET=PLEASE-REPLACE-FROM-ADMIN-PANEL), damit der Server lauffähig ist und der User den Wert nachträglich einträgt.
Env-Variablen
OIDC_ISSUER=https://login.onyva.dev
OIDC_CLIENT_ID= # registrierter Slug, kein Zufallswert
OIDC_CLIENT_SECRET= # aus Admin-Panel oder lokal erzeugt + dort eingetragen
OIDC_REDIRECT_URI= # exakt im Admin-Panel; bei /api-Prefix: https://app.example.com/api/auth/callback
OIDC_LOGOUT_REDIRECT_URI= # z.B. https://app.example.com/
SESSION_SECRET= # eigener Wert für SessionMiddleware-Cookie-Signatur (≥32 Zeichen)
AUTH_ENABLED=true
Beim Iterieren am Auth: harter Neustart, kein Reload
Der Middleware-Stack wird in Starlette einmal gebaut (beim ersten Request nach Startup). uvicorn --reload triggert zwar einen Neustart bei Code-Änderung, aber wenn du dir nicht sicher bist, dass der Stack wirklich neu aufgebaut wurde (oder du in derselben Datei iterativ Imports/Reihenfolge änderst): Prozess hart beenden.
# Windows (PowerShell)
Get-Process python | Stop-Process -Force
# Linux/Mac
pkill -f 'uvicorn'
Dann uvicorn neu starten und Smoke-Test laufen lassen. Wenn der Smoke-Test nicht das tut, was du gerade geschrieben hast — hast du noch einen alten Prozess offen.
Smoke-Test in einer Zeile
Nach Implementierung diese fünf Calls absetzen (Server muss laufen, hier Port 8000):
# 1. Unauth HTML-Request → 307 zu /auth/login
curl -sI -H 'Accept: text/html' http://localhost:8000/ | grep -E 'HTTP|Location'
# erwartet: HTTP/1.1 307 Temporary Redirect
# Location: /auth/login
# 2. Unauth JSON-API-Request → 401, KEIN Redirect
curl -sI -X POST -H 'Accept: application/json' http://localhost:8000/some-api | grep HTTP
# erwartet: HTTP/1.1 401 Unauthorized
# 3. /auth/login → 307 zu login.onyva.dev/oauth/authorize?... (mit state + code_challenge)
curl -sI http://localhost:8000/auth/login | grep -E 'HTTP|Location'
# erwartet: Location: https://login.onyva.dev/oauth/authorize?response_type=code&...&state=...&code_challenge=...&code_challenge_method=S256
# 4. Öffentlicher Pfad (z.B. Logo) → 200
curl -sI http://localhost:8000/logo.png | grep HTTP
# erwartet: HTTP/1.1 200 OK
# 5a. /auth/callback ohne state, ohne code → 400 ("Kein Authorization Code")
curl -sI 'http://localhost:8000/auth/callback' | grep HTTP
# erwartet: HTTP/1.1 400 Bad Request
# 5b. /auth/callback ohne state, MIT Dummy-Code → 502 (Token-Tausch lehnt ab)
# Wichtig: NICHT 400 "State-Mismatch" — sonst kann das Admin-Panel nicht
# per IdP-initiated SSO in die App schicken.
curl -sI 'http://localhost:8000/auth/callback?code=DUMMY' | grep HTTP
# erwartet: HTTP/1.1 502 Bad Gateway (mit Body: "Invalid or expired code")
# FALSCH: HTTP/1.1 400 Bad Request ← IdP-initiated SSO ist kaputt
# 5c. /auth/callback mit state aber ohne passenden Session-State → 400
curl -sI 'http://localhost:8000/auth/callback?code=x&state=invalid' | grep HTTP
# erwartet: HTTP/1.1 400 Bad Request ("State-Mismatch")
Wenn 1+2+3 wie erwartet zurückkommen, ist die Integration in 90% der Fälle korrekt verdrahtet — die restliche 10% sind dann real-Login-Flow-Themen (falsches Secret, Redirect URI nicht registriert, etc.) und passieren erst beim ersten echten Browser-Login.
Wichtige Fallstricke
- Middleware-Reihenfolge —
SessionMiddlewaremuss NACH dem@app.middleware("http")-Guard registriert werden (siehe oben). Häufigste Ursache fürAssertionError: SessionMiddleware must be installed. - State ist Pflicht — außer bei IdP-initiated SSO — wenn das Admin-Panel von login.onyva.dev den User direkt mit
?code=...(ohnestate) zum Callback schickt, fehlt absichtlich der State. Der Callback muss diesen Fall erkennen (stateim Query fehlt → IdP-initiated → State-Check + PKCE-Verifier überspringen, sonst „State-Mismatch im Auth-Callback" sobald jemand die App über das Admin-Panel öffnet). Bei SP-initiated bleibt der State-Check Pflicht. - Redirect URI exakt —
/api/auth/callback≠/auth/callback,:8000≠:8276,…/callback/≠…/callback(trailing slash!). Im Admin-Panel muss genau dieselbe URL stehen, sonst lehnt der Auth-Server den Token-Tausch ab. - CLIENT_ID ist kein Geheimnis und nicht zufällig — ein im Admin-Panel registrierter Slug (z.B.
transkript-revier). Nur das CLIENT_SECRET ist eine generierte Zufallszeichenkette. - Token-Endpunkt: Form, kein JSON —
Content-Type: application/x-www-form-urlencoded - base64url braucht Padding beim Dekodieren — JWT-Payloads oder PKCE-Werte:
s += "=" * (-len(s) % 4)vorbase64.urlsafe_b64decode(...) code_verifierLänge — PKCE-Spec verlangt 43–128 Zeichen.secrets.token_urlsafe(64)liefert 86, passt.- API-401 vs HTML-Redirect — siehe Auth-Guard. 3xx auf einen
fetch()-Call führt zu unsinnigen "JSON-Parse-Fehlern". samesite=lax, nichtstrict— sonst geht das Session-Cookie beim Cross-Site-Redirect von login.onyva.dev verloren.OIDC_ISSUERtrimmen —os.getenv("OIDC_ISSUER").rstrip("/")vor URL-Bau, sonst…//oauth/authorize.@app.get + @app.post-Stacking funktioniert nicht — für Legacy-Logout zwei separate Funktionen mit gemeinsamem Body.- Session-Middleware — bei Starlette/FastAPI:
itsdangerousals Dependency erforderlich. Falls die App das nicht hat, alternativ in-memory dict für PKCE-State. - Öffentliche Routen ausdrücklich ausnehmen — Upload-Links, Webhooks, Health-Checks müssen vom Auth Guard ausgeschlossen bleiben
- Kein Redirect auf Auth-Routen —
return_toniemals auf/auth/*setzen - Alte Auth muss komplett raus — beim Ersetzen eines bestehenden Systems: alte Konstanten, Login-HTML,
POST /login-Route, Helper und Env-Variablen vollständig entfernen, sonst hängt der User weiterhin am alten Flow - Bei Auth-Iterationen hart neustarten —
uvicorn --reloadreicht oft, aber wenn Middleware-Code/Reihenfolge geändert wurde und der Smoke-Test seltsame Resultate liefert: Prozess killen und neu starten.
Schnellcheckliste
- Bestehende Auth komplett entfernt (Konstanten, Routen, HTML, Env-Variablen)
- Env-Variablen konfiguriert und
.env.exampleaktualisiert - CLIENT_ID + CLIENT_SECRET + Redirect URI im Admin-Panel von login.onyva.dev eingetragen (exakte URL, ohne trailing slash)
- SessionMiddleware nach dem
@app.middleware-Guard registriert (LIFO!) - Login-Route:
state+ PKCE generiert, kurzlebig gespeichert, in Authorize-URL enthalten - Callback-Route:
statevalidiert (mit Pop, nicht nur Lookup), Token-Tausch (Form, nicht JSON), Claims in Session - Callback unterstützt IdP-initiated SSO — fehlt
stateim Query, State-Check + PKCE-Verifier überspringen (sonst kein Login über das Admin-Panel) - User-Session-Cookie:
httponly,samesite=lax,securein Produktion - Guard: HTML-GET → 307 redirect, alles andere → 401 JSON
- Frontend:
fetch()/XHR fängt 401 ab und machtwindow.location.href = '/auth/login' - Logout-Route: lokale Session gelöscht, Weiterleitung zu zentralem Logout — falls Legacy:
POST /logoutmit zweiter Funktion unterstützen - Auth Guard für geschützte Routen
- Öffentliche Routen explizit ausgenommen (
PUBLIC_PATHS-Set o.ä.) AUTH_ENABLED=falseBypass für Entwicklung (sowohl im Guard als auch inrequire_auth)auth_disabled=1Callback-Fall abgedeckt- Smoke-Test (5 curl-Calls) durchgelaufen
- Server hart neu gestartet (alte Prozesse beendet)