From 6b09352a9348009a466f1354b2971defca7b4f81 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Fri, 27 Mar 2026 03:36:09 +0200 Subject: [PATCH 1/2] Add AllRequiredFactorsAuthorizationManager.anyOf Closes gh-18960 Signed-off-by: Evgeniy Cheban --- ...llRequiredFactorsAuthorizationManager.java | 29 +++++++++ ...uiredFactorsAuthorizationManagerTests.java | 62 ++++++++++++++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java index 8691b0333c..e1ba0fc726 100644 --- a/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java @@ -20,7 +20,9 @@ import java.time.Clock; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @@ -40,6 +42,7 @@ import org.springframework.util.Assert; * is not expired for each {@link RequiredFactor}. * * @author Rob Winch + * @author Evgeniy Cheban * @since 7.0 * @see AuthorityAuthorizationManager */ @@ -49,6 +52,32 @@ public final class AllRequiredFactorsAuthorizationManager implements Authoriz private final List requiredFactors; + /** + * Creates an {@link AuthorizationManager} that grants access if at least one + * {@link AllRequiredFactorsAuthorizationManager} granted, collects + * {@link RequiredFactorError}s omitting duplicate errors of the same factor. + * @param the type of object that is being authorized + * @param managers the {@link AllRequiredFactorsAuthorizationManager}s to use + * @return the {@link AuthorizationManager} to use + * @since 7.1 + * @see AuthorizationManagers#anyOf(AuthorizationManager[]) + */ + @SafeVarargs + public static AuthorizationManager anyOf(AllRequiredFactorsAuthorizationManager... managers) { + return (authentication, object) -> { + Map factorErrors = new LinkedHashMap<>(); + for (AllRequiredFactorsAuthorizationManager manager : managers) { + FactorAuthorizationDecision decision = manager.authorize(authentication, object); + if (decision.isGranted()) { + return decision; + } + decision.getFactorErrors() + .forEach((e) -> factorErrors.putIfAbsent(e.getRequiredFactor().getAuthority(), e)); + } + return new FactorAuthorizationDecision(List.copyOf(factorErrors.values())); + }; + } + /** * Creates a new instance. * @param requiredFactors the authorities that are required. diff --git a/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java index 8d544727d9..ce9d10e2b0 100644 --- a/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java @@ -31,26 +31,37 @@ import org.springframework.security.core.authority.FactorGrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.InstanceOfAssertFactories.type; /** * Test {@link AllRequiredFactorsAuthorizationManager}. * * @author Rob Winch + * @author Evgeniy Cheban * @since 7.0 */ class AllRequiredFactorsAuthorizationManagerTests { private static final Object DOES_NOT_MATTER = new Object(); - private static RequiredFactor REQUIRED_PASSWORD = RequiredFactor + private static final RequiredFactor REQUIRED_PASSWORD = RequiredFactor .withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY) .build(); - private static RequiredFactor EXPIRING_PASSWORD = RequiredFactor + private static final RequiredFactor EXPIRING_PASSWORD = RequiredFactor .withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY) .validDuration(Duration.ofHours(1)) .build(); + private static final RequiredFactor REQUIRED_OTT = RequiredFactor + .withAuthority(FactorGrantedAuthority.OTT_AUTHORITY) + .build(); + + private static final RequiredFactor EXPIRING_OTT = RequiredFactor + .withAuthority(FactorGrantedAuthority.OTT_AUTHORITY) + .validDuration(Duration.ofHours(1)) + .build(); + @Test void authorizeWhenGranted() { AllRequiredFactorsAuthorizationManager allFactors = AllRequiredFactorsAuthorizationManager.builder() @@ -219,6 +230,53 @@ class AllRequiredFactorsAuthorizationManagerTests { assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_PASSWORD)); } + @Test + void anyOfWhenOneGrantedThenGranted() { + AllRequiredFactorsAuthorizationManager expiringPasswordAndOtt = AllRequiredFactorsAuthorizationManager + .builder() + .requireFactor(EXPIRING_PASSWORD) + .requireFactor(EXPIRING_OTT) + .build(); + AllRequiredFactorsAuthorizationManager passwordAndExpiringOtt = AllRequiredFactorsAuthorizationManager + .builder() + .requireFactor(REQUIRED_PASSWORD) + .requireFactor(EXPIRING_OTT) + .build(); + FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(EXPIRING_PASSWORD.getAuthority()) + .issuedAt(Instant.now().minus(Duration.ofHours(2))) + .build(); + FactorGrantedAuthority ottFactor = FactorGrantedAuthority.withAuthority(EXPIRING_OTT.getAuthority()).build(); + AuthorizationManager anyOf = AllRequiredFactorsAuthorizationManager.anyOf(expiringPasswordAndOtt, + passwordAndExpiringOtt); + Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor, ottFactor); + AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER); + assertThat(result).isNotNull(); + assertThat(result.isGranted()).isTrue(); + } + + @Test + void anyOfWhenRequiredFactorMissingThenMissing() { + AllRequiredFactorsAuthorizationManager passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder() + .requireFactor(REQUIRED_PASSWORD) + .requireFactor(REQUIRED_OTT) + .build(); + AllRequiredFactorsAuthorizationManager passwordAndExpiringOtt = AllRequiredFactorsAuthorizationManager + .builder() + .requireFactor(REQUIRED_PASSWORD) + .requireFactor(EXPIRING_OTT) + .build(); + FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(REQUIRED_PASSWORD.getAuthority()) + .build(); + AuthorizationManager anyOf = AllRequiredFactorsAuthorizationManager.anyOf(passwordAndOtt, + passwordAndExpiringOtt); + Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor); + AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER); + assertThat(result).asInstanceOf(type(FactorAuthorizationDecision.class)).satisfies((decision) -> { + assertThat(decision.isGranted()).isFalse(); + assertThat(decision.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_OTT)); + }); + } + @Test void setClockWhenNullThenIllegalArgumentException() { AllRequiredFactorsAuthorizationManager allFactors = AllRequiredFactorsAuthorizationManager.builder() From ff820a868e7baf5e690a081f2f4d7c5380f0aa69 Mon Sep 17 00:00:00 2001 From: Robert Winch <362503+rwinch@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:49:03 -0500 Subject: [PATCH 2/2] 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> --- ...llRequiredFactorsAuthorizationManager.java | 61 +++++++++++---- ...uiredFactorsAuthorizationManagerTests.java | 60 ++++++++++++++- .../pages/servlet/authentication/mfa.adoc | 17 ++++ docs/modules/ROOT/pages/whats-new.adoc | 1 + .../AnyOfRequiredFactorsConfiguration.java | 76 ++++++++++++++++++ .../AnyOfRequiredFactorsConfiguration.kt | 77 +++++++++++++++++++ 6 files changed, 271 insertions(+), 21 deletions(-) create mode 100644 docs/src/test/java/org/springframework/security/docs/servlet/authentication/allfactorsanyof/AnyOfRequiredFactorsConfiguration.java create mode 100644 docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/allfactorsanyof/AnyOfRequiredFactorsConfiguration.kt diff --git a/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java index e1ba0fc726..72576169ec 100644 --- a/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java @@ -20,11 +20,11 @@ import java.time.Clock; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -54,28 +54,23 @@ public final class AllRequiredFactorsAuthorizationManager implements Authoriz /** * Creates an {@link AuthorizationManager} that grants access if at least one - * {@link AllRequiredFactorsAuthorizationManager} granted, collects - * {@link RequiredFactorError}s omitting duplicate errors of the same factor. + * {@link AllRequiredFactorsAuthorizationManager} granted. When all managers deny, + * collects the unique {@link RequiredFactorError}s from each manager. * @param the type of object that is being authorized - * @param managers the {@link AllRequiredFactorsAuthorizationManager}s to use + * @param managers the {@link AllRequiredFactorsAuthorizationManager}s to use; cannot + * be empty or contain null elements * @return the {@link AuthorizationManager} to use * @since 7.1 * @see AuthorizationManagers#anyOf(AuthorizationManager[]) */ @SafeVarargs public static AuthorizationManager anyOf(AllRequiredFactorsAuthorizationManager... managers) { - return (authentication, object) -> { - Map factorErrors = new LinkedHashMap<>(); - for (AllRequiredFactorsAuthorizationManager manager : managers) { - FactorAuthorizationDecision decision = manager.authorize(authentication, object); - if (decision.isGranted()) { - return decision; - } - decision.getFactorErrors() - .forEach((e) -> factorErrors.putIfAbsent(e.getRequiredFactor().getAuthority(), e)); - } - return new FactorAuthorizationDecision(List.copyOf(factorErrors.values())); - }; + Assert.notEmpty(managers, "managers cannot be empty"); + Assert.noNullElements(managers, "managers cannot contain null elements"); + if (managers.length == 1) { + return managers[0]; + } + return new AnyOfFactorsAuthorizationManager<>(managers); } /** @@ -179,6 +174,38 @@ public final class AllRequiredFactorsAuthorizationManager implements Authoriz return new Builder<>(); } + /** + * An {@link AuthorizationManager} that grants access if at least one + * {@link AllRequiredFactorsAuthorizationManager} granted. When all deny, collects the + * unique {@link RequiredFactorError}s from each manager. + * + * @param the type of object being authorized + */ + private static final class AnyOfFactorsAuthorizationManager implements AuthorizationManager { + + private final AllRequiredFactorsAuthorizationManager[] managers; + + AnyOfFactorsAuthorizationManager(AllRequiredFactorsAuthorizationManager[] managers) { + Assert.notEmpty(managers, "managers cannot be empty"); + Assert.noNullElements(managers, "managers cannot contain null elements"); + this.managers = managers; + } + + @Override + public AuthorizationResult authorize(Supplier authentication, T object) { + Set factorErrors = new LinkedHashSet<>(); + for (AllRequiredFactorsAuthorizationManager manager : this.managers) { + FactorAuthorizationDecision decision = manager.authorize(authentication, object); + if (decision.isGranted()) { + return decision; + } + factorErrors.addAll(decision.getFactorErrors()); + } + return new FactorAuthorizationDecision(List.copyOf(factorErrors)); + } + + } + /** * A builder for {@link AllRequiredFactorsAuthorizationManager}. * diff --git a/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java index ce9d10e2b0..302a297bc7 100644 --- a/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java @@ -44,11 +44,11 @@ class AllRequiredFactorsAuthorizationManagerTests { private static final Object DOES_NOT_MATTER = new Object(); - private static final RequiredFactor REQUIRED_PASSWORD = RequiredFactor + private static RequiredFactor REQUIRED_PASSWORD = RequiredFactor .withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY) .build(); - private static final RequiredFactor EXPIRING_PASSWORD = RequiredFactor + private static RequiredFactor EXPIRING_PASSWORD = RequiredFactor .withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY) .validDuration(Duration.ofHours(1)) .build(); @@ -255,7 +255,7 @@ class AllRequiredFactorsAuthorizationManagerTests { } @Test - void anyOfWhenRequiredFactorMissingThenMissing() { + void anyOfWhenSameAuthorityDifferentValidDurationThenBothErrorsReturned() { AllRequiredFactorsAuthorizationManager passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder() .requireFactor(REQUIRED_PASSWORD) .requireFactor(REQUIRED_OTT) @@ -273,10 +273,62 @@ class AllRequiredFactorsAuthorizationManagerTests { AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER); assertThat(result).asInstanceOf(type(FactorAuthorizationDecision.class)).satisfies((decision) -> { assertThat(decision.isGranted()).isFalse(); - assertThat(decision.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_OTT)); + assertThat(decision.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_OTT), + RequiredFactorError.createMissing(EXPIRING_OTT)); }); } + @Test + void anyOfWhenIdenticalErrorInMultipleManagersThenDeduplicated() { + AllRequiredFactorsAuthorizationManager passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder() + .requireFactor(REQUIRED_PASSWORD) + .requireFactor(REQUIRED_OTT) + .build(); + AllRequiredFactorsAuthorizationManager passwordOnly = AllRequiredFactorsAuthorizationManager.builder() + .requireFactor(REQUIRED_PASSWORD) + .build(); + AuthorizationManager anyOf = AllRequiredFactorsAuthorizationManager.anyOf(passwordAndOtt, passwordOnly); + Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER); + assertThat(result).asInstanceOf(type(FactorAuthorizationDecision.class)).satisfies((decision) -> { + assertThat(decision.isGranted()).isFalse(); + assertThat(decision.getFactorErrors()).containsOnly(RequiredFactorError.createMissing(REQUIRED_PASSWORD), + RequiredFactorError.createMissing(REQUIRED_OTT)); + }); + } + + @Test + void anyOfWhenDeniedThenErrorsRetainedInManagerOrder() { + AllRequiredFactorsAuthorizationManager passwordOnly = AllRequiredFactorsAuthorizationManager.builder() + .requireFactor(REQUIRED_PASSWORD) + .build(); + AllRequiredFactorsAuthorizationManager ottOnly = AllRequiredFactorsAuthorizationManager.builder() + .requireFactor(REQUIRED_OTT) + .build(); + AuthorizationManager anyOf = AllRequiredFactorsAuthorizationManager.anyOf(passwordOnly, ottOnly); + Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER); + assertThat(result).asInstanceOf(type(FactorAuthorizationDecision.class)).satisfies((decision) -> { + assertThat(decision.isGranted()).isFalse(); + assertThat(decision.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_PASSWORD), + RequiredFactorError.createMissing(REQUIRED_OTT)); + }); + } + + @Test + void anyOfWhenEmptyManagersThenIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> AllRequiredFactorsAuthorizationManager.anyOf()); + } + + @Test + void anyOfWhenSingleManagerThenReturnsSameInstance() { + AllRequiredFactorsAuthorizationManager manager = AllRequiredFactorsAuthorizationManager.builder() + .requireFactor(REQUIRED_PASSWORD) + .build(); + AuthorizationManager result = AllRequiredFactorsAuthorizationManager.anyOf(manager); + assertThat(result == manager).isTrue(); + } + @Test void setClockWhenNullThenIllegalArgumentException() { AllRequiredFactorsAuthorizationManager allFactors = AllRequiredFactorsAuthorizationManager.builder() diff --git a/docs/modules/ROOT/pages/servlet/authentication/mfa.adoc b/docs/modules/ROOT/pages/servlet/authentication/mfa.adoc index 6de25b4c78..f4b7c1e776 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/mfa.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/mfa.adoc @@ -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 diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 54bb18fb42..4d4a71f4e9 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -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 diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/allfactorsanyof/AnyOfRequiredFactorsConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/allfactorsanyof/AnyOfRequiredFactorsConfiguration.java new file mode 100644 index 0000000000..6a6f572723 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/allfactorsanyof/AnyOfRequiredFactorsConfiguration.java @@ -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 webauthn = AllRequiredFactorsAuthorizationManager + .builder() + .requireFactor((factor) -> factor.webauthnAuthority()) + .build(); + // <2> + AllRequiredFactorsAuthorizationManager passwordAndOtt = AllRequiredFactorsAuthorizationManager + .builder() + .requireFactor((factor) -> factor.passwordAuthority()) + .requireFactor((factor) -> factor.ottAuthority()) + .build(); + // <3> + DefaultAuthorizationManagerFactory 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"); + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/allfactorsanyof/AnyOfRequiredFactorsConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/allfactorsanyof/AnyOfRequiredFactorsConfiguration.kt new file mode 100644 index 0000000000..6d79563926 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/allfactorsanyof/AnyOfRequiredFactorsConfiguration.kt @@ -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() + .requireFactor { factor -> factor.webauthnAuthority() } + .build() + // <2> + val passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder() + .requireFactor { factor -> factor.passwordAuthority() } + .requireFactor { factor -> factor.ottAuthority() } + .build() + // <3> + val mfa = DefaultAuthorizationManagerFactory() + 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") + } + +}