Authentik Migration - LLARS Authentication¶
Status: ABGESCHLOSSEN Datum: 25. November 2025 Autor: Claude Code
Zusammenfassung¶
Die Authentik-Integration wurde erfolgreich implementiert: - Login: Funktioniert über Flow Executor API - Token: RS256-signiert, validierbar über JWKS - Validierung: Korrekte Token-Prüfung im Backend - Dev/Prod Parity: Gleiche Authentifizierungslogik in beiden Modi - SSO-Session: Authentik-Session-Cookie wird optional an den Browser weitergereicht (z. B. für Matomo)
Problem-Zusammenfassung¶
Ursprünglicher Zustand (Kritische Sicherheitsprobleme)¶
Die LLARS-Authentifizierung hatte mehrere kritische Probleme:
- Direkte Datenbank-Zugriffe: Der Login-Endpoint griff direkt auf Authentik's PostgreSQL-Datenbank zu, statt die OAuth2/OIDC-APIs zu nutzen
- HS256 JWT-Tokens: Eigene Tokens wurden mit HS256 signiert, statt Authentik's RS256-signierte Tokens zu verwenden
- Hardcoded Development-Passwort:
admin123wurde als universelles Passwort im Development-Modus akzeptiert - Dev/Prod Parity verletzt: Unterschiedliche Authentifizierungslogik für Development und Production
Gewünschter Zustand¶
- Vollständige OIDC-Integration mit Authentik
- RS256-signierte JWT-Tokens (validierbar über JWKS-Endpoint)
- Gleiche Authentifizierungslogik für Development und Production
- Kein direkter Datenbankzugriff auf Authentik
Lösung: Authentik Flow Executor API¶
Architektur¶
Frontend (Vue.js)
│
│ POST /auth/login {username, password}
▼
Backend (Flask)
│
│ 1. Start Flow (GET /api/v3/flows/executor/llars-api-authentication/)
│ 2. Submit Username (POST mit uid_field)
│ 3. Submit Password (POST mit password)
│ 4. OAuth2 Authorization Request
│ 5. Exchange Code for Token
▼
Authentik (OIDC Provider)
│
│ RS256-signiertes JWT Token
▼
Backend → Frontend
Warum Flow Executor API?¶
Authentik unterstützt keinen klassischen Resource Owner Password Credentials (ROPC) Grant. Stattdessen:
client_credentialsGrant funktioniert nur mit Service Accounts, nicht mit Benutzer-PasswörternpasswordGrant wird von Authentik wieclient_credentialsbehandelt
Die Lösung ist die Flow Executor API: 1. Programmieren des gleichen Flows, den ein Benutzer im Browser durchlaufen würde 2. Nach erfolgreicher Flow-Authentifizierung: OAuth2 Authorization Code holen 3. Code gegen Token austauschen
Implementierte Änderungen¶
1. Authentik Provider RS256 Konfiguration¶
Beide Provider wurden auf RS256 umgestellt:
# In Authentik Shell
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.crypto.models import CertificateKeyPair
cert = CertificateKeyPair.objects.filter(name__contains="Self-signed").first()
for provider in OAuth2Provider.objects.all():
provider.signing_key = cert
provider.save()
2. Custom Authentication Flow erstellt¶
Flow: llars-api-authentication
Stages:
1. IdentificationStage - Benutzer-Identifikation (ohne MFA)
2. PasswordStage - Passwort-Validierung
3. UserLoginStage - Session erstellen
3. Backend Login-Endpoint¶
Datei: app/routes/authentik_routes.py
@authentik_auth_blueprint.route('/login', methods=['POST'])
def login():
# 1. Flow starten
session.get(f"{authentik_url}/api/v3/flows/executor/{flow_slug}/")
# 2. Username submitten
session.post(flow_url, json={'uid_field': username})
# 3. Passwort submitten
session.post(flow_url, json={'password': password})
# 4. OAuth2 Authorization Code holen
session.get(auth_url, params={
'response_type': 'code',
'client_id': client_id,
'redirect_uri': redirect_uri,
'scope': 'openid profile email'
})
# 5. Code gegen Token austauschen
requests.post(token_url, data={
'grant_type': 'authorization_code',
'code': auth_code,
'redirect_uri': redirect_uri,
'client_id': client_id,
'client_secret': client_secret
})
4. SSO-Session Cookie Forwarding (optional)¶
Wenn eine Authentik-Session im Flow erzeugt wurde, wird das
authentik_session-Cookie serverseitig an den Browser weitergereicht.
So können andere OIDC-geschützte Apps (z. B. Matomo) die Session
ohne erneuten Login nutzen.
5. Token-Validierung (nur RS256)¶
Datei: app/auth/oidc_validator.py
def validate_token(token: str) -> Optional[Dict]:
# Nur RS256 akzeptieren
if alg != 'RS256':
print(f"Unsupported token algorithm: {alg}. Only RS256 is accepted.")
return None
# Key von JWKS-Endpoint holen
public_key = get_public_key(kid)
# Token validieren
decoded = jwt.decode(token, public_key, algorithms=['RS256'], ...)
Konfiguration¶
Umgebungsvariablen (.env)¶
# Authentik Basis-URL (intern vom Backend erreichbar)
AUTHENTIK_INTERNAL_URL=http://authentik-server:9000
# OAuth2 Client-Konfiguration
AUTHENTIK_BACKEND_CLIENT_ID=llars-backend
AUTHENTIK_BACKEND_CLIENT_SECRET=llars-backend-secret-change-in-production
# OIDC Issuer URL (für Token-Validierung)
AUTHENTIK_ISSUER_URL=http://authentik-server:9000/application/o/llars-backend/
Authentik Provider-Einstellungen¶
| Einstellung | Wert |
|---|---|
| Client ID | llars-backend |
| Client Type | Confidential |
| Signing Algorithm | RS256 |
| Signing Key | Self-signed Certificate |
| Redirect URIs | http://authentik-server:9000/ (für Code-Exchange) |
| Authorization Flow | default-provider-authorization-implicit-consent |
Implementierungsstatus¶
Erfolgreich implementiert¶
- RS256-Konfiguration in Authentik
- JWKS-Endpoint erreichbar vom Backend (
/application/o/llars-backend/jwks/) - Custom Authentication Flow ohne MFA (
llars-api-authentication) - Flow Executor API - Username-Authentifizierung
- Flow Executor API - Passwort-Authentifizierung
- OAuth2 Authorization Code Flow
- Token-Exchange
- Backend Login-Endpoint Delegation
- Token-Validierung via JWKS
- Client Secret Synchronisierung
Behobene Probleme¶
- JWKS URL falsch: Code verwendete
/.well-known/jwks.json, Authentik nutzt/jwks/ - Client Secret Mismatch: Secret in .env stimmte nicht mit Authentik überein
- Login-Endpoint dupliziert: Alter Login-Code in
routes.pyüberschrieb neuen Code
Troubleshooting¶
Passwort ungültig obwohl korrekt¶
# Passwort in Authentik setzen
docker compose exec -T authentik-server ak shell -c "
from authentik.core.models import User
user = User.objects.get(username='akadmin')
user.set_password('admin123')
user.save()
"
RS256 Provider prüfen¶
docker compose exec -T authentik-server ak shell -c "
from authentik.providers.oauth2.models import OAuth2Provider
for p in OAuth2Provider.objects.all():
print(f'{p.name}: signing_key={p.signing_key}')
"
JWKS-Endpoint testen¶
docker compose exec -T backend-flask-service curl -s \
http://authentik-server:9000/application/o/llars-backend/jwks/ | jq .