Context
SpringUserFramework is adding an opt-in WebAuthn step-up primitive (library issue: devondragon/SpringUserFramework#335, finding SUF-02). When user.security.stepUp.enabled=true, sensitive operations on passwordless (passkey-only) accounts require a WebAuthn assertion performed within a short TTL (default 300s):
POST /user/setPassword
- passkey delete / rename (
WebAuthnManagementAPI)
The library ships the server side. This app needs the client handling and the E2E coverage, and serves as the reference implementation.
Scope changed 2026-08-18. This issue originally specified a bespoke ceremony against POST /user/stepup/options and POST /user/stepup/verify. Those endpoints are not being built. The library now expresses step-up as a freshness requirement on the WebAuthn factor that Spring Security already issues, so the step-up ceremony is the ordinary passkey login ceremony, re-run while the user is still logged in. Re-running it refreshes the factor's issuedAt and preserves the session's other authorities.
What the demo app needs
- Handle the
step-up-required response. Sensitive endpoints return 401 with code step-up-required when no sufficiently recent assertion exists. On that response, run the existing passkey ceremony and retry the original call.
- Reuse the existing ceremony.
src/main/resources/static/js/user/webauthn-authenticate.js already exports authenticateWithPasskey(), which does /webauthn/authenticate/options -> navigator.credentials.get() -> /login/webauthn. No new ceremony code, and no new endpoints to call.
- UI wiring on the affected flows: set-password on a passwordless account, and passkey delete/rename in
webauthn-manage.js. Prompt the user before triggering the authenticator so the browser dialog is not a surprise.
- Playwright E2E for the enabled path (
user.security.stepUp.enabled=true), following the virtual-authenticator setup already used in playwright/tests/mfa/mfa-flow.spec.ts: passwordless account, attempt a sensitive op, ceremony, success; plus the negative path (stale or absent factor, no ceremony, 401).
Why the E2E matters more than usual
Step-up is a full re-login, so the session-fixation strategy runs, the framework's documented duplicate InteractiveAuthenticationSuccessEvent fires, and the /login/webauthn JSON response lands in the middle of another flow. That was checked by reading the code but not in a browser; this issue is where it actually gets verified.
Notes
Context
SpringUserFramework is adding an opt-in WebAuthn step-up primitive (library issue: devondragon/SpringUserFramework#335, finding SUF-02). When
user.security.stepUp.enabled=true, sensitive operations on passwordless (passkey-only) accounts require a WebAuthn assertion performed within a short TTL (default 300s):POST /user/setPasswordWebAuthnManagementAPI)The library ships the server side. This app needs the client handling and the E2E coverage, and serves as the reference implementation.
Scope changed 2026-08-18. This issue originally specified a bespoke ceremony against
POST /user/stepup/optionsandPOST /user/stepup/verify. Those endpoints are not being built. The library now expresses step-up as a freshness requirement on the WebAuthn factor that Spring Security already issues, so the step-up ceremony is the ordinary passkey login ceremony, re-run while the user is still logged in. Re-running it refreshes the factor'sissuedAtand preserves the session's other authorities.What the demo app needs
step-up-requiredresponse. Sensitive endpoints return 401 with codestep-up-requiredwhen no sufficiently recent assertion exists. On that response, run the existing passkey ceremony and retry the original call.src/main/resources/static/js/user/webauthn-authenticate.jsalready exportsauthenticateWithPasskey(), which does/webauthn/authenticate/options->navigator.credentials.get()->/login/webauthn. No new ceremony code, and no new endpoints to call.webauthn-manage.js. Prompt the user before triggering the authenticator so the browser dialog is not a surprise.user.security.stepUp.enabled=true), following the virtual-authenticator setup already used inplaywright/tests/mfa/mfa-flow.spec.ts: passwordless account, attempt a sensitive op, ceremony, success; plus the negative path (stale or absent factor, no ceremony, 401).Why the E2E matters more than usual
Step-up is a full re-login, so the session-fixation strategy runs, the framework's documented duplicate
InteractiveAuthenticationSuccessEventfires, and the/login/webauthnJSON response lands in the middle of another flow. That was checked by reading the code but not in a browser; this issue is where it actually gets verified.Notes
stepUp.enabled=false, so existing demo flows are unaffected until the flag is turned on.