diff --git a/config/src/main/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthentication.java b/config/src/main/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthentication.java
new file mode 100644
index 0000000000..ddc5840442
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthentication.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.authorization;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Import;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
+
+/**
+ * Exposes a {@link DefaultAuthorizationManagerFactory} as a Bean with the
+ * {@link #authorities()} specified as additional required authorities. The configuration
+ * will be picked up by both
+ * {@link org.springframework.security.config.annotation.web.configuration.EnableWebSecurity}
+ * and
+ * {@link org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity}.
+ *
+ *
+
+ * @Configuration
+ * @EnableGlobalMultiFactorAuthentication(authorities = { GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
+ * public class MyConfiguration {
+ * // ...
+ * }
+ *
+ *
+ * NOTE: At this time reactive applications do not support MFA and thus are not impacted.
+ * This will likely be enhanced in the future.
+ *
+ * @author Rob Winch
+ * @since 7.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Documented
+@Import(GlobalMultiFactorAuthenticationConfiguration.class)
+public @interface EnableGlobalMultiFactorAuthentication {
+
+ /**
+ * The additional authorities that are required.
+ * @return the additional authorities that are required (e.g. {
+ * GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
+ * @see org.springframework.security.core.GrantedAuthorities
+ */
+ String[] authorities();
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/authorization/GlobalMultiFactorAuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authorization/GlobalMultiFactorAuthenticationConfiguration.java
new file mode 100644
index 0000000000..7ebf32f261
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/authorization/GlobalMultiFactorAuthenticationConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.authorization;
+
+import java.util.Map;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ImportAware;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
+
+/**
+ * Uses {@link EnableGlobalMultiFactorAuthentication} to configure a
+ * {@link DefaultAuthorizationManagerFactory}.
+ *
+ * @author Rob Winch
+ * @since 7.0
+ * @see EnableGlobalMultiFactorAuthentication
+ */
+class GlobalMultiFactorAuthenticationConfiguration implements ImportAware {
+
+ private String[] authorities;
+
+ @Bean
+ DefaultAuthorizationManagerFactory authorizationManagerFactory(ObjectProvider roleHierarchy) {
+ DefaultAuthorizationManagerFactory.Builder builder = DefaultAuthorizationManagerFactory.builder()
+ .requireAdditionalAuthorities(this.authorities);
+ roleHierarchy.ifAvailable(builder::roleHierarchy);
+ return builder.build();
+ }
+
+ @Override
+ public void setImportMetadata(AnnotationMetadata importMetadata) {
+ Map multiFactorAuthenticationAttrs = importMetadata
+ .getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
+
+ this.authorities = (String[]) multiFactorAuthenticationAttrs.get("authorities");
+ }
+
+}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthenticationTests.java
new file mode 100644
index 0000000000..b38a6f43e4
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthenticationTests.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.authorization;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.core.GrantedAuthorities;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link EnableGlobalMultiFactorAuthentication}.
+ *
+ * @author Rob Winch
+ */
+@ExtendWith(SpringExtension.class)
+@WebAppConfiguration
+public class EnableGlobalMultiFactorAuthenticationTests {
+
+ @Autowired
+ MockMvc mvc;
+
+ @Autowired
+ Service service;
+
+ @Test
+ @WithMockUser(
+ authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY })
+ void webWhenAuthorized() throws Exception {
+ this.mvc.perform(get("/")).andExpect(status().isOk());
+ }
+
+ @Test
+ @WithMockUser
+ void webWhenNotAuthorized() throws Exception {
+ this.mvc.perform(get("/")).andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithMockUser(
+ authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY })
+ void methodWhenAuthorized() throws Exception {
+ Assertions.assertThatNoException().isThrownBy(() -> this.service.authenticated());
+ }
+
+ @Test
+ @WithMockUser
+ void methodWhenNotAuthorized() throws Exception {
+ Assertions.assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.service.authenticated());
+ }
+
+ @EnableWebSecurity
+ @EnableMethodSecurity
+ @Configuration
+ @EnableGlobalMultiFactorAuthentication(
+ authorities = { GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY })
+ static class Config {
+
+ @Bean
+ Service service() {
+ return new Service();
+ }
+
+ @Bean
+ MockMvc mvc(WebApplicationContext context) {
+ return MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
+ }
+
+ @RestController
+ static class OkController {
+
+ @GetMapping("/")
+ String ok() {
+ return "ok";
+ }
+
+ }
+
+ }
+
+ static class Service {
+
+ @PreAuthorize("isAuthenticated()")
+ void authenticated() {
+ }
+
+ }
+
+}
diff --git a/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc b/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc
index ef44b909c0..22446f4776 100644
--- a/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc
+++ b/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc
@@ -62,6 +62,14 @@ This yields a more familiar configuration:
include-code::./UseAuthorizationManagerFactoryConfiguration[tag=httpSecurity,indent=0]
+[[enable-global-mfa]]
+=== @EnableGlobalMultiFactorAuthentication
+
+You can simplify the configuration even further by using `@EnableGlobalMultiFactorAuthentication` to create the `AuthorizationManagerFactory` for you.
+
+include-code::./EnableGlobalMultiFactorAuthenticationConfiguration[tag=enable-global-mfa,indent=0]
+
+
[[obtaining-more-authorization]]
== Authorizing More Scopes
diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java
new file mode 100644
index 0000000000..eb569078dc
--- /dev/null
+++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java
@@ -0,0 +1,58 @@
+package org.springframework.security.docs.servlet.authentication.enableglobalmfa;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.core.GrantedAuthorities;
+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)
+// tag::enable-global-mfa[]
+@EnableGlobalMultiFactorAuthentication(authorities = {
+ GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+ GrantedAuthorities.FACTOR_OTT_AUTHORITY })
+// end::enable-global-mfa[]
+public class EnableGlobalMultiFactorAuthenticationConfiguration {
+
+ // tag::httpSecurity[]
+ @Bean
+ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ // @formatter:off
+ http
+ .authorizeHttpRequests((authorize) -> authorize
+ .requestMatchers("/admin/**").hasRole("ADMIN")
+ .anyRequest().authenticated()
+ )
+ .formLogin(Customizer.withDefaults())
+ .oneTimeTokenLogin(Customizer.withDefaults());
+ // @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/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java
new file mode 100644
index 0000000000..70dcb737a2
--- /dev/null
+++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.docs.servlet.authentication.enableglobalmfa;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.GrantedAuthorities;
+import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests {@link CustomX509Configuration}.
+ *
+ * @author Rob Winch
+ */
+@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
+@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
+public class EnableGlobalMultiFactorAuthenticationTests {
+
+ public final SpringTestContext spring = new SpringTestContext(this);
+
+ @Autowired
+ MockMvc mockMvc;
+
+ @Test
+ @WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY })
+ void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
+ this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
+ // @formatter:off
+ this.mockMvc.perform(get("/"))
+ .andExpect(status().isOk())
+ .andExpect(authenticated().withUsername("user"));
+ // @formatter:on
+ }
+
+ @Test
+ @WithMockUser(authorities = GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
+ void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception {
+ this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
+ // @formatter:off
+ this.mockMvc.perform(get("/"))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("http://localhost/login?factor=ott"));
+ // @formatter:on
+ }
+
+ @Test
+ @WithMockUser(authorities = GrantedAuthorities.FACTOR_OTT_AUTHORITY)
+ void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception {
+ this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
+ // @formatter:off
+ this.mockMvc.perform(get("/"))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("http://localhost/login?factor=password"));
+ // @formatter:on
+ }
+
+ @Test
+ @WithMockUser
+ void getWhenAuthenticatedThenRedirectsToPassword() throws Exception {
+ this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
+ // @formatter:off
+ this.mockMvc.perform(get("/"))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("http://localhost/login?factor=password"));
+ // @formatter:on
+ }
+
+ @Test
+ void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception {
+ this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
+ // @formatter:off
+ this.mockMvc.perform(get("/"))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("http://localhost/login"));
+ // @formatter:on
+ }
+
+ @RestController
+ static class Http200Controller {
+ @GetMapping("/**")
+ String ok() {
+ return "ok";
+ }
+ }
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt
new file mode 100644
index 0000000000..eea878f2da
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2004-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.kt.docs.servlet.authentication.enableglobalmfa
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.core.GrantedAuthorities
+import org.springframework.security.test.context.support.WithMockUser
+import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
+import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
+import org.springframework.test.context.TestExecutionListeners
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+
+/**
+ * Tests [CustomX509Configuration].
+ *
+ * @author Rob Winch
+ */
+@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
+@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
+class AuthorizationManagerFactoryTests {
+ @JvmField
+ val spring: SpringTestContext = SpringTestContext(this)
+
+ @Autowired
+ var mockMvc: MockMvc? = null
+
+ @Test
+ @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY])
+ @Throws(Exception::class)
+ fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
+ this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
+ .autowire()
+ // @formatter:off
+ this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
+ // @formatter:on
+ }
+
+ @Test
+ @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY])
+ @Throws(Exception::class)
+ fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() {
+ this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
+ .autowire()
+ // @formatter:off
+ this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+ .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+ .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=ott"))
+ // @formatter:on
+ }
+
+ @Test
+ @WithMockUser(authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY])
+ @Throws(Exception::class)
+ fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
+ this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
+ .autowire()
+ // @formatter:off
+ this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+ .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+ .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password"))
+ // @formatter:on
+ }
+
+ @Test
+ @WithMockUser
+ @Throws(Exception::class)
+ fun getWhenAuthenticatedThenRedirectsToPassword() {
+ this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
+ .autowire()
+ // @formatter:off
+ this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+ .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+ .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor=password"))
+ // @formatter:on
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun getWhenUnauthenticatedThenRedirectsToBoth() {
+ this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
+ .autowire()
+ // @formatter:off
+ this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+ .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+ .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login"))
+ // @formatter:on
+ }
+
+ @RestController
+ internal class Http200Controller {
+ @GetMapping("/**")
+ fun ok(): String {
+ return "ok"
+ }
+ }
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/ListAuthoritiesEverywhereConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/ListAuthoritiesEverywhereConfiguration.kt
new file mode 100644
index 0000000000..7c00ede58d
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/ListAuthoritiesEverywhereConfiguration.kt
@@ -0,0 +1,54 @@
+package org.springframework.security.kt.docs.servlet.authentication.enableglobalmfa
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+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.GrantedAuthorities
+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 ListAuthoritiesEverywhereConfiguration {
+
+ // tag::httpSecurity[]
+ @Bean
+ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
+ // @formatter:off
+ http {
+ authorizeHttpRequests {
+ authorize("/admin/**", hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN")) // <1>
+ authorize(anyRequest, hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY))
+ }
+ formLogin { }
+ oneTimeTokenLogin { }
+ }
+ // @formatter:on
+ return http.build()
+ }
+ // end::httpSecurity[]
+
+
+ // end::httpSecurity[]
+ @Bean
+ fun userDetailsService(): UserDetailsService {
+ return InMemoryUserDetailsManager(
+ User.withDefaultPasswordEncoder()
+ .username("user")
+ .password("password")
+ .authorities("app")
+ .build()
+ )
+ }
+
+ @Bean
+ fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
+ return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
+ }
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/UseAuthorizationManagerFactoryConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/UseAuthorizationManagerFactoryConfiguration.kt
new file mode 100644
index 0000000000..f037dd8023
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/UseAuthorizationManagerFactoryConfiguration.kt
@@ -0,0 +1,61 @@
+package org.springframework.security.kt.docs.servlet.authentication.enableglobalmfa
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authorization.AuthorizationManagerFactory
+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.GrantedAuthorities
+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 UseAuthorizationManagerFactoryConfiguration {
+ // tag::httpSecurity[]
+ @Bean
+ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
+ // @formatter:off
+ http {
+ authorizeHttpRequests {
+ authorize("/admin/**", hasRole("ADMIN"))
+ authorize(anyRequest, authenticated)
+ }
+ formLogin { }
+ oneTimeTokenLogin { }
+ }
+ // @formatter:on
+ return http.build()
+ }
+ // end::httpSecurity[]
+
+ // tag::authorizationManagerFactoryBean[]
+ @Bean
+ fun authz(): AuthorizationManagerFactory {
+ return DefaultAuthorizationManagerFactory.builder()
+ .requireAdditionalAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY).build()
+ }
+ // end::authorizationManagerFactoryBean[]
+
+ @Bean
+ fun userDetailsService(): UserDetailsService {
+ return InMemoryUserDetailsManager(
+ User.withDefaultPasswordEncoder()
+ .username("user")
+ .password("password")
+ .authorities("app")
+ .build()
+ )
+ }
+
+ @Bean
+ fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
+ return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
+ }
+}