Skip to content

Handle step-up-required (401) by re-running the passkey ceremony on sensitive operations #75

Description

@devondragon

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

  1. 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.
  2. 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.
  3. 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.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions