============================================================================== FactorGrantedAuthority - password, magic link and passkey are not interchangeable ============================================================================== === An endpoint guarded by hasAuthority("FACTOR_WEBAUTHN") === POST /login (password) -> HTTP 302, Location: http://localhost:8080/ $ POST /webauthn/register/options HTTP 200 {"attestation":"none","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"},"challenge":"2LH6xjc9-LwKl-KldFIox5EkCGBVuDXps87yofG4piA","excludeCredentials":[],"extensions":{"credProps":true},"pubKeyCredParams":[{"alg":-8,"type":"public-key"},{"alg":-7,"type":"public-key"},{"alg":-257,"type":"public-key"}],"rp":{"id":"localhost","name":"ankurm passkeys demo"},"timeout":300000,"user":{"name":"user","id":"p6EhVt4x7b3PsL3M94Vuczh2BQyWCSurlu1YBG6lAvs","displayName":"user"}} authenticator produced credentialId AkXU5KRzDgeoY4RFXG0mqg and a 178-byte CBOR attestation object $ POST /webauthn/register HTTP 200 {"success":true} password session -> GET /passkey-only: HTTP 302, Location: http://localhost:8080/login?factor.type=webauthn&factor.reason=missing logged out, cookie jar emptied === One-time token: the way in when there is no passkey yet, and the way back === POST /ott/generate -> HTTP 302, Location: http://localhost:8080/login/ott token delivered out of band (the handler wrote it to a file): 9ae7d90d-74fe-49ec-9502-fe857fe8972a POST /login/ott -> HTTP 302, Location: http://localhost:8080/ $ GET /me HTTP 200 {"name":"user","authenticationType":"OneTimeTokenAuthentication","authorities":["FactorGrantedAuthority [authority=FACTOR_OTT, issuedAt=2026-08-25T17:20:54.745020051Z]","ROLE_USER"]} === The same token, a second time === POST /login/ott -> HTTP 302, Location: http://localhost:8080/login?error one-time-token session -> GET /passkey-only: HTTP 302, Location: http://localhost:8080/login?factor.type=webauthn&factor.reason=missing logged out, cookie jar emptied $ POST /login/webauthn HTTP 200 {"authenticated":true,"redirectUrl":"/"} $ passkey session -> GET /passkey-only HTTP 200 {"ok":"this endpoint required FACTOR_WEBAUTHN"}