3.5 KiB
Discoverable Credentials (Usernameless WebAuthn)
Problem
The current WebAuthn login flow is username-first: the user types their username,
the server looks up their credential IDs, and sends those as allowCredentials
to the browser. This requires the user to remember and type their username.
Additionally, the current WebAuthn login flow has bugs that make it non-functional: the login button isn't wired up in JS, the JS isn't loaded on the login page, and the server returns HX-Redirect headers but the JS tries to parse JSON.
Decision
Switch to discoverable credentials (passkeys). The browser/OS credential picker handles identity selection — no username needed.
Key choices:
- Usernameless only — remove the username field from WebAuthn login entirely
- User verification: preferred — ask for PIN/biometric but allow authenticators that only support presence (touch)
- Resident key: required — credentials must be stored on the authenticator as discoverable credentials
- JSON response — the WebAuthn complete endpoint returns
{"redirect": "..."}since the flow uses fetch(), not HTMX
Registration changes
WebAuthnService.begin_registration() passes
resident_key_requirement=ResidentKeyRequirement.REQUIRED and
user_verification=UserVerificationRequirement.PREFERRED to
Fido2Server.register_begin(). This tells the authenticator to store a resident
credential with the user's ID embedded.
No schema changes needed. The existing webauthn_credentials table and model
already store everything required.
Authentication changes
Begin (POST /login/webauthn/begin)
- No username parameter (change from POST to GET since there's no form body)
- Call
begin_authentication(credentials=None, user_verification=PREFERRED)— emptyallowCredentialstriggers the browser's passkey picker - Store only
webauthn_login_statein session (no userid — we don't know it yet)
Complete (POST /login/webauthn/complete)
- Extract
userHandlefrom the assertion response — this contains theuser_idbytes set during registration (userid.encode()) - Decode
userHandleto get the userid string - Look up the user's stored credentials by userid
- Verify the assertion against those credentials
- Update sign count
- Set session
- Return
JSONResponse({"redirect": "/manage/credentials"})(or the OIDC redirect target if a pending authorization exists)
Service layer
Add user_verification parameter to begin_registration() and
begin_authentication() in WebAuthnService, passing them through to the
fido2 server methods.
Frontend changes
webauthn.js
beginAuthentication()drops the username parameter and FormData body- Change fetch to GET for
/login/webauthn/begin - On success, read
data.redirectfrom JSON response (server now returns JSON) - Wire
#webauthn-login-btnin theDOMContentLoadedhandler
login.html
- Remove the username field and label from the WebAuthn section
- Add
{% block scripts %}to loadwebauthn.js
Files changed
| File | Change |
|---|---|
src/porchlight/authn/webauthn.py |
Add resident_key_requirement and user_verification params |
src/porchlight/authn/routes.py |
Rewrite begin (no username, GET), fix complete (userHandle lookup, JSON response) |
src/porchlight/static/webauthn.js |
Drop username, wire login button, fix response handling |
src/porchlight/templates/login.html |
Remove username field, add scripts block |
No database migrations. No model changes. No new repository methods.