1
0
mirror of synced 2026-05-22 13:23:17 +00:00

EnableGlobalMultiFactorAuthentication->EnableMultiFactorAuthentication

Closes gh-18127
This commit is contained in:
Rob Winch
2025-11-03 22:16:16 -06:00
parent aaf738f7ac
commit 884cf0d62e
12 changed files with 72 additions and 54 deletions
@@ -27,12 +27,12 @@ import org.springframework.security.authorization.AuthorizationManagerFactories;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
/**
* Uses {@link EnableGlobalMultiFactorAuthentication} to configure a
* Uses {@link EnableMultiFactorAuthentication} to configure a
* {@link DefaultAuthorizationManagerFactory}.
*
* @author Rob Winch
* @since 7.0
* @see EnableGlobalMultiFactorAuthentication
* @see EnableMultiFactorAuthentication
*/
class AuthorizationManagerFactoryConfiguration implements ImportAware {
@@ -49,7 +49,7 @@ class AuthorizationManagerFactoryConfiguration implements ImportAware {
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> multiFactorAuthenticationAttrs = importMetadata
.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName());
this.authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
}
@@ -26,9 +26,12 @@ 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
* Enables Multi-Factor Authentication (MFA) support within Spring Security.
*
* When {@link #authorities()} is specified creates 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}.
@@ -36,7 +39,7 @@ import org.springframework.security.authorization.DefaultAuthorizationManagerFac
* <pre>
* &#64;Configuration
* &#64;EnableGlobalMultiFactorAuthentication(authorities = { GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
* &#64;EnableMultiFactorAuthentication(authorities = { GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
* public class MyConfiguration {
* // ...
* }
@@ -51,8 +54,8 @@ import org.springframework.security.authorization.DefaultAuthorizationManagerFac
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(GlobalMultiFactorAuthenticationSelector.class)
public @interface EnableGlobalMultiFactorAuthentication {
@Import(MultiFactorAuthenticationSelector.class)
public @interface EnableMultiFactorAuthentication {
/**
* The additional authorities that are required.
@@ -25,19 +25,19 @@ import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
/**
* Uses {@link EnableGlobalMultiFactorAuthentication} to configure a
* Uses {@link EnableMultiFactorAuthentication} to configure a
* {@link DefaultAuthorizationManagerFactory}.
*
* @author Rob Winch
* @since 7.0
* @see EnableGlobalMultiFactorAuthentication
* @see EnableMultiFactorAuthentication
*/
class GlobalMultiFactorAuthenticationSelector implements ImportSelector {
class MultiFactorAuthenticationSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
Map<String, Object> multiFactorAuthenticationAttrs = metadata
.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName());
String[] authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
List<String> imports = new ArrayList<>(2);
if (authorities.length > 0) {
@@ -53,14 +53,14 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link EnableGlobalMultiFactorAuthentication}.
* Tests for {@link EnableMultiFactorAuthentication}.
*
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@WithMockUser(authorities = FactorGrantedAuthority.PASSWORD_AUTHORITY)
public class EnableGlobalMultiFactorAuthenticationFiltersSetTests {
public class EnableMultiFactorAuthenticationFiltersSetTests {
@Autowired
private AuthenticationManager manager;
@@ -105,7 +105,7 @@ public class EnableGlobalMultiFactorAuthenticationFiltersSetTests {
@EnableWebSecurity
@Configuration
@EnableGlobalMultiFactorAuthentication(
@EnableMultiFactorAuthentication(
authorities = { FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY })
static class Config {
@@ -59,13 +59,13 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link EnableGlobalMultiFactorAuthentication}.
* Tests for {@link EnableMultiFactorAuthentication}.
*
* @author Rob Winch
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
public class EnableGlobalMultiFactorAuthenticationTests {
public class EnableMultiFactorAuthenticationTests {
private static final String ATTR_NAME = "org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors$SecurityContextRequestPostProcessorSupport$TestSecurityContextRepository.REPO";
@@ -111,7 +111,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
@EnableGlobalMultiFactorAuthentication(
@EnableMultiFactorAuthentication(
authorities = { FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY })
static class Config {
@@ -18,17 +18,17 @@ In order to require MFA with Spring Security you must:
- Specify an authorization rule that requires multiple factors
- Setup authentication for each of those factors
[[egmfa]]
== @EnableGlobalMultiFactorAuthentication
[[emfa]]
== @EnableMultiFactorAuthentication
javadoc:org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication[format=annotation] simplifies Global MFA (the entire application requires MFA).
javadoc:org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication[format=annotation] makes it easy to enable multifactor authentication.
Below you can find a configuration that adds the requirement for both passwords and OTT to every authorization rule.
include-code::./EnableGlobalMultiFactorAuthenticationConfiguration[tag=enable-global-mfa,indent=0]
include-code::./EnableMultiFactorAuthenticationConfiguration[tag=enable-mfa,indent=0]
We are now able to concisely create a configuration that always requires multiple factors.
include-code::./EnableGlobalMultiFactorAuthenticationConfiguration[tag=httpSecurity,indent=0]
include-code::./EnableMultiFactorAuthenticationConfiguration[tag=httpSecurity,indent=0]
<1> URLs that begin with `/admin/**` require the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`, `ROLE_ADMIN`.
<2> Every other URL requires the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`
<3> Set up the authentication mechanisms that can provide the required factors.
@@ -40,18 +40,18 @@ If the user logged in initially with a token, then Spring Security redirects to
[[authorization-manager-factory]]
== AuthorizationManagerFactory
The `@EnableGlobalMultiFactorAuthentication` annotation is just a shortcut for publishing an javadoc:org.springframework.security.authorization.AuthorizationManagerFactory[] Bean.
The `@EnableMultiFactorAuthentication` `authorities` property is just a shortcut for publishing an javadoc:org.springframework.security.authorization.AuthorizationManagerFactory[] Bean.
When an `AuthorizationManagerFactory` Bean is available, it is used by Spring Security to create authorization rules, like `hasAnyRole(String)`, that are defined on the `AuthorizationManagerFactory` Bean interface.
The implementation published by `@EnableGlobalMultiFactorAuthentication` will ensure that each authorization is combined with the requirement of having the specified factors.
The implementation published by `@EnableMultiFactorAuthentication` will ensure that each authorization is combined with the requirement of having the specified factors.
The `AuthorizationManagerFactory` Bean below is what is published in the previously discussed xref:./mfa.adoc#using-egmfa[`@EnableGlobalMultiFactorAuthentication` example].
The `AuthorizationManagerFactory` Bean below is what is published in the previously discussed xref:./mfa.adoc#emfa[`@EnableMultiFactorAuthentication` example].
include-code::./UseAuthorizationManagerFactoryConfiguration[tag=authorizationManagerFactoryBean,indent=0]
[[selective-mfa]]
== Selectively Requiring MFA
We have demonstrated how to configure an entire application to require MFA (Global MFA) by using xref:./mfa.adoc#egmfa[`@EnableGlobalMultiFactorAuthentication`].
We have demonstrated how to configure an entire application to require MFA by using xref:./mfa.adoc#emfa[`@EnableMultiFactorAuthentication`]s `authorities` property.
However, there are times that an application only wants parts of the application to require MFA.
Consider the following requirements:
@@ -63,6 +63,13 @@ In this case, some URLs require MFA while others do not.
This means that the global approach that we saw before does not work.
Fortunately, we can use what we learned in xref:./mfa.adoc#authorization-manager-factory[] to solve this in a concise manner.
Start by specifying `@EnableMultiFactorAuthentication` without any authorities.
By doing so we enable MFA support, but no `AuthorizationManagerFactory` Bean is published.
include-code::./SelectiveMfaConfiguration[tag=enable-mfa,indent=0]
Next create an `AuthorizationManagerFactory` instance, but do not publish it as a Bean.
include-code::./SelectiveMfaConfiguration[tag=httpSecurity,indent=0]
<1> Create a `DefaultAuthorizationManagerFactory` as we did previously, but do not publish it as a Bean.
By not publishing it as a Bean, we are able to selectively use the `AuthorizationManagerFactory` instead of using it for every authorization rule.
@@ -1,9 +1,9 @@
package org.springframework.security.docs.servlet.authentication.egmfa;
package org.springframework.security.docs.servlet.authentication.emfa;
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.authorization.EnableMultiFactorAuthentication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.FactorGrantedAuthority;
@@ -16,12 +16,12 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
// tag::enable-global-mfa[]
@EnableGlobalMultiFactorAuthentication(authorities = {
// tag::enable-mfa[]
@EnableMultiFactorAuthentication(authorities = {
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY })
// end::enable-global-mfa[]
public class EnableGlobalMultiFactorAuthenticationConfiguration {
// end::enable-mfa[]
public class EnableMultiFactorAuthenticationConfiguration {
// tag::httpSecurity[]
@Bean
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.security.docs.servlet.authentication.egmfa;
package org.springframework.security.docs.servlet.authentication.emfa;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -44,7 +44,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
*/
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
public class EnableGlobalMultiFactorAuthenticationTests {
public class EnableMultiFactorAuthenticationTests {
public final SpringTestContext spring = new SpringTestContext(this);
@@ -54,7 +54,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
@Test
@WithMockUser(authorities = { FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY, "ROLE_USER" })
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
// @formatter:off
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
@@ -65,7 +65,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
@Test
@WithMockUser(authorities = FactorGrantedAuthority.PASSWORD_AUTHORITY)
void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
// @formatter:off
this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection())
@@ -76,7 +76,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
@Test
@WithMockUser(authorities = FactorGrantedAuthority.OTT_AUTHORITY)
void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
// @formatter:off
this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection())
@@ -87,7 +87,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
@Test
@WithMockUser
void getWhenAuthenticatedThenRedirectsToPassword() throws Exception {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
// @formatter:off
this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection())
@@ -97,7 +97,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
@Test
void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
this.spring.register(EnableMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
// @formatter:off
this.mockMvc.perform(get("/"))
.andExpect(status().is3xxRedirection())
@@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationManagerFactories;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.FactorGrantedAuthority;
@@ -16,6 +17,9 @@ import org.springframework.security.web.authentication.ott.OneTimeTokenGeneratio
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
@EnableWebSecurity
// tag::enable-mfa[]
@EnableMultiFactorAuthentication(authorities = {})
// end::enable-mfa[]
@Configuration(proxyBeanMethods = false)
class SelectiveMfaConfiguration {
@@ -1,8 +1,8 @@
package org.springframework.security.kt.docs.servlet.authentication.egmfa
package org.springframework.security.kt.docs.servlet.authentication.emfa
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication
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
@@ -17,12 +17,12 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
// tag::enable-global-mfa[]
@EnableGlobalMultiFactorAuthentication( authorities = [
// tag::enable-mfa[]
@EnableMultiFactorAuthentication( authorities = [
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY])
// end::enable-global-mfa[]
internal class EnableGlobalMultiFactorAuthenticationConfiguration {
// end::enable-mfa[]
internal class EnableMultiFactorAuthenticationConfiguration {
// tag::httpSecurity[]
@Bean
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.servlet.authentication.egmfa
package org.springframework.security.kt.docs.servlet.authentication.emfa
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.RestController
*/
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
class EnableGlobalMultiFactorAuthenticationConfigurationTests {
class EnableMultiFactorAuthenticationConfigurationTests {
@JvmField
val spring: SpringTestContext = SpringTestContext(this)
@@ -50,7 +50,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
@WithMockUser(authorities = [FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY, "ROLE_ADMIN"])
@Throws(Exception::class)
fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
// @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isOk())
@@ -62,7 +62,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
@WithMockUser(authorities = [FactorGrantedAuthority.PASSWORD_AUTHORITY])
@Throws(Exception::class)
fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
// @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
@@ -74,7 +74,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
@WithMockUser(authorities = [FactorGrantedAuthority.OTT_AUTHORITY])
@Throws(Exception::class)
fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
// @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
@@ -86,7 +86,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
@WithMockUser
@Throws(Exception::class)
fun getWhenAuthenticatedThenRedirectsToPassword() {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
// @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
@@ -97,7 +97,7 @@ class EnableGlobalMultiFactorAuthenticationConfigurationTests {
@Test
@Throws(Exception::class)
fun getWhenUnauthenticatedThenRedirectsToBoth() {
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
this.spring.register(EnableMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
// @formatter:off
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
@@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authorization.AuthorizationManagerFactories
import org.springframework.security.authorization.AuthorizationManagerFactory
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication
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
@@ -15,6 +16,9 @@ import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
// tag::enable-mfa[]
@EnableMultiFactorAuthentication(authorities = [])
// end::enable-mfa[]
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
internal class SelectiveMfaConfiguration {