feat(auth): let a single-sign-on account confirm it is you at its provider

An account created through OIDC holds no password, passkey or second
factor, so the recent-proof gate in front of token minting, MCP
connections, exports and the admin reset had nothing to ask it for and
the web could only say "sign in again" (#1296).

GET /api/auth/oidc/reproof sends the browser back to the configured
provider with prompt=login and max_age=0, its own state, nonce and PKCE
verifier, and the signed-in account and session sealed into the state
cookie. The shared callback branches on that intent: it refuses unless
the browser still carries the bound session (checked before the code is
redeemed), the ID token names the account's linked issuer and subject,
and auth_time lies inside the gate's five-minute window. It then stamps
Session.reproofAt exactly as a password re-proof does, creates no
session, and returns to the page it left with ?reproof=done|failed.

recentProofMethods offers "oidc" only to an account without a second
factor whose identity is linked while a provider is configured, and the
fresh-factor gate never lists it. GET /api/auth/me/mfa publishes the
list as proofMethods, so the passkey card offers what the account can
actually give, and register-options takes { method: "oidc" } on a fresh
provider re-proof, so such an account can add a passkey as a second way
to confirm. Both re-proof dialogs offer the provider and a toast reports
the outcome on return.

The callback's missing-state branch and the passkey card's enrollment
flow move into their own functions to keep both handlers under the size
ratchet.
This commit is contained in:
Marc-André Bombeck committed 2026-10-29 08:56:51 +01:00
1 parent 35a0ceb12c
commit bbef4eea99
35 files changed
+1654 -187

No files matched your search

+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "Passwort",
"totp": "Authenticator-Code",
"passkey": "Passkey",
"webauthn": "Sicherheitsschlüssel"
"webauthn": "Sicherheitsschlüssel",
"oidc": "Identitätsanbieter"
},
"currentPassword": "Aktuelles Passwort",
"authenticatorCode": "Authenticator-Code",
"authenticatorPlaceholder": "Sechsstelliger Authenticator-Code",
"verifyAndAdd": "Bestätigen und hinzufügen",
"factorDescription": "Bestätige eine vorhandene Anmeldemethode, bevor du einen zweiten Faktor hinzufügst.",
"verifyAndContinue": "Bestätigen und fortfahren"
"verifyAndContinue": "Bestätigen und fortfahren",
"oidcHint": "Du meldest dich bei deinem Identitätsanbieter noch einmal an und kommst direkt auf diese Seite zurück. Dann wiederholst du, was du gerade tun wolltest.",
"continueToProvider": "Weiter zur Anmeldung"
},
"passkeyName": "Name",
"noPasskeys": "Keine Passkeys registriert.",
@@ -7517,7 +7520,9 @@
"description": "Damit werden deine Gesundheitsdaten weitergegeben oder exportiert. Bestätige, dass du es bist, um fortzufahren.",
"descriptionChange": "Das ändert etwas, das nur du ändern können solltest. Bestätige, dass du es bist, um fortzufahren.",
"signInAgain": "Melde dich neu an und versuche es dann noch einmal. Dein Konto hat kein Passwort und keinen Passkey, den du hier bestätigen könntest.",
"failed": "Das hat nicht geklappt. Prüfe deine Eingabe und versuche es noch einmal."
"failed": "Das hat nicht geklappt. Prüfe deine Eingabe und versuche es noch einmal.",
"oidcDone": "Bestätigt. In den nächsten fünf Minuten kannst du weitermachen, ohne erneut gefragt zu werden.",
"oidcFailed": "Dein Identitätsanbieter konnte nicht bestätigen, dass du es bist. Versuche es noch einmal."
},
"passwordTitle": "Passwort",
"tokenNameLabel": "Token-Name",
+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "Password",
"totp": "Authenticator code",
"passkey": "Passkey",
"webauthn": "Security key"
"webauthn": "Security key",
"oidc": "Identity provider"
},
"currentPassword": "Current password",
"authenticatorCode": "Authenticator code",
"authenticatorPlaceholder": "6-digit authenticator code",
"verifyAndAdd": "Verify and add",
"factorDescription": "Verify an existing sign-in method before adding a second factor.",
"verifyAndContinue": "Verify and continue"
"verifyAndContinue": "Verify and continue",
"oidcHint": "You sign in once more at your identity provider and come straight back to this page. Then do what you were about to do again.",
"continueToProvider": "Continue to sign-in"
},
"passkeyName": "Name",
"noPasskeys": "No passkeys registered.",
@@ -7517,7 +7520,9 @@
"description": "This shares or exports your health data. Confirm it's you to continue.",
"descriptionChange": "This changes something only you should be able to change. Confirm it's you to continue.",
"signInAgain": "Sign in again, then try once more. Your account has no password or passkey to confirm here.",
"failed": "That did not work. Check what you entered and try again."
"failed": "That did not work. Check what you entered and try again.",
"oidcDone": "Confirmed. For the next five minutes you can go ahead without being asked again.",
"oidcFailed": "Your identity provider could not confirm it's you. Try again."
},
"passwordTitle": "Password",
"tokenNameLabel": "Token name",
+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "Contraseña",
"totp": "Código del autenticador",
"passkey": "Clave de acceso",
"webauthn": "Llave de seguridad"
"webauthn": "Llave de seguridad",
"oidc": "Proveedor de identidad"
},
"currentPassword": "Contraseña actual",
"authenticatorCode": "Código del autenticador",
"authenticatorPlaceholder": "Código de seis dígitos del autenticador",
"verifyAndAdd": "Verificar y añadir",
"factorDescription": "Verifica un método de inicio de sesión existente antes de añadir un segundo factor.",
"verifyAndContinue": "Verificar y continuar"
"verifyAndContinue": "Verificar y continuar",
"oidcHint": "Vuelves a iniciar sesión en tu proveedor de identidad y regresas directamente a esta página. Después repite lo que ibas a hacer.",
"continueToProvider": "Continuar al inicio de sesión"
},
"passkeyName": "Nombre",
"noPasskeys": "No hay claves de acceso registradas.",
@@ -7517,7 +7520,9 @@
"description": "Esto comparte o exporta tus datos de salud. Confirma que eres tú para continuar.",
"descriptionChange": "Esto cambia algo que solo tú deberías poder cambiar. Confirma que eres tú para continuar.",
"signInAgain": "Vuelve a iniciar sesión y inténtalo de nuevo. Tu cuenta no tiene contraseña ni llave de acceso que confirmar aquí.",
"failed": "No ha funcionado. Revisa lo que has introducido e inténtalo de nuevo."
"failed": "No ha funcionado. Revisa lo que has introducido e inténtalo de nuevo.",
"oidcDone": "Confirmado. Durante los próximos cinco minutos puedes continuar sin que se te vuelva a preguntar.",
"oidcFailed": "Tu proveedor de identidad no pudo confirmar que eres tú. Inténtalo de nuevo."
},
"passwordTitle": "Contraseña",
"tokenNameLabel": "Nombre del token",
+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "Mot de passe",
"totp": "Code d’authentification",
"passkey": "Clé d’accès",
"webauthn": "Clé de sécurité"
"webauthn": "Clé de sécurité",
"oidc": "Fournisseur d'identité"
},
"currentPassword": "Mot de passe actuel",
"authenticatorCode": "Code d’authentification",
"authenticatorPlaceholder": "Code d’authentification à 6 chiffres",
"verifyAndAdd": "Vérifier et ajouter",
"factorDescription": "Vérifiez une méthode de connexion existante avant d'ajouter un second facteur.",
"verifyAndContinue": "Vérifier et continuer"
"verifyAndContinue": "Vérifier et continuer",
"oidcHint": "Vous vous reconnectez auprès de votre fournisseur d'identité et revenez directement sur cette page. Refaites ensuite ce que vous étiez en train de faire.",
"continueToProvider": "Continuer vers la connexion"
},
"passkeyName": "Nom",
"noPasskeys": "Aucune clé d’accès enregistrée.",
@@ -7517,7 +7520,9 @@
"description": "Cette action partage ou exporte vos données de santé. Confirmez qu'il s'agit bien de vous pour continuer.",
"descriptionChange": "Cela modifie quelque chose que vous seul devriez pouvoir modifier. Confirmez qu'il s'agit bien de vous pour continuer.",
"signInAgain": "Reconnectez-vous, puis réessayez. Votre compte n'a ni mot de passe ni clé d'accès à confirmer ici.",
"failed": "Cela n'a pas fonctionné. Vérifiez votre saisie et réessayez."
"failed": "Cela n'a pas fonctionné. Vérifiez votre saisie et réessayez.",
"oidcDone": "C'est confirmé. Pendant les cinq prochaines minutes, vous pouvez continuer sans qu'on vous le redemande.",
"oidcFailed": "Votre fournisseur d'identité n'a pas pu confirmer que c'est bien vous. Réessayez."
},
"passwordTitle": "Mot de passe",
"tokenNameLabel": "Nom du jeton",
+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "Password",
"totp": "Codice dell’app di autenticazione",
"passkey": "Passkey",
"webauthn": "Chiave di sicurezza"
"webauthn": "Chiave di sicurezza",
"oidc": "Provider di identità"
},
"currentPassword": "Password attuale",
"authenticatorCode": "Codice dell’app di autenticazione",
"authenticatorPlaceholder": "Codice a 6 cifre dell’app di autenticazione",
"verifyAndAdd": "Verifica e aggiungi",
"factorDescription": "Verifica un metodo di accesso esistente prima di aggiungere un secondo fattore.",
"verifyAndContinue": "Verifica e continua"
"verifyAndContinue": "Verifica e continua",
"oidcHint": "Accedi di nuovo presso il tuo provider di identità e torni subito a questa pagina. Poi ripeti quello che stavi per fare.",
"continueToProvider": "Continua all'accesso"
},
"passkeyName": "Nome",
"noPasskeys": "Nessuna passkey registrata.",
@@ -7517,7 +7520,9 @@
"description": "Questa azione condivide o esporta i tuoi dati sanitari. Conferma che sei tu per continuare.",
"descriptionChange": "Questo modifica qualcosa che solo tu dovresti poter modificare. Conferma che sei tu per continuare.",
"signInAgain": "Accedi di nuovo, poi riprova. Il tuo account non ha una password o una passkey da confermare qui.",
"failed": "Non ha funzionato. Controlla quanto inserito e riprova."
"failed": "Non ha funzionato. Controlla quanto inserito e riprova.",
"oidcDone": "Confermato. Per i prossimi cinque minuti puoi procedere senza che ti venga chiesto di nuovo.",
"oidcFailed": "Il tuo provider di identità non ha potuto confermare che sei tu. Riprova."
},
"passwordTitle": "Password",
"tokenNameLabel": "Nome del token",
+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "비밀번호",
"totp": "인증 앱 코드",
"passkey": "패스키",
"webauthn": "보안 키"
"webauthn": "보안 키",
"oidc": "ID 공급자"
},
"currentPassword": "현재 비밀번호",
"authenticatorCode": "인증 앱 코드",
"authenticatorPlaceholder": "6자리 인증 앱 코드",
"verifyAndAdd": "확인 후 추가",
"factorDescription": "두 번째 인증 수단을 추가하기 전에 기존 로그인 방법을 확인하세요.",
"verifyAndContinue": "확인하고 계속"
"verifyAndContinue": "확인하고 계속",
"oidcHint": "ID 공급자에서 한 번 더 로그인하면 바로 이 페이지로 돌아옵니다. 그런 다음 하려던 작업을 다시 진행하세요.",
"continueToProvider": "로그인으로 계속"
},
"passkeyName": "이름",
"noPasskeys": "등록된 패스키가 없어요.",
@@ -7517,7 +7520,9 @@
"description": "건강 데이터를 공유하거나 내보내는 작업입니다. 계속하려면 본인임을 확인하세요.",
"descriptionChange": "본인만 변경할 수 있어야 하는 항목을 변경합니다. 계속하려면 본인임을 확인하세요.",
"signInAgain": "다시 로그인한 후 한 번 더 시도하세요. 이 계정에는 여기서 확인할 비밀번호나 패스키가 없습니다.",
"failed": "확인하지 못했습니다. 입력한 내용을 확인하고 다시 시도하세요."
"failed": "확인하지 못했습니다. 입력한 내용을 확인하고 다시 시도하세요.",
"oidcDone": "확인되었습니다. 앞으로 5분 동안은 다시 묻지 않고 계속할 수 있습니다.",
"oidcFailed": "ID 공급자가 본인임을 확인하지 못했습니다. 다시 시도하세요."
},
"passwordTitle": "비밀번호",
"tokenNameLabel": "토큰 이름",
+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "Wachtwoord",
"totp": "Authenticatorcode",
"passkey": "Passkey",
"webauthn": "Beveiligingssleutel"
"webauthn": "Beveiligingssleutel",
"oidc": "Identiteitsprovider"
},
"currentPassword": "Huidig wachtwoord",
"authenticatorCode": "Authenticatorcode",
"authenticatorPlaceholder": "Authenticatorcode van 6 cijfers",
"verifyAndAdd": "Verifiëren en toevoegen",
"factorDescription": "Verifieer een bestaande inlogmethode voordat je een tweede factor toevoegt.",
"verifyAndContinue": "Verifiëren en doorgaan"
"verifyAndContinue": "Verifiëren en doorgaan",
"oidcHint": "Je logt nog een keer in bij je identiteitsprovider en komt meteen terug op deze pagina. Doe daarna opnieuw wat je wilde doen.",
"continueToProvider": "Verder naar inloggen"
},
"passkeyName": "Naam",
"noPasskeys": "Geen passkeys geregistreerd.",
@@ -7517,7 +7520,9 @@
"description": "Hiermee deel of exporteer je je gezondheidsgegevens. Bevestig dat jij het bent om door te gaan.",
"descriptionChange": "Hiermee wijzig je iets wat alleen jij zou moeten kunnen wijzigen. Bevestig dat jij het bent om door te gaan.",
"signInAgain": "Log opnieuw in en probeer het daarna nog een keer. Je account heeft geen wachtwoord of passkey waarmee je hier kunt bevestigen.",
"failed": "Dat is niet gelukt. Controleer wat je hebt ingevoerd en probeer het opnieuw."
"failed": "Dat is niet gelukt. Controleer wat je hebt ingevoerd en probeer het opnieuw.",
"oidcDone": "Bevestigd. De komende vijf minuten kun je verder zonder dat het opnieuw wordt gevraagd.",
"oidcFailed": "Je identiteitsprovider kon niet bevestigen dat jij het bent. Probeer het opnieuw."
},
"passwordTitle": "Wachtwoord",
"tokenNameLabel": "Tokennaam",
+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "Hasło",
"totp": "Kod z aplikacji uwierzytelniającej",
"passkey": "Klucz dostępu",
"webauthn": "Klucz bezpieczeństwa"
"webauthn": "Klucz bezpieczeństwa",
"oidc": "Dostawca tożsamości"
},
"currentPassword": "Aktualne hasło",
"authenticatorCode": "Kod z aplikacji uwierzytelniającej",
"authenticatorPlaceholder": "6-cyfrowy kod z aplikacji uwierzytelniającej",
"verifyAndAdd": "Zweryfikuj i dodaj",
"factorDescription": "Potwierdź istniejącą metodę logowania, zanim dodasz drugi składnik.",
"verifyAndContinue": "Potwierdź i kontynuuj"
"verifyAndContinue": "Potwierdź i kontynuuj",
"oidcHint": "Zalogujesz się jeszcze raz u swojego dostawcy tożsamości i wrócisz od razu na tę stronę. Potem powtórz to, co chcesz zrobić.",
"continueToProvider": "Przejdź do logowania"
},
"passkeyName": "Nazwa",
"noPasskeys": "Brak zarejestrowanych kluczy dostępu.",
@@ -7517,7 +7520,9 @@
"description": "Ta czynność udostępnia lub eksportuje Twoje dane zdrowotne. Potwierdź, że to Ty, aby kontynuować.",
"descriptionChange": "To zmienia coś, co tylko Ty powinieneś móc zmienić. Potwierdź, że to Ty, aby kontynuować.",
"signInAgain": "Zaloguj się ponownie, a potem spróbuj jeszcze raz. Twoje konto nie ma hasła ani klucza dostępu do potwierdzenia tutaj.",
"failed": "To się nie udało. Sprawdź wprowadzone dane i spróbuj ponownie."
"failed": "To się nie udało. Sprawdź wprowadzone dane i spróbuj ponownie.",
"oidcDone": "Potwierdzono. Przez następne pięć minut możesz kontynuować bez ponownego pytania.",
"oidcFailed": "Twój dostawca tożsamości nie mógł potwierdzić, że to Ty. Spróbuj ponownie."
},
"passwordTitle": "Hasło",
"tokenNameLabel": "Nazwa tokena",
+8 -3
View File
@@ -6237,14 +6237,17 @@
"password": "Senha",
"totp": "Código do autenticador",
"passkey": "Chave de acesso",
"webauthn": "Chave de segurança"
"webauthn": "Chave de segurança",
"oidc": "Provedor de identidade"
},
"currentPassword": "Senha atual",
"authenticatorCode": "Código do autenticador",
"authenticatorPlaceholder": "Código do autenticador de 6 dígitos",
"verifyAndAdd": "Verificar e adicionar",
"factorDescription": "Verifique um método de entrada existente antes de adicionar um segundo fator.",
"verifyAndContinue": "Verificar e continuar"
"verifyAndContinue": "Verificar e continuar",
"oidcHint": "Você entra mais uma vez no seu provedor de identidade e volta direto para esta página. Depois, repita o que ia fazer.",
"continueToProvider": "Continuar para o login"
},
"passkeyName": "Nome",
"noPasskeys": "Nenhuma chave de acesso registrada.",
@@ -7517,7 +7520,9 @@
"description": "Isto compartilha ou exporta os seus dados de saúde. Confirme que é você para continuar.",
"descriptionChange": "Isto altera algo que só você deveria poder alterar. Confirme que é você para continuar.",
"signInAgain": "Entre novamente e tente mais uma vez. Sua conta não tem senha nem chave de acesso para confirmar aqui.",
"failed": "Não deu certo. Confira o que você digitou e tente de novo."
"failed": "Não deu certo. Confira o que você digitou e tente de novo.",
"oidcDone": "Confirmado. Nos próximos cinco minutos você pode continuar sem que isso seja pedido de novo.",
"oidcFailed": "Seu provedor de identidade não conseguiu confirmar que é você. Tente novamente."
},
"passwordTitle": "Senha",
"tokenNameLabel": "Nome do token",