From 01a8fe135c97b0f831c4328d6c09bb0804e65610 Mon Sep 17 00:00:00 2001 From: owain Date: Tue, 9 Jun 2026 21:34:23 +0100 Subject: [PATCH] Disable password login once a passkey is linked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /token: reject password auth with a clear message if pocketid_sub is set on the account — passkey-linked users must sign in via PocketID - Link callback + auto-link-by-email: null out hashed_password when the passkey is attached so the old hash can't be used even if the check above were bypassed Co-Authored-By: Claude Opus 4.8 --- backend/app/api/auth.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 923c8e5..13b070f 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -142,6 +142,11 @@ async def login( user = result.scalar_one_or_none() if not user or not user.hashed_password: raise HTTPException(status_code=400, detail="Invalid credentials") + if user.pocketid_sub is not None: + raise HTTPException( + status_code=400, + detail="Password login is disabled for this account — use your passkey to sign in.", + ) if not verify_password(form_data.password, user.hashed_password): raise HTTPException(status_code=400, detail="Invalid credentials") token = create_access_token({"sub": str(user.id)}) @@ -262,6 +267,7 @@ async def pocketid_callback(code: str, state: Optional[str] = None, db: AsyncSes if target is None: return RedirectResponse(url="/login?auth_error=link_failed") target.pocketid_sub = sub + target.hashed_password = None # disable password login once passkey is linked if not target.email and email: dup = await db.execute( select(User).where(User.email == email, User.id != target.id) @@ -293,6 +299,7 @@ async def pocketid_callback(code: str, state: Optional[str] = None, db: AsyncSes existing = result.scalar_one_or_none() if existing and existing.pocketid_sub is None: existing.pocketid_sub = sub + existing.hashed_password = None # disable password login once passkey is linked user = existing # 3) Otherwise provision a new account with a collision-safe username.