fix(auth): Sitzungen ueber die Benutzer-ID statt ueber den Namen aufloesen

Das Sitzungs-Token trug den Benutzernamen als Subjekt, und get_current_user
suchte danach. Eine Umbenennung machte damit jedes Token auf jedem anderen
Geraet stillschweigend ungueltig — der Nutzer flog ohne Hinweis raus.

Tokens tragen jetzt zusaetzlich uid, die unveraenderliche Zeilen-ID, und die
Aufloesung geht darueber. sub bleibt als Anzeigename erhalten. Tokens ohne
uid werden weiter ueber den Namen aufgeloest, damit vor der Aenderung
ausgestellte Sitzungen gueltig bleiben — mit "Angemeldet bleiben" laufen die
180 Tage.

Fuer SSO war die Zuordnung schon vorher richtig: OIDCIdentity haengt an
(provider_key, sub) des Providers, und bei bekanntem sub wird der lokale
Benutzername nicht angefasst. Ein umbenannter Nutzer in Authentik landet also
unveraendert auf demselben Konto. Vier Tests halten beide Richtungen fest:
Umbenennung beim Provider, lokale Umbenennung mit weiterlaufender Sitzung,
lokale Umbenennung mit erhaltener SSO-Verknuepfung, und die Kompatibilitaet
alter Tokens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-10 06:01:05 +02:00
co-authored by Claude Opus 5
parent cc4daf4a84
commit 725b61c8cb
5 changed files with 108 additions and 14 deletions
+4 -4
View File
@@ -9,7 +9,7 @@ from sqlalchemy import func
from sqlalchemy.orm import Session
import models
from auth import create_access_token, get_current_user, get_password_hash, verify_password
from auth import create_user_token, get_current_user, get_password_hash, verify_password
from database import get_db
# When "Angemeldet bleiben" is ticked the token lives for half a year.
@@ -61,7 +61,7 @@ def setup(req: SetupRequest, db: Session = Depends(get_db)):
db.add(models.UserSettings(user_id=user.id))
db.commit()
db.refresh(user)
token = create_access_token({"sub": user.username})
token = create_user_token(user)
return {"access_token": token, "token_type": "bearer", "user": _user_dict(user)}
@@ -83,7 +83,7 @@ def login(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="2fa_required",
)
token = create_access_token({"sub": user.username})
token = create_user_token(user)
return {"access_token": token, "token_type": "bearer", "user": _user_dict(user)}
@@ -110,7 +110,7 @@ def login_json(req: LoginRequest, db: Session = Depends(get_db)):
detail="Ungültiger 2FA-Code",
)
expires = REMEMBER_ME_EXPIRY if req.remember_me else None
token = create_access_token({"sub": user.username}, expires_delta=expires)
token = create_user_token(user, expires_delta=expires)
return {"access_token": token, "token_type": "bearer", "user": _user_dict(user)}
+6 -6
View File
@@ -33,7 +33,7 @@ from sqlalchemy.orm import Session
import models
import oidc_client
import oidc_config
from auth import ALGORITHM, SECRET_KEY, create_access_token, get_current_user
from auth import ALGORITHM, SECRET_KEY, create_user_token, get_current_user
from database import get_db
from dav_util import public_base
from oidc_client import OIDCError
@@ -275,7 +275,7 @@ def oidc_callback(provider_key: str, request: Request,
return _fail(request, e.slug)
logger.info("OIDC login (%s) user id=%s via %s", action, user.id, provider.key)
token = create_access_token({"sub": user.username})
token = create_user_token(user)
resp = RedirectResponse(url="/?sso=1", status_code=302)
resp.delete_cookie(FLOW_COOKIE, path="/api/auth/oidc")
@@ -300,13 +300,13 @@ def oidc_complete(request: Request, response: Response, db: Session = Depends(ge
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
uid = payload.get("uid")
except JWTError:
raise HTTPException(401, "SSO-Sitzung ungültig")
if not username:
if uid is None:
raise HTTPException(401, "SSO-Sitzung ungültig")
user = db.query(models.User).filter(models.User.username == username).first()
user = db.query(models.User).filter(models.User.id == uid).first()
if not user:
raise HTTPException(401, "SSO-Sitzung ungültig")
@@ -404,7 +404,7 @@ def oidc_exchange(req: ExchangeRequest, db: Session = Depends(get_db)):
logger.info("OIDC exchange (%s) user id=%s via %s", action, user.id, provider.key)
return {
"access_token": create_access_token({"sub": user.username}),
"access_token": create_user_token(user),
"token_type": "bearer",
"user": _user_dict(user),
}
+4 -2
View File
@@ -17,7 +17,7 @@ from sqlalchemy.orm import Session
from sqlalchemy import func
import models
from auth import create_access_token, get_current_user, get_password_hash, verify_password
from auth import create_user_token, get_current_user, get_password_hash, verify_password
from database import DATA_DIR, get_db
router = APIRouter()
@@ -116,7 +116,9 @@ def update_profile(
db.commit()
# The JWT 'sub' is the login name — renaming it invalidates the old
# token, so hand back a fresh one for the client to store.
result["access_token"] = create_access_token({"sub": new_login})
# The session survives a rename on its own (uid), but the token
# still carries the old name for display — hand out a fresh one.
result["access_token"] = create_user_token(current_user)
return result
if data.directory_hidden is not None:
current_user.directory_hidden = data.directory_hidden