You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ship a built-in WebAuthn step-up primitive so consumers get step-up re-authentication out of the box, rather than having to implement the StepUpService SPI themselves.
Background
The security concern from SUF-02 (#328) is already closed: POST /user/setPassword now requires a StepUpService when one is provided and is disabled by default otherwise (shipped in #334). This ticket is the enhancement — a default, framework-provided step-up implementation — not a security gap.
Design
Revised 2026-08-18 after a spike (findings, assumptions pinned in #364). The original design proposed custom challenge/verify endpoints with a server-side challenge store, written before this project moved to Spring Boot 4.1 / Spring Security 7.1. SS 7.1 supplies the mechanism directly, so that surface is dropped. The superseded design is preserved in the local design doc's appendix.
Step-up is expressed as a freshness requirement on the WebAuthn factor:
WebAuthnAuthenticationProvider stamps FactorGrantedAuthority.fromAuthority("FACTOR_WEBAUTHN") on every assertion, with issuedAt defaulting to now.
RequiredFactor.validDuration(ttl) plus AllRequiredFactorsAuthorizationManager deny a factor older than the TTL.
AbstractAuthenticationProcessingFilter with mfaEnabled=true merges a re-assertion into the existing session, deduping by authority string in favor of the new authorities. Re-running /login/webauthn while logged in therefore refreshes the clock and keeps the user's roles.
So the ceremony is the ordinary passkey login, re-run. No new endpoints, no challenge storage, no session marker, no library-owned WebAuthnRelyingPartyOperations bean, and no second client ceremony.
The StepUpService SPI shipped in #334 is unchanged: a default implementation satisfies boolean isStepUpSatisfied(User, String, HttpServletRequest) by inspecting the current Authentication, so 5.3.x consumer implementations keep working and still take precedence.
Default StepUpService bean under @ConditionalOnMissingBean(StepUpService.class), applying the validDuration factor check.
Activate filter factor-merging when step-up is enabled, not only when user.mfa.enabled=true. The MfaFilterMergingConfiguration warning about flipping mfaEnabled on consumer-defined filters carries over and must be repeated in the step-up docs.
Extend the gate to WebAuthnManagementAPI delete/rename, which today are no-ops for passwordless accounts because requireCurrentPasswordIfSet has no password to check.
Sensitive operations rejected for staleness return 401 with code step-up-required; the client re-runs the passkey ceremony and retries.
Reject FACTOR_-prefixed authority names in user.roles-and-privileges at startup. AllRequiredFactors resolves a required factor by taking the first authority matching by string and type-checking only that one, and the provider appends its stamp after the UserDetailsService's authorities, so a consumer privilege named FACTOR_WEBAUTHN sorts first and shadows the genuine factor. The gate then denies immediately after a real assertion: fail-closed, but step-up becomes permanently unsatisfiable with an opaque symptom. Pinned in test: pin Spring Security factor-freshness behavior for WebAuthn step-up #364.
Docs (CONFIG.md, dsspringuserconfig.properties) and tests.
Open decisions
Binding model. Factor freshness is a per-session time window: one ceremony authorizes any sensitive op inside the TTL. The superseded design was single-use and action-bound. Weaker binding, standard mechanism, mitigated by a short TTL.
Accounts with no passkey. OAuth-only accounts have no WEBAUTHN factor to refresh. Either leave them on the existing allowInitialPasswordSetWithoutStepUp behavior, or make the required factor configurable, since PASSWORD and the other factors carry the same issuedAt.
Not yet verified
Step-up is a full re-login, so the session-fixation strategy runs, the documented duplicate InteractiveAuthenticationSuccessEvent fires, and the login JSON response lands mid-flow. Nothing in the code looks problematic, but only a browser can confirm it. That is the demo-app work below.
Scope / effort
Roughly a day for the library side, down from the original feature-scale estimate. Not fully validated end to end until the demo-app ceremony lands.
Summary
Ship a built-in WebAuthn step-up primitive so consumers get step-up re-authentication out of the box, rather than having to implement the
StepUpServiceSPI themselves.Background
The security concern from SUF-02 (#328) is already closed:
POST /user/setPasswordnow requires aStepUpServicewhen one is provided and is disabled by default otherwise (shipped in #334). This ticket is the enhancement — a default, framework-provided step-up implementation — not a security gap.Design
Revised 2026-08-18 after a spike (findings, assumptions pinned in #364). The original design proposed custom challenge/verify endpoints with a server-side challenge store, written before this project moved to Spring Boot 4.1 / Spring Security 7.1. SS 7.1 supplies the mechanism directly, so that surface is dropped. The superseded design is preserved in the local design doc's appendix.
Step-up is expressed as a freshness requirement on the WebAuthn factor:
WebAuthnAuthenticationProviderstampsFactorGrantedAuthority.fromAuthority("FACTOR_WEBAUTHN")on every assertion, withissuedAtdefaulting to now.RequiredFactor.validDuration(ttl)plusAllRequiredFactorsAuthorizationManagerdeny a factor older than the TTL.AbstractAuthenticationProcessingFilterwithmfaEnabled=truemerges a re-assertion into the existing session, deduping by authority string in favor of the new authorities. Re-running/login/webauthnwhile logged in therefore refreshes the clock and keeps the user's roles.So the ceremony is the ordinary passkey login, re-run. No new endpoints, no challenge storage, no session marker, no library-owned
WebAuthnRelyingPartyOperationsbean, and no second client ceremony.The
StepUpServiceSPI shipped in #334 is unchanged: a default implementation satisfiesboolean isStepUpSatisfied(User, String, HttpServletRequest)by inspecting the currentAuthentication, so 5.3.x consumer implementations keep working and still take precedence.Work
user.security.stepUp.*config:enabled(defaultfalse),ttlSeconds(default300),factor(defaultWEBAUTHN).StepUpServicebean under@ConditionalOnMissingBean(StepUpService.class), applying thevalidDurationfactor check.user.mfa.enabled=true. TheMfaFilterMergingConfigurationwarning about flippingmfaEnabledon consumer-defined filters carries over and must be repeated in the step-up docs.WebAuthnManagementAPIdelete/rename, which today are no-ops for passwordless accounts becauserequireCurrentPasswordIfSethas no password to check.step-up-required; the client re-runs the passkey ceremony and retries.FACTOR_-prefixed authority names inuser.roles-and-privilegesat startup.AllRequiredFactorsresolves a required factor by taking the first authority matching by string and type-checking only that one, and the provider appends its stamp after theUserDetailsService's authorities, so a consumer privilege namedFACTOR_WEBAUTHNsorts first and shadows the genuine factor. The gate then denies immediately after a real assertion: fail-closed, but step-up becomes permanently unsatisfiable with an opaque symptom. Pinned in test: pin Spring Security factor-freshness behavior for WebAuthn step-up #364.CONFIG.md,dsspringuserconfig.properties) and tests.Open decisions
allowInitialPasswordSetWithoutStepUpbehavior, or make the required factor configurable, sincePASSWORDand the other factors carry the sameissuedAt.Not yet verified
Step-up is a full re-login, so the session-fixation strategy runs, the documented duplicate
InteractiveAuthenticationSuccessEventfires, and the login JSON response lands mid-flow. Nothing in the code looks problematic, but only a browser can confirm it. That is the demo-app work below.Scope / effort
Roughly a day for the library side, down from the original feature-scale estimate. Not fully validated end to end until the demo-app ceremony lands.
References
StepUpService,user.security.allowInitialPasswordSetWithoutStepUp.