FIDO2 troubleshooting¶
FIDO2 / WebAuthn fails opaquely. The package logs a stack trace on registration failure but the user-facing message is generic. This page is a triage tree for the most common causes.
Diagnostic preflight¶
Before reaching for stack traces, confirm:
Check |
How |
|---|---|
Page is on HTTPS (or |
Open the page; look at the address bar. WebAuthn refuses anything else. |
|
|
JavaScript console |
Open DevTools → Console. WebAuthn errors land here verbatim. |
Server log for |
Enable |
90% of FIDO2 failures are caught by one of those four.
Symptom → cause¶
“An error occurred trying to register” / browser console: SecurityError¶
Almost always RP ID mismatch.
The page is at https://app.example.com/... but FIDO_SERVER_ID is set to
example.com (or vice versa). The browser refuses to issue a credential.
Fix: set FIDO_SERVER_ID to either the page’s domain exactly, or a
registrable parent domain (e.g. example.com is valid for app.example.com).
“User cancelled the operation”¶
The user dismissed the OS prompt — pressed Cancel on the Touch ID dialog, unplugged the YubiKey, etc. Not a bug; retry.
“Bad credentials” / InvalidStateError¶
The key the user is presenting is already registered against this account. WebAuthn refuses to double-register. Suggest the user pick a different key or rename the existing one.
Registration succeeds, but auth fails with the same key¶
Most common cause: FIDO_SERVER_ID was changed between registration and
authentication. The package stores the RP ID in UserKey.properties["domain"]
at registration; auth filters keys by properties__domain=request.get_host().
If they don’t match, the key is silently excluded.
Diagnostic SQL:
SELECT user_id, properties->>'domain', enabled
FROM multifactor_userkey
WHERE key_type = 'FIDO2';
If domain doesn’t match your current host, you have stale keys. Either
revert FIDO_SERVER_ID or have users re-enrol.
Auth works on first try, fails on the second¶
Session state lost between the GET (start) and POST (complete) of the challenge. See common issues.
The challenge state is stored in request.session["fido_state"]. If your
session doesn’t persist between requests (multiple processes, signed-cookie
overflow, CDN session stripping), this is your problem.
Cross-subdomain key behaviour¶
A key registered with FIDO_SERVER_ID="example.com" works on any subdomain
because the RP ID is a suffix. A key registered with
FIDO_SERVER_ID="app.example.com" does not work on
other.example.com.
Pick the narrowest RP ID consistent with your topology — subdomain takeover otherwise hands over MFA for the whole apex.
USB key works, Touch ID doesn’t (or vice versa)¶
The package uses user_verification="discouraged" — the most permissive
setting. If you’ve subclassed factors.fido2.Authenticate to require
verification, only keys with PINs or biometrics will pass. The default does
not require this.
Safari fails when Chrome works¶
Safari is stricter about RP ID format and rejects mixed IPv4/hostname
configurations. The browser console will tell you specifically; check
DevTools → Console. Often the fix is to use the hostname (e.g.
localhost) rather than 127.0.0.1.
Reproducing locally¶
To test FIDO2 locally with a real domain (e.g. for a key that requires a non-localhost RP ID):
# Cloudflare Tunnel
cloudflared tunnel --url http://localhost:8000
# ngrok
ngrok http 8000
Then set MULTIFACTOR["FIDO_SERVER_ID"] = "abcd1234.trycloudflare.com"
(or whatever ngrok prints) and reload. The bundled testsite reads
os.environ["DOMAIN"] for exactly this purpose:
DOMAIN=abcd1234.trycloudflare.com python testsite/manage.py runserver
When to read the package source¶
multifactor/factors/fido2.py is ~120 lines. Useful breakpoints when
stepping through:
Register.get()line 53 — start of registration;stateis written here.Register.post()line 67 —register_completeraises here on failure.Authenticate.get()line 96 — start of auth;stateis written here.Authenticate.post()line 105 —authenticate_completeraises here.
The except: at line 85 swallows registration errors and emits a generic
500-style JSON response. The exception is logged at ERROR level — enable
the multifactor.factors.fido2 logger (see Logging) to see
the underlying traceback.
See also¶
FIDO2 guide — happy-path setup.
Logging — turning on the package’s loggers.