[← 06 — The bootstrap problem](06-the-bootstrap-problem.md) · [index](README.md) · next: [08 — The one-time-token fallback](08-one-time-token-fallback.md) # Failure modes, and what each one looks like Passkey failures are quiet. The authentication endpoint returns a bare 401 with no body, the registration endpoint returns a 500 with a generic error page, and the reason is only ever in the server log at DEBUG. This chapter is a lookup table. Run with the `trace` profile to see any of it: ```bash ./scripts/run.sh trace ``` ## Every registration failure is an HTTP 500 This is worth stating on its own, because it is the first thing that confuses people. `WebAuthnRegistrationFilter` has no error handling: whatever WebAuthn4J throws propagates out of the filter and Boot's error page turns it into a 500. Three different mistakes, three identical responses ([`pk-origin.txt`](../output/pk-origin.txt), [`pk-user-verification.txt`](../output/pk-user-verification.txt), [`pk-duplicate.txt`](../output/pk-duplicate.txt)): ``` $ POST /webauthn/register HTTP 500 {"timestamp":"...","status":500,"error":"Internal Server Error","path":"/webauthn/register"} ``` Authentication is different. `WebAuthnAuthenticationProvider` catches everything: ```java catch (RuntimeException ex) { throw new BadCredentialsException(ex.getMessage(), ex); } ``` so the client gets a clean `401` with an empty body, and the cause is discarded before it reaches any response the browser can see. Both behaviours mean the same thing: **read the server log, not the HTTP response**. ## The table | symptom | cause | where to look | |---|---|---| | `HTTP 400`, empty body, on `/webauthn/register/options` | nobody is logged in, or the session expired | `IllegalArgumentException: Authentication must be authenticated` | | `HTTP 403` on any `/webauthn/**` POST | missing CSRF token — all five endpoints are state-changing POSTs | `CsrfFilter` at position 5 | | `HTTP 500` on `/webauthn/register` | origin mismatch | `BadOriginException: The collectedClientData origin '...' doesn't match expected: ...` | | `HTTP 500` on `/webauthn/register` | UV required, authenticator did not verify | `UserNotVerifiedException: ... UV flag in authenticatorData is not set` | | `HTTP 500` on `/webauthn/register` | the credential id is already registered | `IllegalArgumentException: Credential with id ... already exists` | | `HTTP 401`, empty body, on `/login/webauthn` | origin mismatch, bad signature, unknown credential id, or a null stored attestation object | `BadCredentialsException` wrapping the real cause | | `navigator.credentials.create` never prompts | not a secure context — plain HTTP on anything but `localhost` | browser console, not the server | | credentials vanish on restart | `MapUserCredentialRepository`, the default | [09 — Persistence](09-persistence.md) | | `NullPointerException` during registration | an authenticator transport Spring does not know, e.g. `cable` | [spring-security#19366](https://github.com/spring-projects/spring-security/issues/19366) | | WebAuthn breaks under Spring Session with Redis | creation options serialisation | [spring-security#16328](https://github.com/spring-projects/spring-security/issues/16328) | ## The origin mismatch, in full This is the one that matters, because it is the phishing defence working. From [`docs/output/pk-origin.txt`](../output/pk-origin.txt), with the client sending `http://evil.localhost:8080` and the relying party configured for `http://localhost:8080`: ``` com.webauthn4j.verifier.exception.BadOriginException: The collectedClientData origin 'http://evil.localhost:8080' doesn't match expected: http://localhost:8080 at com.webauthn4j.verifier.OriginVerifierImpl.verify(OriginVerifierImpl.java:72) at com.webauthn4j.verifier.RegistrationDataVerifier.verify(RegistrationDataVerifier.java:171) ``` and the same mistake one ceremony later: ``` $ POST /login/webauthn (origin http://evil.localhost:8080) HTTP 401 (empty body) ``` In a real browser this failure is not reachable: the browser writes the origin itself and the credential is scoped to an rpId, so a phishing page never gets an assertion to send. The software authenticator in this module can lie about its origin precisely so the server-side half of the check can be watched. ## Debugging checklist 1. Is the rpId a domain, and does it match or registrably-suffix the origin's host? 2. Is the allowed origin the exact string, with scheme and port? 3. Is there a `WebAuthnRelyingPartyOperations` bean quietly overriding the DSL? ([02](02-minimum-configuration.md)) 4. Is the request a POST with a CSRF token? 5. Is the session the same one that received the challenge? The default `PublicKeyCredentialCreationOptionsRepository` is `HttpSession`-backed, so a load balancer without sticky sessions breaks registration and nothing else. 6. Turn on `logging.level.com.webauthn4j: DEBUG` and read the actual exception. [← 06 — The bootstrap problem](06-the-bootstrap-problem.md) · [index](README.md) · next: [08 — The one-time-token fallback](08-one-time-token-fallback.md)