1
0

Add passkeys project: WebAuthn ceremonies, a software authenticator and the one-time-token fallback

Fourth Maven project in the repository. Registration and authentication run end to
end with no browser and no hardware key: VirtualAuthenticator emits real CBOR
attestation objects and real ES256 assertion signatures, and tools/PasskeyCeremony.java
drives the live HTTP endpoints with them.

Profiles cover userVerification REQUIRED, DIRECT attestation, a disallowed origin and
JDBC persistence. Eleven doc chapters and twelve captured transcripts under docs/passkeys
and docs/output/pk-*.txt, all regenerated by passkeys/scripts/run-all.sh.
This commit is contained in:
2026-08-25 22:54:40 +05:30
parent e9381dc5be
commit f6dd692177
59 changed files with 3567 additions and 4 deletions

View File

@@ -0,0 +1,34 @@
==============================================================================
attestation: DIRECT requested, attestation: none accepted
==============================================================================
=== Registration ceremony, then authentication ceremony, no browser involved ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
$ POST /webauthn/register/options
HTTP 200
{"attestation":"direct","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"},"challenge":"EDPeleI6Jl4Bjc6-cOqnnZqNHCt0P921DI6jsCNvCBQ","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":"OahmMEZ7JKUXyrKH9QxxzqtW7tnuMfUhmnHnmpg80Bc","displayName":"user"}}
authenticator produced credentialId m9GhJSmVMlSefrRpC7EitQ and a 178-byte CBOR attestation object
$ POST /webauthn/register
HTTP 200
{"success":true}
$ GET /diag/credentials
HTTP 200
{"principal":"user","principalType":"UsernamePasswordAuthenticationToken","authorities":["FactorGrantedAuthority [authority=FACTOR_PASSWORD, issuedAt=2026-08-25T17:20:10.080515066Z]","ROLE_USER"],"userEntity":{"name":"user","displayName":"user","id":"OahmMEZ7JKUXyrKH9QxxzqtW7tnuMfUhmnHnmpg80Bc"},"credentials":[{"label":"yubikey-on-my-desk","credentialId":"m9GhJSmVMlSefrRpC7EitQ","signatureCount":0,"uvInitialized":true,"backupEligible":true,"backupState":true,"transports":["hybrid","internal"],"attestationObjectBytes":178,"created":"2026-08-25T17:20:10.707040039Z","lastUsed":"2026-08-25T17:20:10.707040039Z"}]}
logged out, cookie jar emptied
=== Session dropped. Authenticating with the passkey alone ===
$ POST /webauthn/authenticate/options
HTTP 200
{"allowCredentials":[],"challenge":"01OJTPvWOBAxxyk3vTq1FFSaVBEqw-_tMMj2FDlFDq8","extensions":{},"rpId":"localhost","timeout":300000,"userVerification":"preferred"}
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
$ GET /me
HTTP 200
{"name":"user","authenticationType":"WebAuthnAuthentication","authorities":["FactorGrantedAuthority [authority=FACTOR_WEBAUTHN, issuedAt=2026-08-25T17:20:11.002451577Z]","ROLE_USER"]}

View File

@@ -0,0 +1,15 @@
==============================================================================
Bootstrapping - registering a passkey requires an existing authenticated session
==============================================================================
=== Asking for registration options with nobody logged in ===
$ POST /webauthn/register/options (anonymous)
HTTP 400
(empty body)
=== A one-time token for a username that does not exist ===
POST /ott/generate -> HTTP 302, Location: http://localhost:8080/login/ott
a token was still generated and delivered: d75e51fe-cbde-4add-8d70-231fc6ca490f
the response is byte-for-byte what a real username produces - no enumeration oracle
POST /login/ott -> HTTP 302, Location: http://localhost:8080/login?error (the failure lands here instead)

View File

@@ -0,0 +1,36 @@
==============================================================================
Registration and authentication ceremonies, driven without a browser
==============================================================================
Spring Security 7.1.1, Spring Boot 4.1.1, rpId localhost, default settings.
The authenticator is src/main/java/com/ankurm/passkeys/virtual/VirtualAuthenticator.java.
=== Registration ceremony, then authentication ceremony, no browser involved ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
$ POST /webauthn/register/options
HTTP 200
{"attestation":"none","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"},"challenge":"cfccFSLkLYqwn_qsRIDEpAa8fUnVtxkAB82NBijQyyA","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":"qJBkbDbHGSVdnYG0VqD0JAN-t7GAejJ3Lw3HDL9bKow","displayName":"user"}}
authenticator produced credentialId lAfNKiLC_virGa8Yrfr4gA and a 178-byte CBOR attestation object
$ POST /webauthn/register
HTTP 200
{"success":true}
$ GET /diag/credentials
HTTP 200
{"principal":"user","principalType":"UsernamePasswordAuthenticationToken","authorities":["FactorGrantedAuthority [authority=FACTOR_PASSWORD, issuedAt=2026-08-25T17:18:48.132461443Z]","ROLE_USER"],"userEntity":{"displayName":"user","name":"user","id":"qJBkbDbHGSVdnYG0VqD0JAN-t7GAejJ3Lw3HDL9bKow"},"credentials":[{"label":"yubikey-on-my-desk","credentialId":"lAfNKiLC_virGa8Yrfr4gA","signatureCount":0,"uvInitialized":true,"backupEligible":true,"backupState":true,"transports":["hybrid","internal"],"attestationObjectBytes":178,"created":"2026-08-25T17:18:48.764625858Z","lastUsed":"2026-08-25T17:18:48.764625858Z"}]}
logged out, cookie jar emptied
=== Session dropped. Authenticating with the passkey alone ===
$ POST /webauthn/authenticate/options
HTTP 200
{"allowCredentials":[],"challenge":"BUu8TaKJn6f43nZk4oL3GpD517F8cDol5XOlc7GULq8","extensions":{},"rpId":"localhost","timeout":300000,"userVerification":"preferred"}
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
$ GET /me
HTTP 200
{"name":"user","authenticationType":"WebAuthnAuthentication","authorities":["FactorGrantedAuthority [authority=FACTOR_WEBAUTHN, issuedAt=2026-08-25T17:18:49.046877669Z]","ROLE_USER"]}

View File

@@ -0,0 +1,64 @@
==============================================================================
Signature counter: stored on every assertion, compared against on none
==============================================================================
=== Signature counter: does the relying party detect a cloned authenticator? ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
$ POST /webauthn/register/options
HTTP 200
{"attestation":"none","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"},"challenge":"U-Ag0aFP_XRcaPxY293PTL-bRtb263WeXHdkShFPY58","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":"YD7ijO_ydB9lpHfkEczKQ-RGqud-lI0XnrvNYxXOF0M","displayName":"user"}}
authenticator produced credentialId vALRaznkn_0DdHi9VgpA2A and a 178-byte CBOR attestation object
$ POST /webauthn/register
HTTP 200
{"success":true}
logged out, cookie jar emptied
--- assertion 1, authenticator signCount = 1
$ POST /webauthn/authenticate/options
HTTP 200
{"allowCredentials":[],"challenge":"cYWUR_Jdr9MZZ-mRwzk7lnDPd9zGjzbaoaJy_Hwybrk","extensions":{},"rpId":"localhost","timeout":300000,"userVerification":"preferred"}
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
stored signatureCount now: 1
logged out, cookie jar emptied
--- assertion 2, authenticator signCount = 2
$ POST /webauthn/authenticate/options
HTTP 200
{"allowCredentials":[],"challenge":"HWqOXFziV2H1LG98g2MGr5qvbbrBLgsPi1yOBTw73qg","extensions":{},"rpId":"localhost","timeout":300000,"userVerification":"preferred"}
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
stored signatureCount now: 2
logged out, cookie jar emptied
--- assertion 3, authenticator signCount = 3
$ POST /webauthn/authenticate/options
HTTP 200
{"allowCredentials":[],"challenge":"GqB6o-RP_G76sVZm6KVk1MHBGziU61UlVTd62lzerYo","extensions":{},"rpId":"localhost","timeout":300000,"userVerification":"preferred"}
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
stored signatureCount now: 3
logged out, cookie jar emptied
=== Replaying a stale counter. A cloned key would look exactly like this ===
--- assertion 4, authenticator signCount = 1 (lower than the stored 3)
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
replayed a counter of 1 after the relying party had stored 3: HTTP 200

View File

@@ -0,0 +1,22 @@
==============================================================================
Registering the same credential id twice
==============================================================================
=== Registering the same credential id twice ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
$ POST /webauthn/register/options
HTTP 200
{"attestation":"none","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"},"challenge":"ypikZgeGfU0eoZxEDstXG6v9tHy2iayNhk8nx9m6KUY","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":"AX63-GT5WAL-SV1hZ4Ok7H1NrOA5AjFkfu3SFW1XOz0","displayName":"user"}}
authenticator produced credentialId 7fFoG7Cjo2WI-baGw1ik7g and a 178-byte CBOR attestation object
$ POST /webauthn/register
HTTP 200
{"success":true}
same authenticator, same credential id, second registration:
excludeCredentials now: [{"id":"7fFoG7Cjo2WI-baGw1ik7g","transports":["hybrid","internal"]
$ POST /webauthn/register
HTTP 500
{"timestamp":"2026-08-25T17:20:25.951Z","status":500,"error":"Internal Server Error","path":"/webauthn/register"}

View File

@@ -0,0 +1,29 @@
==============================================================================
The security filter chain with webAuthn() and oneTimeTokenLogin() configured
==============================================================================
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
1 DisableEncodeUrlFilter
2 WebAsyncManagerIntegrationFilter
3 SecurityContextHolderFilter
4 HeaderWriterFilter
5 CsrfFilter
6 LogoutFilter
7 GenerateOneTimeTokenFilter
8 UsernamePasswordAuthenticationFilter
9 OneTimeTokenAuthenticationFilter
10 DefaultResourcesFilter
11 DefaultResourcesFilter
12 DefaultResourcesFilter
13 DefaultLoginPageGeneratingFilter
14 DefaultLogoutPageGeneratingFilter
15 DefaultOneTimeTokenSubmitPageGeneratingFilter
16 WebAuthnAuthenticationFilter
17 RequestCacheAwareFilter
18 SecurityContextHolderAwareRequestFilter
19 AnonymousAuthenticationFilter
20 ExceptionTranslationFilter
21 PublicKeyCredentialCreationOptionsFilter
22 PublicKeyCredentialRequestOptionsFilter
23 AuthorizationFilter
24 WebAuthnRegistrationFilter
25 DefaultWebAuthnRegistrationPageGeneratingFilter

38
docs/output/pk-jdbc.txt Normal file
View File

@@ -0,0 +1,38 @@
==============================================================================
JDBC persistence - H2, with Spring Security's own schema
==============================================================================
schema-locations point at classpath:org/springframework/security/user-entities-schema.sql
and user-credentials-schema.sql, which live in spring-security-web, not in
spring-security-webauthn. Nothing creates these tables for you.
=== Registration ceremony, then authentication ceremony, no browser involved ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
$ POST /webauthn/register/options
HTTP 200
{"attestation":"none","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"},"challenge":"hahNpzhow1MxVRCiR8XSlgtl-C7u0a0jsv6J3REL-A8","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":"hfu2DnARNIH5VbqLz2F0gBR93S2Z9o2MmCakwWCgAgM","displayName":"user"}}
authenticator produced credentialId Pp2xpfO7m1rnhV0cqUDvvA and a 178-byte CBOR attestation object
$ POST /webauthn/register
HTTP 200
{"success":true}
$ GET /diag/credentials
HTTP 200
{"principal":"user","principalType":"UsernamePasswordAuthenticationToken","authorities":["FactorGrantedAuthority [authority=FACTOR_PASSWORD, issuedAt=2026-08-25T17:21:25.201127080Z]","ROLE_USER"],"userEntity":{"name":"user","displayName":"user","id":"hfu2DnARNIH5VbqLz2F0gBR93S2Z9o2MmCakwWCgAgM"},"credentials":[{"label":"yubikey-on-my-desk","credentialId":"Pp2xpfO7m1rnhV0cqUDvvA","signatureCount":0,"uvInitialized":true,"backupEligible":true,"backupState":true,"transports":["hybrid","internal"],"attestationObjectBytes":178,"created":"2026-08-25T17:21:25.819064Z","lastUsed":"2026-08-25T17:21:25.819064Z"}]}
logged out, cookie jar emptied
=== Session dropped. Authenticating with the passkey alone ===
$ POST /webauthn/authenticate/options
HTTP 200
{"allowCredentials":[],"challenge":"zS7lWt58FceogQ7sg7Uj6vfBOVgLafRlEeGpq5WZIjg","extensions":{},"rpId":"localhost","timeout":300000,"userVerification":"preferred"}
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
$ GET /me
HTTP 200
{"name":"user","authenticationType":"WebAuthnAuthentication","authorities":["FactorGrantedAuthority [authority=FACTOR_WEBAUTHN, issuedAt=2026-08-25T17:21:26.157551281Z]","ROLE_USER"]}

39
docs/output/pk-origin.txt Normal file
View File

@@ -0,0 +1,39 @@
==============================================================================
Registration from a disallowed origin
==============================================================================
=== Client data from an origin the relying party did not allow ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
origin sent by the client: http://evil.localhost:8080
origin allowed by the relying party: http://localhost:8080
$ POST /webauthn/register
HTTP 500
{"timestamp":"2026-08-25T17:19:51.884Z","status":500,"error":"Internal Server Error","path":"/webauthn/register"}
--- what the server logged ---
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) ~[webauthn4j-core-0.31.9.RELEASE.jar:na]
at com.webauthn4j.verifier.OriginVerifierImpl.verify(OriginVerifierImpl.java:48) ~[webauthn4j-core-0.31.9.RELEASE.jar:na]
at com.webauthn4j.verifier.RegistrationDataVerifier.verify(RegistrationDataVerifier.java:171) ~[webauthn4j-core-0.31.9.RELEASE.jar:na]
at com.webauthn4j.WebAuthnRegistrationManager.verify(WebAuthnRegistrationManager.java:337) ~[webauthn4j-core-0.31.9.RELEASE.jar:na]
==============================================================================
Assertion from a disallowed origin
==============================================================================
=== An assertion from a disallowed origin - the same mistake, one ceremony later ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
$ POST /webauthn/register/options
HTTP 200
{"attestation":"none","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"},"challenge":"IOcuyb_uTuoFxh1RYNr7H--vx1dVRNoM4CyXTdFK9cc","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":"aXe2d8pDxUar9cTAC93qmunRjRSnQolJcJaYQvFpoGk","displayName":"user"}}
authenticator produced credentialId IrIGbWFcDF7yRWZGkYJowQ and a 178-byte CBOR attestation object
$ POST /webauthn/register
HTTP 200
{"success":true}
logged out, cookie jar emptied
$ POST /login/webauthn (origin http://evil.localhost:8080)
HTTP 401
(empty body)

15
docs/output/pk-ott.txt Normal file
View File

@@ -0,0 +1,15 @@
==============================================================================
One-time token login - the way in, and the way back after a lost device
==============================================================================
=== 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): c5a7e60d-acd0-48a7-bb90-c03195fd783b
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:21:08.899894517Z]","ROLE_USER"]}
=== The same token, a second time ===
POST /login/ott -> HTTP 302, Location: http://localhost:8080/login?error

View File

@@ -0,0 +1,41 @@
==============================================================================
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"}

View File

@@ -0,0 +1,15 @@
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.ankurm.passkeys.PasskeyContractTests
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.942 s -- in com.ankurm.passkeys.PasskeyContractTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.331 s
[INFO] Finished at: 2026-08-25T22:52:02+05:30
[INFO] ------------------------------------------------------------------------

View File

@@ -0,0 +1,42 @@
==============================================================================
userVerification PREFERRED - the default
==============================================================================
=== A credential created and asserted with the UV flag clear ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
$ POST /webauthn/register/options
HTTP 200
{"attestation":"none","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"},"challenge":"dvOsS644o1RkfTwGf-OaGb5zAi2kv8J_CILmgnSyR_Q","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":"9kHjCZP7Duq18lf4acn_-dvW6csqKHiN0rOG6wQQysg","displayName":"user"}}
authenticator produced credentialId igJ93sbwKB1657SEcNaZiQ and a 178-byte CBOR attestation object
$ POST /webauthn/register
HTTP 200
{"success":true}
$ GET /diag/credentials (note uvInitialized)
HTTP 200
{"principal":"user","principalType":"UsernamePasswordAuthenticationToken","authorities":["FactorGrantedAuthority [authority=FACTOR_PASSWORD, issuedAt=2026-08-25T17:19:19.094840764Z]","ROLE_USER"],"userEntity":{"name":"user","id":"9kHjCZP7Duq18lf4acn_-dvW6csqKHiN0rOG6wQQysg","displayName":"user"},"credentials":[{"label":"no-uv","credentialId":"igJ93sbwKB1657SEcNaZiQ","signatureCount":0,"uvInitialized":false,"backupEligible":true,"backupState":true,"transports":["hybrid","internal"],"attestationObjectBytes":178,"created":"2026-08-25T17:19:19.696691237Z","lastUsed":"2026-08-25T17:19:19.696691237Z"}]}
logged out, cookie jar emptied
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
authentication with UV clear: HTTP 200
==============================================================================
userVerification REQUIRED - the uvrequired profile
==============================================================================
=== A credential created and asserted with the UV flag clear ===
POST /login (password) -> HTTP 302, Location: http://localhost:8080/
$ POST /webauthn/register/options
HTTP 200
{"attestation":"none","authenticatorSelection":{"residentKey":"required","userVerification":"required"},"challenge":"HoePMBML6zmDi7dBhbG034OdothhFfJC0M4al6MU5F0","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":"5xLUhpfzTToevAbZn3eHAfBAhIwj1w8RmVhtqaZpBPE","displayName":"user"}}
authenticator produced credentialId BdpidHMhT0YRUI9YNpMhTQ and a 178-byte CBOR attestation object
$ POST /webauthn/register
HTTP 500
{"timestamp":"2026-08-25T17:19:36.938Z","status":500,"error":"Internal Server Error","path":"/webauthn/register"}
registration refused, which is what userVerification REQUIRED does