Erstanmeldung fragt nach, statt still ein zweites Konto anzulegen
Wer sich zum ersten Mal ueber den Anmeldedienst anmeldet und hier noch kein Konto hat, bekam bisher wortlos ein neues - mit Rolle "Benutzer" und ohne Verlauf. Wer schon ein Konto hatte, wollte aber fast immer genau das verknuepfen und stand dann mit zwei Konten da. Jetzt zeigt der Rueckweg eine Entscheidungsseite: bestehendes Konto verknuepfen oder ein neues anlegen. Das Verknuepfen verlangt einmalig Benutzername und Passwort des bestehenden Kontos. Genau dieser Nachweis ist der Grund, warum der Anmeldeweg selbst nie fragen darf, welches Konto gemeint ist - ohne ihn koennte sich jeder mit einem beliebigen Konto beim Anmeldedienst an ein fremdes Vorrania-Konto haengen. Die geprueften Angaben liegen dafuer kurzlebig und einmalig in oidc_pending_links; vom Code steht dort nur der SHA-256-Hash, wie bei den API-Tokens. Ein falsch getipptes Passwort verbraucht den Vorgang nicht, und unbekannter Benutzername und falsches Passwort antworten gleich - sonst liessen sich vorhandene Konten abzaehlen. AUTO_CREATE=false blendet den Knopf zum Anlegen aus und laesst sich auch ueber den Dialog nicht umgehen. Der App-Weg bleibt unveraendert, weil er keinen Zwischenschritt anzeigen kann. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a134b2aad5
commit
eb29143f97
@@ -23,12 +23,14 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..deps import get_current_user
|
||||
from ..models import User, UserIdentity
|
||||
from ..models import OidcPendingLink, User, UserIdentity
|
||||
from ..oidc_config import OidcProvider, provider_finden, registrierte_provider
|
||||
from ..schemas import (
|
||||
OidcIdentityOut,
|
||||
OidcLinkStart,
|
||||
OidcNativeLogin,
|
||||
OidcPendingOut,
|
||||
OidcPendingVerknuepfen,
|
||||
OidcProviderOut,
|
||||
Token,
|
||||
)
|
||||
@@ -48,6 +50,8 @@ WEB_FEHLER = "/login"
|
||||
# Eine Verknuepfung endet dort, wo sie angestossen wurde – nicht auf der
|
||||
# Anmeldemaske: Der Nutzer ist ja die ganze Zeit angemeldet geblieben.
|
||||
WEB_KONTO = "/konto"
|
||||
# Wohin es geht, wenn die Kennung echt ist, aber noch keinem Konto gehoert.
|
||||
WEB_ENTSCHEIDUNG = "/sso-neu"
|
||||
|
||||
|
||||
def _provider_oder_404(slug: str) -> OidcProvider:
|
||||
@@ -189,7 +193,18 @@ def anmeldung_abschliessen(
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
|
||||
nutzer = dienst.benutzer_aus_claims(db, provider, claims)
|
||||
nutzer = dienst.konto_suchen(db, provider, claims)
|
||||
if nutzer is None:
|
||||
# Die Kennung ist echt, gehoert aber noch niemandem. Statt still ein
|
||||
# zweites Konto anzulegen, nachfragen – wer schon ein Konto hat, will
|
||||
# meistens genau das verknuepfen.
|
||||
dienst.offen_aufraeumen(db)
|
||||
merkzettel = dienst.offen_anlegen(db, provider, claims)
|
||||
logger.info("SSO '%s': unbekannte Kennung, frage nach.", slug)
|
||||
return RedirectResponse(
|
||||
f"{WEB_ENTSCHEIDUNG}#code={quote(merkzettel)}",
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
except dienst.OidcFehler as fehler:
|
||||
logger.warning("SSO '%s' gescheitert: %s", slug, fehler)
|
||||
return _fehler_weiterleitung(fehler.schluessel, verknuepfung=verknuepfen_fuer is not None)
|
||||
@@ -335,3 +350,71 @@ def verknuepfung_loesen(
|
||||
# "letzte Anmeldemoeglichkeit" ist kein Serverfehler, sondern eine
|
||||
# bewusste Weigerung – der Text erklaert den Ausweg.
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, str(fehler)) from fehler
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Erstanmeldung: Kennung ist echt, gehoert aber noch keinem Konto
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _offen_oder_404(db: Session, code: str) -> OidcPendingLink:
|
||||
eintrag = dienst.offen_lesen(db, code)
|
||||
if eintrag is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
"Der Anmeldevorgang ist abgelaufen. Bitte noch einmal anmelden.",
|
||||
)
|
||||
return eintrag
|
||||
|
||||
|
||||
@router.get("/pending/{code}", response_model=OidcPendingOut)
|
||||
def offene_anmeldung(code: str, db: Session = Depends(get_db)) -> OidcPendingOut:
|
||||
"""Was die Entscheidungsseite anzeigen soll.
|
||||
|
||||
Ohne Anmeldung abrufbar – der Einmalcode aus der Adresse ist der Nachweis.
|
||||
"""
|
||||
eintrag = _offen_oder_404(db, code)
|
||||
provider = _provider_oder_404(eintrag.provider)
|
||||
return OidcPendingOut(
|
||||
provider=eintrag.provider,
|
||||
display_name=provider.display_name,
|
||||
email=eintrag.email,
|
||||
username_vorschlag=eintrag.username_vorschlag,
|
||||
anlegen_erlaubt=provider.auto_create,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/pending/{code}/anlegen", response_model=Token)
|
||||
def offene_anmeldung_neues_konto(code: str, db: Session = Depends(get_db)) -> Token:
|
||||
"""Legt ein neues Vorrania-Konto zu dieser Kennung an."""
|
||||
eintrag = _offen_oder_404(db, code)
|
||||
provider = _provider_oder_404(eintrag.provider)
|
||||
try:
|
||||
nutzer = dienst.offen_neues_konto(db, provider, eintrag)
|
||||
except dienst.OidcFehler as fehler:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, str(fehler)) from fehler
|
||||
logger.info("SSO '%s': neues Konto '%s' auf Wunsch angelegt.", provider.slug, nutzer.username)
|
||||
return _token_fuer(nutzer)
|
||||
|
||||
|
||||
@router.post("/pending/{code}/verknuepfen", response_model=Token)
|
||||
def offene_anmeldung_verknuepfen(
|
||||
code: str, payload: OidcPendingVerknuepfen, db: Session = Depends(get_db)
|
||||
) -> Token:
|
||||
"""Haengt die Kennung an ein bestehendes Konto – nach Passwort-Nachweis."""
|
||||
eintrag = _offen_oder_404(db, code)
|
||||
provider = _provider_oder_404(eintrag.provider)
|
||||
try:
|
||||
nutzer = dienst.offen_verknuepfen(
|
||||
db, provider, eintrag, payload.username, payload.password
|
||||
)
|
||||
except dienst.OidcFehler as fehler:
|
||||
# Falsches Passwort ist 401, eine belegte Kennung ein Konflikt.
|
||||
code_http = (
|
||||
status.HTTP_401_UNAUTHORIZED
|
||||
if fehler.schluessel == "anmeldung_falsch"
|
||||
else status.HTTP_409_CONFLICT
|
||||
)
|
||||
raise HTTPException(code_http, str(fehler)) from fehler
|
||||
logger.info("SSO '%s': mit bestehendem Konto '%s' verknuepft.", provider.slug, nutzer.username)
|
||||
return _token_fuer(nutzer)
|
||||
|
||||
Reference in New Issue
Block a user