-
Notifications
You must be signed in to change notification settings - Fork 43
Description
Summary
Wrap Spring Security 7's built-in Multi-Factor Authentication infrastructure in simple user.mfa.* configuration properties, consistent with how we wrap passkeys via user.webauthn.*.
A consuming app would enable MFA with:
user.mfa.enabled: true
user.mfa.factors: PASSWORD, WEBAUTHNThis makes ALL authenticated endpoints require all configured factors. Spring Security 7 handles the enforcement, redirection between factor login pages, and session management automatically.
Background & Research
Spring Security 7 (shipped with Spring Boot 4, which we already target) has built-in MFA via:
FactorGrantedAuthority— automatically added toAuthenticationon each successful auth stepFactorGrantedAuthority.WEBAUTHN_AUTHORITY— predefined constant for passkeys (ourWebAuthnAuthenticationProvideralready adds this)FactorGrantedAuthority.PASSWORD_AUTHORITY— for password login@EnableMultiFactorAuthentication— annotation to globally require multiple factorsAuthorizationManagerFactories.multiFactor()— programmatic per-endpoint factor requirementsDelegatingMissingAuthorityAccessDeniedHandler— redirects partially-authenticated users to the correct factor's login page
Current State
Our WebAuthnAuthenticationSuccessHandler already preserves FactorGrantedAuthority.WEBAUTHN_AUTHORITY through the principal conversion. The MFA infrastructure essentially works today — a consuming app could manually use @EnableMultiFactorAuthentication with our library. This issue is about making it easy.
Compliance Context
- PCI DSS 4.x (FAQ 1596): Passkeys satisfy MFA alone if factors are separate and secure, but many auditors prefer explicit two-step auth
- NIST SP 800-63-4: Synced passkeys can achieve AAL2; AAL2 must offer phishing-resistant option
- Industry: Keycloak, Auth0, Okta all support passkeys as both password replacement AND MFA — configurable per-deployment
Implementation Plan
New Files (~6)
-
MfaConfigProperties.java—@ConfigurationProperties(prefix = "user.mfa")boolean enabled(default: false)List<String> factors(e.g., PASSWORD, WEBAUTHN)String passwordEntryPointUri— redirect when PASSWORD factor missingString webauthnEntryPointUri— redirect when WEBAUTHN factor missing
-
MfaConfiguration.java—@Configuration+@ConditionalOnProperty(name = "user.mfa.enabled")- Programmatically replicates
@EnableMultiFactorAuthentication(which needs compile-time constants) - Bean:
DefaultAuthorizationManagerFactory<?>viaAuthorizationManagerFactories.multiFactor().requireFactors(...)— makes.authenticated()require all factors - Bean:
BeanPostProcessorthat callssetMfaEnabled(true)on all auth filters - Static helper:
mapFactorToAuthority("PASSWORD")→FactorGrantedAuthority.PASSWORD_AUTHORITY - Startup validation: error if factors empty, error if WEBAUTHN in factors but webauthn disabled, warning re: passwordless accounts + PASSWORD factor
- Programmatically replicates
-
MfaStatusResponse.java— DTO:mfaEnabled,requiredFactors,satisfiedFactors,missingFactors,fullyAuthenticated -
MfaAPI.java—@RestControllerat/user/mfaGET /user/mfa/status— returns which factors are required/satisfied/missing for current session- Must be accessible to partially-authenticated users (added to unprotected URIs)
Modified Files (~4)
-
WebSecurityConfig.java- Inject
ObjectProvider<MfaConfigProperties> - Add
setupMfa()— configuresDelegatingMissingAuthorityAccessDeniedHandler - Update
getUnprotectedURIsList()— add/user/mfa/**when MFA enabled
- Inject
-
dsspringuserconfig.properties— add MFA defaults -
WebAuthnAuthenticationSuccessHandlerTest.java— add test verifyingFactorGrantedAuthority.WEBAUTHN_AUTHORITYpreservation -
New test files —
MfaConfigurationTest,MfaAPITest,MfaSecurityTest
Edge Cases
- Passwordless accounts + PASSWORD factor: Users with passkey-only accounts can't satisfy PASSWORD. Startup warning + documentation.
- OAuth2 + MFA coexistence: OAuth2 uses
authenticationEntryPoint(unauthenticated). MFA usesaccessDeniedHandler(partially-authenticated). No conflict. - WebAuthn endpoints during MFA flow: Filters process before authorization — no URI exemptions needed.
Out of Scope (Future)
- Per-URI step-up authentication (
user.mfa.protectedURIs) - Factor validity duration (
user.mfa.factorValidity: 30m) - MFA challenge UI pages (see companion issue in DemoApp repo)
Related
- Companion issue in SpringUserFrameworkDemoApp for MFA challenge UI pages
- Spring Security 7 MFA docs: https://docs.spring.io/spring-security/reference/servlet/authentication/mfa.html
- Spring Security 7 MFA blog: https://spring.io/blog/2025/10/21/multi-factor-authentication-in-spring-security-7/