Traduction incomplète
Cette page n'est pas encore entièrement traduite : les passages non traduits sont affichés en anglais.
0018. Sign-in page: passkeys without a button, one button per provider¶
Status: accepted
Date: 2026-11-07
Extends: 0007
Context¶
The sign-in page listed a passkey button, a separator, a code-by-e-mail button and a list of third-party providers under a heading. The maintainer found it cluttered. The target is the form, then at most one button per configured identity provider.
Decision¶
Passkeys have no button. The address field carries autocomplete="username webauthn" (set by accounts.forms.LoginForm, which extends allauth’s form when
MFA_PASSKEY_LOGIN_ENABLED is on). Browsers that support conditional mediation
list the passkeys saved for the site in that field’s suggestions; picking one
signs the person in, with no extra click.
What allauth 65 provides and is reused: the endpoint that issues the challenge
(a GET asking for JSON on mfa_login_webauthn, with no user, so discoverable
credentials), the endpoint that checks the signed answer (a POST of the form),
mfa/js/webauthn-json.js to convert between the browser API and JSON, and the
passkey login itself. allauth’s own login script only binds a button and has no
conditional mode, so a small script, static/js/passkey-login.js (about 120
lines, served locally, no inline code), starts the conditional request. The
labels it needs are data- attributes of a hidden form rendered by
account/snippets/passkey_login.html.
Fallback. Where the browser has no conditional mediation but supports WebAuthn
(PublicKeyCredential exists), a « Use a passkey » button appears under the address
field as soon as something is typed in it. It starts the same request in modal
mode (the browser’s own picker).
The fallback does not depend on the address. The card behind this decision suggested offering the passkey only when the typed address has one. That needs an endpoint answering « does this account have a passkey? », which tells anyone which addresses are registered. The offer is shown for any non-empty address instead, and the request is discoverable, so the server learns nothing.
Failure is quiet. A refused or cancelled conditional request (nobody asked for it) leaves the password form as it is; only a click on the fallback button shows an error message.
Typing a password cancels the pending conditional request (AbortController).
Third-party sign-in. socialaccount/snippets/login.html is overridden: no
separator, no heading, one secondary button per configured provider, labelled
« Continue with » and the provider’s name (OIDC_PROVIDER_NAME, for example « Company SSO »),
under the form. The button is a form posted with the CSRF token to the provider’s
login address, not a link: allauth answers a GET with a confirmation page and starts
the sign-in only for a POST, so one click goes straight to the provider.
SOCIALACCOUNT_LOGIN_ON_GET stays off, since it would let any link start a sign-in.
The page of a home that requires a provider (0021)
uses the same form, and next is kept.
The refusal page for closed OIDC sign-up is unchanged (0007).
Forgotten password. allauth’s « Forgot your password? » link and the reset pages
need outgoing email. They exist only when EMAIL_HOST is set and the email backend
delivers (accounts.services.email_is_configured): the link is dropped from the
form and the reset pages answer 404 otherwise.
Sign-up. The link (« No account yet? ») exists only when local sign-up is open, as before. The « send me a sign-in code » button only appears if that allauth feature is enabled, which it is not.
Consequences¶
Passkeys need an address field to autofill into; a person with a passkey and no password reaches it by tapping the field. Users who never tap it still see the password form and, in browsers without autofill support, the fallback button.
The conditional request is created on every load of the page where passkey login is enabled; the cost is one small GET.
Automated: the page has no passkey button, the field’s attribute, the local scripts and the hidden form, the provider buttons (
tests/test_interface.py), and, in CI, an end-to-end run in Chromium with a virtual authenticator that registers a passkey, then signs in through the conditional path and through the fallback button. Not automatable: the real autofill popup of each browser and platform (Safari and iCloud Keychain, Chrome and Google Password Manager, Firefox, Android, a hardware key). The checklist is in the pull request.