1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Polish AllRequiredFactorsAuthorizationManager.anyOf

- Add validation
- Extract to static inner class
- Uniqueness determined by Set rather than requiredFactor
  This is important for the failure with the same RequiredFactor, but a
  different reason
- Add documentation

Signed-off-by: Robert Winch <362503+rwinch@users.noreply.github.com>
This commit is contained in:
Robert Winch
2026-03-31 10:49:03 -05:00
parent 6b09352a93
commit ff820a868e
6 changed files with 271 additions and 21 deletions
@@ -125,6 +125,23 @@ include-code::./ValidDurationConfiguration[tag=httpSecurity,indent=0]
<5> Otherwise, authentication is required, but it does not care if it is a password or how long ago authentication occurred
<6> Set up the authentication mechanisms that can provide the required factors.
[[all-factors-anyof]]
== AllRequiredFactorsAuthorizationManager.anyOf
In the previous examples, access requires satisfying that the user has authenticated with all factors.
There are times when an application wants to allow users to satisfy one of several different combinations of factors.
javadoc:org.springframework.security.authorization.AllRequiredFactorsAuthorizationManager#anyOf(AllRequiredFactorsAuthorizationManager...)[AllRequiredFactorsAuthorizationManager.anyOf] grants access if at least one of the provided combinations of factors is satisfied.
Consider a scenario where a user can authenticate with WebAuthn alone, or with both a password and a one-time token.
include-code::./AnyOfRequiredFactorsConfiguration[tag=httpSecurity,indent=0]
<1> Require WebAuthn
<2> Require both a password and a one-time token
<3> Combine the combinations of factors with `anyOf`, granting access if either is satisfied
<4> URLs that begin with `/protected/**` require the user to satisfy either combination of factors
<5> All other requests require only authentication
<6> Set up the authentication mechanisms that can provide the required factors
[[programmatic-mfa]]
== Programmatic MFA
+1
View File
@@ -4,6 +4,7 @@
== Core
* https://github.com/spring-projects/spring-security/pull/18634[gh-18634] - Added javadoc:org.springframework.security.util.matcher.InetAddressMatcher[]
* https://github.com/spring-projects/spring-security/issues/18960[gh-18960] - Added xref:servlet/authentication/mfa.adoc#all-factors-anyof[AllRequiredFactorsAuthorizationManager.anyOf]
== Web
* https://github.com/spring-projects/spring-security/issues/18755[gh-18755] - Include `charset` in `WWW-Authenticate` header
@@ -0,0 +1,76 @@
package org.springframework.security.docs.servlet.authentication.allfactorsanyof;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AllRequiredFactorsAuthorizationManager;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
class AnyOfRequiredFactorsConfiguration {
// tag::httpSecurity[]
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
// @formatter:off
// <1>
AllRequiredFactorsAuthorizationManager<Object> webauthn = AllRequiredFactorsAuthorizationManager
.<Object>builder()
.requireFactor((factor) -> factor.webauthnAuthority())
.build();
// <2>
AllRequiredFactorsAuthorizationManager<Object> passwordAndOtt = AllRequiredFactorsAuthorizationManager
.<Object>builder()
.requireFactor((factor) -> factor.passwordAuthority())
.requireFactor((factor) -> factor.ottAuthority())
.build();
// <3>
DefaultAuthorizationManagerFactory<Object> mfa = new DefaultAuthorizationManagerFactory<>();
mfa.setAdditionalAuthorization(AllRequiredFactorsAuthorizationManager.anyOf(webauthn, passwordAndOtt));
http
.authorizeHttpRequests((authorize) -> authorize
// <4>
.requestMatchers("/protected/**").access(mfa.authenticated())
// <5>
.anyRequest().authenticated()
)
// <6>
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults())
.webAuthn((webAuthn) -> webAuthn
.rpName("Spring Security")
.rpId("example.com")
.allowedOrigins("https://example.com")
);
// @formatter:on
return http.build();
}
// end::httpSecurity[]
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.authorities("app")
.build()
);
}
@Bean
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
}
}
@@ -0,0 +1,77 @@
package org.springframework.security.kt.docs.servlet.authentication.allfactorsanyof
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authorization.AllRequiredFactorsAuthorizationManager
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
internal class AnyOfRequiredFactorsConfiguration {
// tag::httpSecurity[]
@Bean
@Throws(Exception::class)
fun springSecurity(http: HttpSecurity): SecurityFilterChain? {
// @formatter:off
// <1>
val webauthn = AllRequiredFactorsAuthorizationManager.builder<Any>()
.requireFactor { factor -> factor.webauthnAuthority() }
.build()
// <2>
val passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder<Any>()
.requireFactor { factor -> factor.passwordAuthority() }
.requireFactor { factor -> factor.ottAuthority() }
.build()
// <3>
val mfa = DefaultAuthorizationManagerFactory<Any>()
mfa.setAdditionalAuthorization(AllRequiredFactorsAuthorizationManager.anyOf(webauthn, passwordAndOtt))
http {
authorizeHttpRequests {
// <4>
authorize("/protected/**", mfa.authenticated())
// <5>
authorize(anyRequest, authenticated)
}
// <6>
formLogin { }
oneTimeTokenLogin { }
webAuthn {
rpName = "Spring Security"
rpId = "example.com"
allowedOrigins = setOf("https://example.com")
}
}
// @formatter:on
return http.build()
}
// end::httpSecurity[]
@Suppress("DEPRECATION")
@Bean
fun userDetailsService(): UserDetailsService {
return InMemoryUserDetailsManager(
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.authorities("app")
.build()
)
}
@Bean
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
}
}