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
+108
-1
@@ -11,16 +11,22 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.models import Role, User, UserIdentity
|
||||
from app.models import OidcPendingLink, Role, User, UserIdentity
|
||||
from app.oidc_config import OidcProvider, provider_aus_umgebung
|
||||
from app.routers.auth import login
|
||||
from app.services import oidc_state
|
||||
from app.security import hash_password, verify_password
|
||||
from app.services.oidc import (
|
||||
OFFEN_GUELTIG,
|
||||
OidcFehler,
|
||||
benutzer_aus_claims,
|
||||
identitaet_loesen,
|
||||
identitaet_verknuepfen,
|
||||
konto_suchen,
|
||||
offen_anlegen,
|
||||
offen_lesen,
|
||||
offen_neues_konto,
|
||||
offen_verknuepfen,
|
||||
)
|
||||
|
||||
BASIS = "https://vorrania.example.com"
|
||||
@@ -431,3 +437,104 @@ def test_app_client_darf_eigene_discovery_adresse_haben():
|
||||
def test_ohne_eigene_adresse_teilen_sich_beide_wege_die_discovery():
|
||||
(p,) = provider_aus_umgebung(VOLLE_UMGEBUNG, BASIS)
|
||||
assert p.app_discovery_url == p.discovery_url
|
||||
|
||||
|
||||
# ---- Erstanmeldung ohne passendes Konto ------------------------------------
|
||||
|
||||
|
||||
def test_unbekannte_kennung_legt_nichts_an(db, provider):
|
||||
"""konto_suchen darf NIE ein Konto erzeugen – sonst gibt es nichts zu fragen."""
|
||||
assert konto_suchen(db, provider, claims()) is None
|
||||
assert db.query(User).count() == 0
|
||||
assert db.query(UserIdentity).count() == 0
|
||||
|
||||
|
||||
def test_offener_merkzettel_haelt_die_geprueften_angaben(db, provider):
|
||||
code = offen_anlegen(db, provider, claims(preferred_username="eva.mueller"))
|
||||
|
||||
eintrag = offen_lesen(db, code)
|
||||
assert eintrag.subject == "abc-123"
|
||||
assert eintrag.email == "eva@example.com"
|
||||
assert eintrag.email_verified is True
|
||||
assert eintrag.username_vorschlag == "eva.mueller"
|
||||
# Der Klartext-Code darf nirgends liegen, nur sein Hash.
|
||||
assert code not in (eintrag.code_hash, eintrag.subject)
|
||||
|
||||
|
||||
def test_offener_merkzettel_laeuft_ab(db, provider):
|
||||
code = offen_anlegen(db, provider, claims())
|
||||
spaeter = datetime.now(timezone.utc) + OFFEN_GUELTIG + timedelta(seconds=1)
|
||||
|
||||
assert offen_lesen(db, code, jetzt=spaeter) is None
|
||||
|
||||
|
||||
def test_falscher_code_findet_nichts(db, provider):
|
||||
offen_anlegen(db, provider, claims())
|
||||
assert offen_lesen(db, "ausgedacht") is None
|
||||
|
||||
|
||||
def test_verknuepfen_braucht_das_richtige_passwort(db, provider):
|
||||
"""Das Passwort ist der Eigentumsnachweis – ohne ihn waere der Dialog eine Luecke."""
|
||||
eva = User(username="eva", password_hash=hash_password("geheim123"), role=Role.admin)
|
||||
db.add(eva)
|
||||
db.commit()
|
||||
eintrag = offen_lesen(db, offen_anlegen(db, provider, claims()))
|
||||
|
||||
with pytest.raises(OidcFehler) as fehler:
|
||||
offen_verknuepfen(db, provider, eintrag, "eva", "falsch")
|
||||
|
||||
assert fehler.value.schluessel == "anmeldung_falsch"
|
||||
assert db.query(UserIdentity).count() == 0
|
||||
# Der Merkzettel bleibt: ein Tippfehler soll nicht den Vorgang wegwerfen.
|
||||
assert db.query(OidcPendingLink).count() == 1
|
||||
|
||||
|
||||
def test_unbekannter_benutzername_verraet_sich_nicht(db, provider):
|
||||
"""Gleiche Meldung wie bei falschem Passwort – sonst zaehlt man Konten ab."""
|
||||
eintrag = offen_lesen(db, offen_anlegen(db, provider, claims()))
|
||||
|
||||
with pytest.raises(OidcFehler) as fehler:
|
||||
offen_verknuepfen(db, provider, eintrag, "gibtsnicht", "beliebig")
|
||||
|
||||
assert fehler.value.schluessel == "anmeldung_falsch"
|
||||
|
||||
|
||||
def test_verknuepfen_haengt_kennung_an_das_bestaetigte_konto(db, provider):
|
||||
eva = User(username="eva", password_hash=hash_password("geheim123"), role=Role.admin)
|
||||
db.add(eva)
|
||||
db.commit()
|
||||
eintrag = offen_lesen(db, offen_anlegen(db, provider, claims()))
|
||||
|
||||
nutzer = offen_verknuepfen(db, provider, eintrag, "eva", "geheim123")
|
||||
|
||||
assert nutzer.id == eva.id
|
||||
assert nutzer.role == Role.admin # Rolle bleibt erhalten
|
||||
assert db.query(User).count() == 1 # kein Zweitkonto
|
||||
assert db.query(OidcPendingLink).count() == 0 # verbraucht
|
||||
# Ab jetzt meldet dieselbe Kennung direkt an.
|
||||
assert konto_suchen(db, provider, claims()).id == eva.id
|
||||
|
||||
|
||||
def test_neues_konto_auf_wunsch(db, provider):
|
||||
eintrag = offen_lesen(db, offen_anlegen(db, provider, claims(preferred_username="eva")))
|
||||
|
||||
neu = offen_neues_konto(db, provider, eintrag)
|
||||
|
||||
assert neu.username == "eva"
|
||||
assert neu.role == Role.user
|
||||
assert neu.password_hash is None
|
||||
assert db.query(OidcPendingLink).count() == 0
|
||||
|
||||
|
||||
def test_ohne_auto_anlage_bleibt_nur_verknuepfen(db, provider):
|
||||
"""AUTO_CREATE=false darf sich nicht ueber den Dialog umgehen lassen."""
|
||||
from dataclasses import replace
|
||||
|
||||
ohne = replace(provider, auto_create=False)
|
||||
eintrag = offen_lesen(db, offen_anlegen(db, ohne, claims()))
|
||||
|
||||
with pytest.raises(OidcFehler) as fehler:
|
||||
offen_neues_konto(db, ohne, eintrag)
|
||||
|
||||
assert fehler.value.schluessel == "anlegen_nicht_erlaubt"
|
||||
assert db.query(User).count() == 0
|
||||
|
||||
Reference in New Issue
Block a user