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:
34
docs/output/pk-attestation.txt
Normal file
34
docs/output/pk-attestation.txt
Normal 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"]}
|
||||
15
docs/output/pk-bootstrap.txt
Normal file
15
docs/output/pk-bootstrap.txt
Normal 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)
|
||||
36
docs/output/pk-ceremony.txt
Normal file
36
docs/output/pk-ceremony.txt
Normal 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"]}
|
||||
64
docs/output/pk-counter.txt
Normal file
64
docs/output/pk-counter.txt
Normal 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
|
||||
22
docs/output/pk-duplicate.txt
Normal file
22
docs/output/pk-duplicate.txt
Normal 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"}
|
||||
29
docs/output/pk-filters.txt
Normal file
29
docs/output/pk-filters.txt
Normal 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
38
docs/output/pk-jdbc.txt
Normal 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
39
docs/output/pk-origin.txt
Normal 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
15
docs/output/pk-ott.txt
Normal 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
|
||||
41
docs/output/pk-step-up.txt
Normal file
41
docs/output/pk-step-up.txt
Normal 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"}
|
||||
15
docs/output/pk-test-run.txt
Normal file
15
docs/output/pk-test-run.txt
Normal 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] ------------------------------------------------------------------------
|
||||
42
docs/output/pk-user-verification.txt
Normal file
42
docs/output/pk-user-verification.txt
Normal 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
|
||||
Reference in New Issue
Block a user