Преглед изворни кода

Document Multi-Factor Simple to Complex

This reworks the Multi-Factor documentation to start with the
simplest scenario and work to progressively more complex requirements.

Closes gh-18029
Rob Winch пре 3 дана
родитељ
комит
e290c98e97
30 измењених фајлова са 1273 додато и 198 уклоњено
  1. 1 1
      docs/modules/ROOT/nav.adoc
  2. 0 109
      docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc
  3. 213 0
      docs/modules/ROOT/pages/servlet/authentication/mfa.adoc
  4. 1 1
      docs/modules/ROOT/pages/whats-new.adoc
  5. 5 1
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java
  6. 12 9
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java
  7. 2 2
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationTests.java
  8. 7 2
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/ListAuthoritiesConfiguration.java
  9. 1 1
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultiFactorAuthenticationTests.java
  10. 79 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfiguration.java
  11. 115 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfigurationTests.java
  12. 17 8
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfiguration.java
  13. 6 6
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.java
  14. 76 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfiguration.java
  15. 94 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfigurationTests.java
  16. 20 5
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/selectivemfa/SelectiveMfaConfiguration.java
  17. 116 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/selectivemfa/SelectiveMfaConfigurationTests.java
  18. 5 1
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.kt
  19. 14 6
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfiguration.kt
  20. 8 13
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfigurationTests.kt
  21. 7 2
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/ListAuthoritiesConfiguration.kt
  22. 1 1
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultiFactorAuthenticationTests.kt
  23. 15 4
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfiguration.kt
  24. 115 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfigurationTests.kt
  25. 15 7
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfiguration.kt
  26. 6 6
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.kt
  27. 76 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfiguration.kt
  28. 100 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfigurationTests.kt
  29. 19 13
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/selectivemfa/SelectiveMfaConfiguration.kt
  30. 127 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/selectivemfa/SelectiveMfaConfigurationTests.kt

+ 1 - 1
docs/modules/ROOT/nav.adoc

@@ -49,7 +49,7 @@
 ***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder]
 ***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
 ***** xref:servlet/authentication/passwords/ldap.adoc[LDAP]
-*** xref:servlet/authentication/adaptive.adoc[Multifactor Authentication]
+*** xref:servlet/authentication/mfa.adoc[Multi-Factor Authentication]
 *** xref:servlet/authentication/persistence.adoc[Persistence]
 *** xref:servlet/authentication/passkeys.adoc[Passkeys]
 *** xref:servlet/authentication/onetimetoken.adoc[One-Time Token]

+ 0 - 109
docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc

@@ -1,109 +0,0 @@
-= Adaptive Authentication
-
-Since authentication needs can vary from person-to-person and even from one login attempt to the next, Spring Security supports adapting authentication requirements to each situation.
-
-Some of the most common applications of this principal are:
-
-1. *Re-authentication* - Users need to provide authentication again in order to enter an area of elevated security
-2. *Multi-factor Authentication* - Users need more than one authentication mechanism to pass in order to access secured resources
-3. *Authorizing More Scopes* - Users are allowed to consent to a subset of scopes from an OAuth 2.0 Authorization Server.
-Then, if later on a scope that they did not grant is needed, consent can be re-requested for just that scope.
-4. *Opting-in to Stronger Authentication Mechanisms* - Users may not be ready yet to start using MFA, but the application wants to allow the subset of security-minded users to opt-in.
-5. *Requiring Additional Steps for Suspicious Logins* - The application may notice that the user's IP address has changed, that they are behind a VPN, or some other consideration that requires additional verification
-
-[[re-authentication]]
-== Re-authentication
-
-The most common of these is re-authentication.
-Imagine an application configured in the following way:
-
-include-code::./SimpleConfiguration[tag=httpSecurity,indent=0]
-
-By default, this application has two authentication mechanisms that it allows, meaning that the user could use either one and be fully-authenticated.
-
-If there is a set of endpoints that require a specific factor, we can specify that in `authorizeHttpRequests` as follows:
-
-include-code::./RequireOttConfiguration[tag=httpSecurity,indent=0]
-<1> - States that all `/profile/**` endpoints require one-time-token login to be authorized
-
-Given the above configuration, users can log in with any mechanism that you support.
-And, if they want to visit the profile page, then Spring Security will redirect them to the One-Time-Token Login page to obtain it.
-
-In this way, the authority given to a user is directly proportional to the amount of proof given.
-This adaptive approach allows users to give only the proof needed to perform their intended operations.
-
-[[multi-factor-authentication]]
-== Multi-Factor Authentication
-
-You may require that all users require both One-Time-Token login and Username/Password login to access any part of your site.
-
-To require both, you can state an authorization rule with `anyRequest` like so:
-
-include-code::./ListAuthoritiesConfiguration[tag=httpSecurity,indent=0]
-<1> - This states that both `FACTOR_PASSWORD` and `FACTOR_OTT` are needed to use any part of the application
-
-Spring Security behind the scenes knows which endpoint to go to depending on which authority is missing.
-If the user logged in initially with their username and password, then Spring Security redirects to the One-Time-Token Login page.
-If the user logged in initially with a token, then Spring Security redirects to the Username/Password Login page.
-
-[[authorization-manager-factory]]
-=== Requiring MFA For All Endpoints
-
-Specifying all authorities for each request pattern could be unwanted boilerplate:
-
-include-code::./ListAuthoritiesEverywhereConfiguration[tag=httpSecurity,indent=0]
-<1> - Since all authorities need to be specified for each endpoint, deploying MFA in this way can create unwanted boilerplate
-
-This can be remedied by publishing an `AuthorizationManagerFactory` bean like so:
-
-include-code::./UseAuthorizationManagerFactoryConfiguration[tag=authorizationManagerFactoryBean,indent=0]
-
-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
-
-You can also configure exception handling to direct Spring Security on how to obtain a missing scope.
-
-Consider an application that requires a specific OAuth 2.0 scope for a given endpoint:
-
-include-code::./ScopeConfiguration[tag=httpSecurity,indent=0]
-
-If this is also configured with an `AuthorizationManagerFactory` bean like this one:
-
-include-code::./MissingAuthorityConfiguration[tag=authorizationManagerFactoryBean,indent=0]
-
-Then the application will require an X.509 certificate as well as authorization from an OAuth 2.0 authorization server.
-
-In the event that the user does not consent to `profile:read`, this application as it stands will issue a 403.
-However, if you have a way for the application to re-ask for consent, then you can implement this in an `AuthenticationEntryPoint` like the following:
-
-include-code::./MissingAuthorityConfiguration[tag=authenticationEntryPoint,indent=0]
-
-Then, your filter chain declaration can bind this entry point to the given authority like so:
-
-include-code::./MissingAuthorityConfiguration[tag=httpSecurity,indent=0]
-
-[[custom-authorization-manager-factory]]
-== Programmatically Decide Which Authorities Are Required
-
-`AuthorizationManager` is the core interface for making authorization decisions.
-Consider an authorization manager that looks at the logged in user to decide which factors are necessary:
-
-include-code::./CustomAuthorizationManagerFactory[tag=authorizationManager,indent=0]
-
-In this case, using One-Time-Token is only required for those who have opted in.
-
-This can then be enforced by a custom `AuthorizationManagerFactory` implementation:
-
-include-code::./CustomAuthorizationManagerFactory[tag=authorizationManagerFactory,indent=0]

+ 213 - 0
docs/modules/ROOT/pages/servlet/authentication/mfa.adoc

@@ -0,0 +1,213 @@
+= Multi-Factor Authentication
+
+https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html[Multi-Factor Authentication (MFA)] requires that a user provide factors in order to authenticate.
+OWASP places factors into the following categories:
+
+- Something the user knows (e.g. a password)
+- Something that the user has (e.g. access to SMS or email)
+- Something you are (e.g. biometrics)
+- Somewhere you are (e.g. geolocation)
+- Something you do (e.g. Behavior Profiling)
+
+== `FactorGrantedAuthority`
+
+At the time of authentication, Spring Security's authentication mechanisms add a javadoc:org.springframework.security.core.authority.FactorGrantedAuthority[] using the constants found in javadoc:org.springframework.security.core.GrantedAuthorities[].
+For example, when a user authenticates using a password a `FactorGrantedAuthority` with the `authority` of `GrantedAuthorities.FACTOR_PASSWORD` is automatically added to the `Authentiation`.
+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
+
+javadoc:org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication[format=annotation] simplifies Global MFA (the entire application requires MFA).
+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]
+
+We are now able to concisely create a configuration that always requires multiple factors.
+
+include-code::./EnableGlobalMultiFactorAuthenticationConfiguration[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.
+
+Spring Security behind the scenes knows which endpoint to go to depending on which authority is missing.
+If the user logged in initially with their username and password, then Spring Security redirects to the One-Time-Token Login page.
+If the user logged in initially with a token, then Spring Security redirects to the Username/Password Login page.
+
+[[authorization-manager-factory]]
+== AuthorizationManagerFactory
+
+The `@EnableGlobalMultiFactorAuthentication` annotation 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 `AuthorizationManagerFactory` Bean below is what is published in the previously discussed xref:./mfa.adoc#using-egmfa[`@EnableGlobalMultiFactorAuthentication` 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`].
+However, there are times that an application only wants parts of the application to require MFA.
+Consider the following requirements:
+
+- URLs that begin with `/admin/**` should require the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`, `ROLE_ADMIN`.
+- URLs that begin with `/user/settings` should require the authorities `FACTOR_OTT`, `FACTOR_PASSWORD`
+- Every other URL requires an authenticated user
+
+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.
+
+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.
+<2> Explicitly use `AuthorizationManagerFactory` so that URLs that begin with `/admin/**` require `FACTOR_OTT`, `FACTOR_PASSWORD`, and `ROLE_ADMIN`.
+<3> Explicitly use `AuthorizationManagerFactory` so that URLs that begin with `/user/settings` require `FACTOR_OTT` and `FACTOR_PASSWORD`
+<4> Otherwise, the request must be authenticated.
+There is no MFA requirement, because the `AuthorizationManagerFactory` is not used.
+<5> Set up the authentication mechanisms that can provide the required factors.
+
+[[programmatic-mfa]]
+== Programmatic MFA
+
+In our previous examples, MFA is a static decision per request.
+There are times when we might want to require MFA for some users, but not others.
+Determining if MFA is enabled per user can be achieved by creating a custom `AuthorizationManager` that conditionally requires factors based upon the `Authentication`.
+
+include-code::./AdminMfaAuthorizationManagerConfiguration[tag=authorizationManager,indent=0]
+<1> MFA is required for the user with the username `admin`
+<2> Otherwise, MFA is not required
+
+To enable the MFA rules globally, we can publish an `AuthorizationManagerFactory` Bean.
+
+include-code::./AdminMfaAuthorizationManagerConfiguration[tag=authorizationManagerFactory,indent=0]
+<1> Inject the custom `AuthorizationManager` as the javadoc:org.springframework.security.authorization.DefaultAuthorizationManagerFactory#setAdditionalAuthorization(org.springframework.security.authorization.AuthorizationManager)[DefaultAuthorization.additionalAuthorization].
+This instructs `DefaultAuthorizationManagerFactory` that any authorization rule should apply our custom `AuthorizationManager` along with any authorization requirements defined by the application (e.g. `hasRole("ADMIN")).
+<2> Publish `DefaultAuthorizationManagerFactory` as a Bean, so it is used globally
+
+This should feel very similar to our previous example in xref:./mfa.adoc#authorization-manager-factory[].
+The difference is that in the previous example, the `Builder` is setting `DefaultAuthorization.additionalAuthorization` with a built in `AuthorizationManager` that always requires the same authorities.
+
+We can now define our authorization rules which are combined with `AdminMfaAuthorizationManager`.
+include-code::./AdminMfaAuthorizationManagerConfiguration[tag=httpSecurity,indent=0]
+<1> URLs that begin with `/admin/**` require `ROLE_ADMIN`.
+If the username is `admin`, then `FACTOR_OTT` and `FACTOR_PASSWORD` are also required.
+<2> Otherwise, the request must be authenticated.
+If the username is `admin`, then `FACTOR_OTT` and `FACTOR_PASSWORD` are also required.
+
+NOTE: MFA is enabled by username and not role because that is how we implemented `RequiredAuthoritiesAuthorizationManagerConfiguration`.
+If we preferred, we could change our logic to enable MFA based upon the roles rather than the username.
+
+[[raam-mfa]]
+== RequiredAuthoritiesAuthorizationManager
+
+We've demonstrated how we can dynamically determine the authorities for a particular user in xref:./mfa.adoc#programmatic-mfa[] using a custom `AuthorizationManager`.
+However, this is such a common scenario that Spring Security provides built in support using javadoc:org.springframework.security.authorization.RequiredAuthoritiesAuthorizationManager[] and javadoc:org.springframework.security.authorization.RequiredAuthoritiesRepository[].
+
+Let's implement the same requirement that we did in xref:./mfa.adoc#programmatic-mfa[] using the built-in support.
+
+We start by creating the `RequiredAuthoritiesAuthorizationManager` Bean to use.
+
+include-code::./RequiredAuthoritiesAuthorizationManagerConfiguration[tag=authorizationManager,indent=0]
+<1> Create a javadoc:org.springframework.security.authorization.MapRequiredAuthoritiesRepository[] that maps users with the username `admin` to require MFA.
+<2> Return a `RequiredAuthoritiesAuthorizationManager` that is injected with the `MapRequiredAuthoritiesRepository`.
+
+Next we can define an `AuthorizationManagerFactory` that uses the `RequiredAuthoritiesAuthorizationManager`.
+
+include-code::./RequiredAuthoritiesAuthorizationManagerConfiguration[tag=authorizationManagerFactory,indent=0]
+<1> Inject the `RequiredAuthoritiesAuthorizationManager` as the javadoc:org.springframework.security.authorization.DefaultAuthorizationManagerFactory#setAdditionalAuthorization(org.springframework.security.authorization.AuthorizationManager)[DefaultAuthorization.additionalAuthorization].
+This instructs `DefaultAuthorizationManagerFactory` that any authorization rule should apply `RequiredAuthoritiesAuthorizationManager` along with any authorization requirements defined by the application (e.g. `hasRole("ADMIN")).
+<2> Publish `DefaultAuthorizationManagerFactory` as a Bean, so it is used globally
+
+We can now define our authorization rules which are combined with `RequiredAuthoritiesAuthorizationManager`.
+include-code::./RequiredAuthoritiesAuthorizationManagerConfiguration[tag=httpSecurity,indent=0]
+<1> URLs that begin with `/admin/**` require `ROLE_ADMIN`.
+If the username is `admin`, then `FACTOR_OTT` and `FACTOR_PASSWORD` are also required.
+<2> Otherwise, the request must be authenticated.
+If the username is `admin`, then `FACTOR_OTT` and `FACTOR_PASSWORD` are also required.
+
+Our example uses an in memory mapping of usernames to the additional required authorities.
+For more dynamic use cases that can be determined by the username, a custom implementation of javadoc:org.springframework.security.authorization.RequiredAuthoritiesRepository[] can be created.
+Possible examples would be looking up if a user has enabled MFA in an explicit setting, determining if a user has registered a passkey, etc.
+
+For cases that need to determine MFA based upon the `Authentication`, a custom `AuthorizationManger` can be used as demonstrated in xref:./mfa.adoc#programmatic-mfa[]
+
+
+[[hasallauthorities]]
+== Using hasAllAuthorities
+
+We've shown a lot of additional infrastructure for supporting MFA.
+However, for simple MFA use-cases, using `hasAllAuthorities` to require multiple factors is effective.
+
+include-code::./ListAuthoritiesConfiguration[tag=httpSecurity,indent=0]
+<1> Require `FACTOR_PASSWORD` and `FACTOR_OTT` for every request
+<2> Set up the authentication mechanisms that can provide the required factors.
+
+The configuration above works well only for the most simple use-cases.
+If you have lots of endpoints, you probably do not want to repeat the requirements for MFA in every authorization rule.
+
+For example, consider the following configuration:
+
+include-code::./MultipleAuthorizationRulesConfiguration[tag=httpSecurity,indent=0]
+<1> For URLs that begin with `/admin/**`, the following authorities are required `FACTOR_OTT`, `FACTOR_PASSWORD`, `ROLE_ADMIN`.
+<2> For every other URL, the following authorities are required `FACTOR_OTT`, `FACTOR_PASSWORD`, `ROLE_USER`.
+<3> Set up the authentication mechanisms that can provide the required factors.
+
+The configuration only specifies two authorization rules, but it is enough to see that the duplication is not desirable.
+Can you imagine what it would be like to declare hundreds of rules like this?
+
+What's more that it becomes difficult to express more complicated authorization rules.
+For example, how would you require two factors and either `ROLE_ADMIN` or `ROLE_USER`?
+
+The answer to these questions, as we have already seen, is to use xref:./mfa.adoc#egmfa[]
+
+[[re-authentication]]
+== Re-authentication
+
+The most common of these is re-authentication.
+Imagine an application configured in the following way:
+
+include-code::./SimpleConfiguration[tag=httpSecurity,indent=0]
+
+By default, this application has two authentication mechanisms that it allows, meaning that the user could use either one and be fully-authenticated.
+
+If there is a set of endpoints that require a specific factor, we can specify that in `authorizeHttpRequests` as follows:
+
+include-code::./RequireOttConfiguration[tag=httpSecurity,indent=0]
+<1> - States that all `/profile/**` endpoints require one-time-token login to be authorized
+
+Given the above configuration, users can log in with any mechanism that you support.
+And, if they want to visit the profile page, then Spring Security will redirect them to the One-Time-Token Login page to obtain it.
+
+In this way, the authority given to a user is directly proportional to the amount of proof given.
+This adaptive approach allows users to give only the proof needed to perform their intended operations.
+
+
+[[obtaining-more-authorization]]
+== Authorizing More Scopes
+
+You can also configure exception handling to direct Spring Security on how to obtain a missing scope.
+
+Consider an application that requires a specific OAuth 2.0 scope for a given endpoint:
+
+include-code::./ScopeConfiguration[tag=httpSecurity,indent=0]
+
+If this is also configured with an `AuthorizationManagerFactory` bean like this one:
+
+include-code::./MissingAuthorityConfiguration[tag=authorizationManagerFactoryBean,indent=0]
+
+Then the application will require an X.509 certificate as well as authorization from an OAuth 2.0 authorization server.
+
+In the event that the user does not consent to `profile:read`, this application as it stands will issue a 403.
+However, if you have a way for the application to re-ask for consent, then you can implement this in an `AuthenticationEntryPoint` like the following:
+
+include-code::./MissingAuthorityConfiguration[tag=authenticationEntryPoint,indent=0]
+
+Then, your filter chain declaration can bind this entry point to the given authority like so:
+
+include-code::./MissingAuthorityConfiguration[tag=httpSecurity,indent=0]

+ 1 - 1
docs/modules/ROOT/pages/whats-new.adoc

@@ -15,7 +15,7 @@ Each section that follows will indicate the more notable removals as well as the
 
 == Core
 
-* Added Support for xref:servlet/authentication/adaptive.adoc[Multi-factor Authentication]
+* Added Support for xref:servlet/authentication/mfa.adoc[Multi-Factor Authentication]
 * Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`
 * Added javadoc:org.springframework.security.authorization.AllAuthoritiesAuthorizationManager[] and javadoc:org.springframework.security.authorization.AllAuthoritiesReactiveAuthorizationManager[] along with corresponding methods for xref:servlet/authorization/authorize-http-requests.adoc#authorize-requests[Authorizing `HttpServletRequests`] and xref:servlet/authorization/method-security.adoc#using-authorization-expression-fields-and-methods[method security expressions].
 * Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components

+ 5 - 1
docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java

@@ -39,7 +39,11 @@ class UseAuthorizationManagerFactoryConfiguration {
 	@Bean
 	AuthorizationManagerFactory<Object> authz() {
 		return DefaultAuthorizationManagerFactory.builder()
-				.requireAdditionalAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY).build();
+			.requireAdditionalAuthorities(
+				GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+				GrantedAuthorities.FACTOR_OTT_AUTHORITY
+			)
+			.build();
 	}
 	// end::authorizationManagerFactoryBean[]
 

+ 12 - 9
docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java

@@ -1,4 +1,4 @@
-package org.springframework.security.docs.servlet.authentication.enableglobalmfa;
+package org.springframework.security.docs.servlet.authentication.egmfa;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -18,8 +18,8 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
 @Configuration(proxyBeanMethods = false)
 // tag::enable-global-mfa[]
 @EnableGlobalMultiFactorAuthentication(authorities = {
-		GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
-		GrantedAuthorities.FACTOR_OTT_AUTHORITY })
+	GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+	GrantedAuthorities.FACTOR_OTT_AUTHORITY })
 // end::enable-global-mfa[]
 public class EnableGlobalMultiFactorAuthenticationConfiguration {
 
@@ -28,12 +28,15 @@ public class EnableGlobalMultiFactorAuthenticationConfiguration {
 	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 		// @formatter:off
 		http
-				.authorizeHttpRequests((authorize) -> authorize
-						.requestMatchers("/admin/**").hasRole("ADMIN")
-						.anyRequest().authenticated()
-				)
-				.formLogin(Customizer.withDefaults())
-				.oneTimeTokenLogin(Customizer.withDefaults());
+			.authorizeHttpRequests((authorize) -> authorize
+				// <1>
+				.requestMatchers("/admin/**").hasRole("ADMIN")
+				// <2>
+				.anyRequest().authenticated()
+			)
+			// <3>
+			.formLogin(Customizer.withDefaults())
+			.oneTimeTokenLogin(Customizer.withDefaults());
 		// @formatter:on
 		return http.build();
 	}

+ 2 - 2
docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationTests.java

@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.security.docs.servlet.authentication.enableglobalmfa;
+package org.springframework.security.docs.servlet.authentication.egmfa;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -52,7 +52,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
 	MockMvc mockMvc;
 
 	@Test
-	@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY })
+	@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_USER" })
 	void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
 		this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
 		// @formatter:off

+ 7 - 2
docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/ListAuthoritiesConfiguration.java

@@ -1,4 +1,4 @@
-package org.springframework.security.docs.servlet.authentication.multifactorauthentication;
+package org.springframework.security.docs.servlet.authentication.hasallauthorities;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -23,8 +23,13 @@ class ListAuthoritiesConfiguration {
 		// @formatter:off
 		http
 			.authorizeHttpRequests((authorize) -> authorize
-				.anyRequest().hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY) // <1>
+				// <1>
+				.anyRequest().hasAllAuthorities(
+					GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+					GrantedAuthorities.FACTOR_OTT_AUTHORITY
+				)
 			)
+			// <2>
 			.formLogin(Customizer.withDefaults())
 			.oneTimeTokenLogin(Customizer.withDefaults());
 		// @formatter:on

+ 1 - 1
docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultiFactorAuthenticationTests.java

@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.security.docs.servlet.authentication.multifactorauthentication;
+package org.springframework.security.docs.servlet.authentication.hasallauthorities;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;

+ 79 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfiguration.java

@@ -0,0 +1,79 @@
+/*
+ * 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.hasallauthorities;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+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.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)
+public class MultipleAuthorizationRulesConfiguration {
+
+	// tag::httpSecurity[]
+	@Bean
+	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+		// @formatter:off
+		http
+			.authorizeHttpRequests((authorize) -> authorize
+				// <1>
+				.requestMatchers("/admin/**").hasAllAuthorities(
+					"ROLE_ADMIN",
+					GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+					GrantedAuthorities.FACTOR_OTT_AUTHORITY
+				)
+				// <2>
+				.anyRequest().hasAllAuthorities(
+					"ROLE_USER",
+					GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+					GrantedAuthorities.FACTOR_OTT_AUTHORITY
+				)
+			)
+			// <3>
+			.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");
+	}
+}
+

+ 115 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfigurationTests.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.hasallauthorities;
+
+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 MultipleAuthorizationRulesConfigurationTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	MockMvc mockMvc;
+
+	@Test
+	@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_USER" })
+	void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
+		this.spring.register(MultipleAuthorizationRulesConfiguration.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(MultipleAuthorizationRulesConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/"))
+			.andExpect(status().is3xxRedirection())
+			.andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
+		// @formatter:on
+	}
+
+	@Test
+	@WithMockUser(authorities = GrantedAuthorities.FACTOR_OTT_AUTHORITY)
+	void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception {
+		this.spring.register(MultipleAuthorizationRulesConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/"))
+			.andExpect(status().is3xxRedirection())
+			.andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
+		// @formatter:on
+	}
+
+	@Test
+	@WithMockUser
+	void getWhenAuthenticatedThenRedirectsToPassword() throws Exception {
+		this.spring.register(MultipleAuthorizationRulesConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/"))
+			.andExpect(status().is3xxRedirection())
+			.andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
+		// @formatter:on
+	}
+
+	@Test
+	void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception {
+		this.spring.register(MultipleAuthorizationRulesConfiguration.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";
+		}
+	}
+}

+ 17 - 8
docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfiguration.java

@@ -1,4 +1,4 @@
-package org.springframework.security.docs.servlet.authentication.customauthorizationmanagerfactory;
+package org.springframework.security.docs.servlet.authentication.programmaticmfa;
 
 import java.util.function.Supplier;
 
@@ -6,7 +6,7 @@ import org.jspecify.annotations.Nullable;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.security.authorization.AuthorityAuthorizationManager;
+import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationManager;
 import org.springframework.security.authorization.AuthorizationManagerFactory;
@@ -27,7 +27,7 @@ import org.springframework.stereotype.Component;
 
 @EnableWebSecurity
 @Configuration(proxyBeanMethods = false)
-class CustomAuthorizationManagerFactory {
+class AdminMfaAuthorizationManagerConfiguration {
 	// tag::httpSecurity[]
 	@Bean
 	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
@@ -46,13 +46,19 @@ class CustomAuthorizationManagerFactory {
 
 	// tag::authorizationManager[]
 	@Component
-	class UserBasedOttAuthorizationManager implements AuthorizationManager<Object> {
+	class AdminMfaAuthorizationManager implements AuthorizationManager<Object> {
 		@Override
 		public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication, Object context) {
 			if ("admin".equals(authentication.get().getName())) {
-				return AuthorityAuthorizationManager.hasAuthority(GrantedAuthorities.FACTOR_OTT_AUTHORITY)
-						.authorize(authentication, context);
+				AuthorizationManager<Object> admins =
+					AllAuthoritiesAuthorizationManager.hasAllAuthorities(
+						GrantedAuthorities.FACTOR_OTT_AUTHORITY,
+						GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY
+					);
+				// <1>
+				return admins.authorize(authentication, context);
 			} else {
+				// <2>
 				return new AuthorizationDecision(true);
 			}
 		}
@@ -61,9 +67,12 @@ class CustomAuthorizationManagerFactory {
 
 	// tag::authorizationManagerFactory[]
 	@Bean
-	AuthorizationManagerFactory<Object> authorizationManagerFactory(UserBasedOttAuthorizationManager optIn) {
+	AuthorizationManagerFactory<Object> authorizationManagerFactory(
+			AdminMfaAuthorizationManager admins) {
 		DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
-		defaults.setAdditionalAuthorization(optIn);
+		// <1>
+		defaults.setAdditionalAuthorization(admins);
+		// <2>
 		return defaults;
 	}
 	// end::authorizationManagerFactory[]

+ 6 - 6
docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.java

@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.springframework.security.docs.servlet.authentication.customauthorizationmanagerfactory;
+package org.springframework.security.docs.servlet.authentication.programmaticmfa;
 
 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 CustomAuthorizationManagerFactoryTests {
+public class AdminMfaAuthorizationManagerConfigurationTests {
 
 	public final SpringTestContext spring = new SpringTestContext(this);
 
@@ -54,7 +54,7 @@ public class CustomAuthorizationManagerFactoryTests {
 	@Test
 	@WithMockUser(username = "admin")
 	void getWhenAdminThenRedirectsToOtt() throws Exception {
-		this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
+		this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
 		// @formatter:off
 		this.mockMvc.perform(get("/"))
 			.andExpect(status().is3xxRedirection())
@@ -65,7 +65,7 @@ public class CustomAuthorizationManagerFactoryTests {
 	@Test
 	@WithMockUser
 	void getWhenNotAdminThenAllows() throws Exception {
-		this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
+		this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
 		// @formatter:off
 		this.mockMvc.perform(get("/"))
 			.andExpect(status().isOk())
@@ -74,9 +74,9 @@ public class CustomAuthorizationManagerFactoryTests {
 	}
 
 	@Test
-	@WithMockUser(username = "admin", authorities = GrantedAuthorities.FACTOR_OTT_AUTHORITY)
+	@WithMockUser(username = "admin", authorities = { GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY })
 	void getWhenAdminAndHasFactorThenAllows() throws Exception {
-		this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
+		this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
 		// @formatter:off
 		this.mockMvc.perform(get("/"))
 			.andExpect(status().isOk())

+ 76 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfiguration.java

@@ -0,0 +1,76 @@
+package org.springframework.security.docs.servlet.authentication.raammfa;
+
+import java.util.List;
+
+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.authorization.MapRequiredAuthoritiesRepository;
+import org.springframework.security.authorization.RequiredAuthoritiesAuthorizationManager;
+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.GrantedAuthorities;
+import org.springframework.security.core.userdetails.PasswordEncodedUser;
+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 RequiredAuthoritiesAuthorizationManagerConfiguration {
+	// 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[]
+
+	// tag::authorizationManager[]
+	@Bean
+	RequiredAuthoritiesAuthorizationManager<Object> adminAuthorization() {
+		// <1>
+		MapRequiredAuthoritiesRepository authorities = new MapRequiredAuthoritiesRepository();
+		authorities.saveRequiredAuthorities("admin", List.of(
+			GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+			GrantedAuthorities.FACTOR_OTT_AUTHORITY)
+		);
+		// <2>
+		return new RequiredAuthoritiesAuthorizationManager<>(authorities);
+	}
+	// end::authorizationManager[]
+
+	// tag::authorizationManagerFactory[]
+	@Bean
+	AuthorizationManagerFactory<Object> authorizationManagerFactory(
+			RequiredAuthoritiesAuthorizationManager admins) {
+		DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
+		// <1>
+		defaults.setAdditionalAuthorization(admins);
+		// <2>
+		return defaults;
+	}
+	// end::authorizationManagerFactory[]
+
+	@Bean
+	public UserDetailsService users() {
+		return new InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin());
+	}
+
+	@Bean
+	OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
+		return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
+	}
+}

+ 94 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfigurationTests.java

@@ -0,0 +1,94 @@
+/*
+ * 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.programmaticmfa;
+
+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 RequiredAuthoritiesAuthorizationManagerConfigurationTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	MockMvc mockMvc;
+
+	@Test
+	@WithMockUser(username = "admin")
+	void getWhenAdminThenRedirectsToOtt() throws Exception {
+		this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/"))
+			.andExpect(status().is3xxRedirection())
+			.andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
+		// @formatter:on
+	}
+
+	@Test
+	@WithMockUser
+	void getWhenNotAdminThenAllows() throws Exception {
+		this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/"))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withUsername("user"));
+		// @formatter:on
+	}
+
+	@Test
+	@WithMockUser(username = "admin", authorities = { GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY })
+	void getWhenAdminAndHasFactorThenAllows() throws Exception {
+		this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/"))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withUsername("admin"));
+		// @formatter:on
+	}
+
+	@RestController
+	static class Http200Controller {
+		@GetMapping("/**")
+		String ok() {
+			return "ok";
+		}
+	}
+}

+ 20 - 5
docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/selectivemfa/SelectiveMfaConfiguration.java

@@ -1,7 +1,9 @@
-package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory;
+package org.springframework.security.docs.servlet.authentication.selectivemfa;
 
 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.Customizer;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -15,17 +17,30 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
 
 @EnableWebSecurity
 @Configuration(proxyBeanMethods = false)
-public class ListAuthoritiesEverywhereConfiguration {
+class SelectiveMfaConfiguration {
 
 	// tag::httpSecurity[]
 	@Bean
-	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 		// @formatter:off
+		// <1>
+		AuthorizationManagerFactory<Object> mfa =
+			DefaultAuthorizationManagerFactory.<Object>builder()
+				.requireAdditionalAuthorities(
+					GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+					GrantedAuthorities.FACTOR_OTT_AUTHORITY
+				)
+				.build();
 		http
 			.authorizeHttpRequests((authorize) -> authorize
-				.requestMatchers("/admin/**").hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN") // <1>
-				.anyRequest().hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY)
+				// <2>
+				.requestMatchers("/admin/**").access(mfa.hasRole("ADMIN"))
+				// <3>
+				.requestMatchers("/user/settings/**").access(mfa.authenticated())
+				// <4>
+				.anyRequest().authenticated()
 			)
+			// <5>
 			.formLogin(Customizer.withDefaults())
 			.oneTimeTokenLogin(Customizer.withDefaults());
 		// @formatter:on

+ 116 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authentication/selectivemfa/SelectiveMfaConfigurationTests.java

@@ -0,0 +1,116 @@
+/*
+ * 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.selectivemfa;
+
+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.redirectedUrlPattern;
+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 SelectiveMfaConfigurationTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	MockMvc mockMvc;
+
+	@Test
+	@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_ADMIN" })
+	void adminWhenMissingOttThenRequired() throws Exception {
+		this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/admin/"))
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrlPattern("http://localhost/login?*"));
+		// @formatter:on
+	}
+
+	@Test
+	@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN" })
+	void adminWhenMfaThenAllowed() throws Exception {
+		this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/admin/"))
+			.andExpect(status().isOk())
+			.andExpect(authenticated().withUsername("user"));
+		// @formatter:on
+	}
+
+	@Test
+	@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_ADMIN" })
+	void userSettingsRequiresMfa() throws Exception {
+		this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/admin/"))
+			.andExpect(status().is3xxRedirection())
+			.andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
+		// @formatter:on
+	}
+
+	@Test
+	@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_USER" })
+	void userSettingsWhenMissingOttThenRequired() throws Exception {
+		this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/user/settings/"))
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrlPattern("http://localhost/login?*"));
+		// @formatter:on
+	}
+
+	@Test
+	@WithMockUser(roles = "USER")
+	void rootDoesNotRequireMfa() throws Exception {
+		this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
+		// @formatter:off
+		this.mockMvc.perform(get("/"))
+				.andExpect(status().isOk());
+		// @formatter:on
+	}
+
+	@RestController
+	static class Http200Controller {
+		@GetMapping("/**")
+		String ok() {
+			return "ok";
+		}
+	}
+}

+ 5 - 1
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.kt

@@ -39,7 +39,11 @@ internal class UseAuthorizationManagerFactoryConfiguration {
     @Bean
     fun authz(): AuthorizationManagerFactory<Object> {
         return DefaultAuthorizationManagerFactory.builder<Object>()
-            .requireAdditionalAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY).build()
+            .requireAdditionalAuthorities(
+                GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+                GrantedAuthorities.FACTOR_OTT_AUTHORITY
+            )
+            .build()
     }
     // end::authorizationManagerFactoryBean[]
 

+ 14 - 6
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/ListAuthoritiesEverywhereConfiguration.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfiguration.kt

@@ -1,7 +1,8 @@
-package org.springframework.security.kt.docs.servlet.authentication.enableglobalmfa
+package org.springframework.security.kt.docs.servlet.authentication.egmfa
 
 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.web.builders.HttpSecurity
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
 import org.springframework.security.config.annotation.web.invoke
@@ -15,7 +16,13 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
 
 @EnableWebSecurity
 @Configuration(proxyBeanMethods = false)
-class ListAuthoritiesEverywhereConfiguration {
+
+// tag::enable-global-mfa[]
+@EnableGlobalMultiFactorAuthentication( authorities = [
+    GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+    GrantedAuthorities.FACTOR_OTT_AUTHORITY])
+// end::enable-global-mfa[]
+internal class EnableGlobalMultiFactorAuthenticationConfiguration {
 
     // tag::httpSecurity[]
     @Bean
@@ -23,9 +30,12 @@ class ListAuthoritiesEverywhereConfiguration {
         // @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))
+                // <1>
+                authorize("/admin/**", hasRole("ADMIN"))
+                // <2>
+                authorize(anyRequest, authenticated)
             }
+            // <3>
             formLogin { }
             oneTimeTokenLogin {  }
         }
@@ -34,8 +44,6 @@ class ListAuthoritiesEverywhereConfiguration {
     }
     // end::httpSecurity[]
 
-
-    // end::httpSecurity[]
     @Bean
     fun userDetailsService(): UserDetailsService {
         return InMemoryUserDetailsManager(

+ 8 - 13
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfigurationTests.kt

@@ -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.enableglobalmfa
+package org.springframework.security.kt.docs.servlet.authentication.egmfa
 
 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 AuthorizationManagerFactoryTests {
+class EnableGlobalMultiFactorAuthenticationConfigurationTests {
     @JvmField
     val spring: SpringTestContext = SpringTestContext(this)
 
@@ -47,11 +47,10 @@ class AuthorizationManagerFactoryTests {
     var mockMvc: MockMvc? = null
 
     @Test
-    @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY])
+    @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN"])
     @Throws(Exception::class)
     fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
-        this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
-            .autowire()
+        this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
         // @formatter:off
         this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
         .andExpect(MockMvcResultMatchers.status().isOk())
@@ -63,8 +62,7 @@ class AuthorizationManagerFactoryTests {
     @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY])
     @Throws(Exception::class)
     fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() {
-        this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
-            .autowire()
+        this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
         // @formatter:off
         this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
         .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
@@ -76,8 +74,7 @@ class AuthorizationManagerFactoryTests {
     @WithMockUser(authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY])
     @Throws(Exception::class)
     fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
-        this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
-            .autowire()
+        this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
         // @formatter:off
         this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
         .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
@@ -89,8 +86,7 @@ class AuthorizationManagerFactoryTests {
     @WithMockUser
     @Throws(Exception::class)
     fun getWhenAuthenticatedThenRedirectsToPassword() {
-        this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
-            .autowire()
+        this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
         // @formatter:off
         this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
         .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
@@ -101,8 +97,7 @@ class AuthorizationManagerFactoryTests {
     @Test
     @Throws(Exception::class)
     fun getWhenUnauthenticatedThenRedirectsToBoth() {
-        this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
-            .autowire()
+        this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
         // @formatter:off
         this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
         .andExpect(MockMvcResultMatchers.status().is3xxRedirection())

+ 7 - 2
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/ListAuthoritiesConfiguration.kt

@@ -1,4 +1,4 @@
-package org.springframework.security.kt.docs.servlet.authentication.multifactorauthentication
+package org.springframework.security.kt.docs.servlet.authentication.hasallauthorities
 
 import org.springframework.context.annotation.Bean
 import org.springframework.context.annotation.Configuration
@@ -23,8 +23,13 @@ internal class ListAuthoritiesConfiguration {
         // @formatter:off
         http {
             authorizeHttpRequests {
-                authorize(anyRequest, hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY))
+                // <1>
+                authorize(anyRequest, hasAllAuthorities(
+                    GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+                    GrantedAuthorities.FACTOR_OTT_AUTHORITY
+                ))
             }
+            // <2>
             formLogin { }
             oneTimeTokenLogin {  }
         }

+ 1 - 1
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultiFactorAuthenticationTests.kt

@@ -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.multifactorauthentication
+package org.springframework.security.kt.docs.servlet.authentication.hasallauthorities
 
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith

+ 15 - 4
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfiguration.kt

@@ -1,4 +1,4 @@
-package org.springframework.security.kt.docs.servlet.authentication.authorizationmanagerfactory
+package org.springframework.security.kt.docs.servlet.authentication.hasallauthorities
 
 import org.springframework.context.annotation.Bean
 import org.springframework.context.annotation.Configuration
@@ -15,7 +15,7 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
 
 @EnableWebSecurity
 @Configuration(proxyBeanMethods = false)
-class ListAuthoritiesEverywhereConfiguration {
+internal class MultipleAuthorizationRulesConfiguration {
 
     // tag::httpSecurity[]
     @Bean
@@ -23,9 +23,20 @@ class ListAuthoritiesEverywhereConfiguration {
         // @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))
+                // <1>
+                authorize("/admin/**", hasAllAuthorities(
+                    "ROLE_ADMIN",
+                    GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+                    GrantedAuthorities.FACTOR_OTT_AUTHORITY
+                ))
+                // <2>
+                authorize(anyRequest, hasAllAuthorities(
+                    "ROLE_USER",
+                    GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+                    GrantedAuthorities.FACTOR_OTT_AUTHORITY
+                ))
             }
+            // <3>
             formLogin { }
             oneTimeTokenLogin {  }
         }

+ 115 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfigurationTests.kt

@@ -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.kt.docs.servlet.authentication.hasallauthorities
+
+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 MultipleAuthorizationRulesConfigurationTests {
+    @JvmField
+    val spring: SpringTestContext = SpringTestContext(this)
+
+    @Autowired
+    var mockMvc: MockMvc? = null
+
+    @Test
+    @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_USER"])
+    @Throws(Exception::class)
+    fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
+        this.spring.register(MultipleAuthorizationRulesConfiguration::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(MultipleAuthorizationRulesConfiguration::class.java, Http200Controller::class.java).autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+        .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+        .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"))
+    		// @formatter:on
+    }
+
+    @Test
+    @WithMockUser(authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY])
+    @Throws(Exception::class)
+    fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
+        this.spring.register(MultipleAuthorizationRulesConfiguration::class.java, Http200Controller::class.java).autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+        .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+        .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
+    		// @formatter:on
+    }
+
+    @Test
+    @WithMockUser
+    @Throws(Exception::class)
+    fun getWhenAuthenticatedThenRedirectsToPassword() {
+        this.spring.register(MultipleAuthorizationRulesConfiguration::class.java, Http200Controller::class.java).autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+        .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+        .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
+    		// @formatter:on
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun getWhenUnauthenticatedThenRedirectsToBoth() {
+        this.spring.register(MultipleAuthorizationRulesConfiguration::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"
+        }
+    }
+}

+ 15 - 7
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfiguration.kt

@@ -1,4 +1,4 @@
-package org.springframework.security.kt.docs.servlet.authentication.customauthorizationmanagerfactory
+package org.springframework.security.kt.docs.servlet.authentication.programmaticmfa
 
 import org.springframework.context.annotation.Bean
 import org.springframework.context.annotation.Configuration
@@ -19,7 +19,7 @@ import java.util.function.Supplier
 
 @EnableWebSecurity
 @Configuration(proxyBeanMethods = false)
-internal class CustomAuthorizationManagerFactory {
+internal class AdminMfaAuthorizationManagerConfiguration {
 
     // tag::httpSecurity[]
     @Bean
@@ -40,13 +40,19 @@ internal class CustomAuthorizationManagerFactory {
 
     // tag::authorizationManager[]
     @Component
-    internal open class UserBasedOttAuthorizationManager : AuthorizationManager<Object> {
+    internal open class AdminMfaAuthorizationManager : AuthorizationManager<Object> {
         override fun authorize(
             authentication: Supplier<out Authentication?>, context: Object): AuthorizationResult {
             return if ("admin" == authentication.get().name) {
-                AuthorityAuthorizationManager.hasAuthority<Object>(GrantedAuthorities.FACTOR_OTT_AUTHORITY)
-                    .authorize(authentication, context)
+                var admins =
+                    AllAuthoritiesAuthorizationManager.hasAllAuthorities<Any>(
+                        GrantedAuthorities.FACTOR_OTT_AUTHORITY,
+                        GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY
+                    )
+                // <1>
+                admins.authorize(authentication, context)
             } else {
+                // <2>
                 AuthorizationDecision(true)
             }
         }
@@ -55,9 +61,11 @@ internal class CustomAuthorizationManagerFactory {
 
     // tag::authorizationManagerFactory[]
     @Bean
-    fun authorizationManagerFactory(optIn: UserBasedOttAuthorizationManager?): AuthorizationManagerFactory<Object> {
+    fun authorizationManagerFactory(admins: AdminMfaAuthorizationManager): AuthorizationManagerFactory<Object> {
         val defaults = DefaultAuthorizationManagerFactory<Object>()
-        defaults.setAdditionalAuthorization(optIn)
+        // <1>
+        defaults.setAdditionalAuthorization(admins)
+        // <2>
         return defaults
     }
     // end::authorizationManagerFactory[]

+ 6 - 6
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.kt

@@ -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.customauthorizationmanagerfactory
+package org.springframework.security.kt.docs.servlet.authentication.programmaticmfa
 
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
@@ -40,7 +40,7 @@ import org.springframework.web.bind.annotation.RestController
  */
 @ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
 @TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
-class CustomAuthorizationManagerFactoryTests {
+class AdminMfaAuthorizationManagerConfigurationTests {
     @JvmField
     val spring: SpringTestContext = SpringTestContext(this)
 
@@ -51,7 +51,7 @@ class CustomAuthorizationManagerFactoryTests {
     @Throws(Exception::class)
     @WithMockUser(username = "admin")
     fun getWhenAdminThenRedirectsToOtt() {
-        this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
+        this.spring.register(AdminMfaAuthorizationManagerConfiguration::class.java, Http200Controller::class.java).autowire()
         // @formatter:off
         this.mockMvc!!.perform(get("/"))
             .andExpect(status().is3xxRedirection())
@@ -63,7 +63,7 @@ class CustomAuthorizationManagerFactoryTests {
     @Throws(Exception::class)
     @WithMockUser
     fun getWhenNotAdminThenAllows() {
-        this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
+        this.spring.register(AdminMfaAuthorizationManagerConfiguration::class.java, Http200Controller::class.java).autowire()
         // @formatter:off
         this.mockMvc!!.perform(get("/"))
             .andExpect(status().isOk())
@@ -73,9 +73,9 @@ class CustomAuthorizationManagerFactoryTests {
 
     @Test
     @Throws(Exception::class)
-    @WithMockUser(username = "admin", authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY])
+    @WithMockUser(username = "admin", authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY ])
     fun getWhenAdminAndHasFactorThenAllows() {
-        this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
+        this.spring.register(AdminMfaAuthorizationManagerConfiguration::class.java, Http200Controller::class.java).autowire()
         // @formatter:off
         this.mockMvc!!.perform(get("/"))
             .andExpect(status().isOk())

+ 76 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfiguration.kt

@@ -0,0 +1,76 @@
+package org.springframework.security.kt.docs.servlet.authentication.raammfa
+
+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.authorization.MapRequiredAuthoritiesRepository
+import org.springframework.security.authorization.RequiredAuthoritiesAuthorizationManager
+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.PasswordEncodedUser
+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
+import java.util.List
+
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+internal class RequiredAuthoritiesAuthorizationManagerConfiguration {
+    // 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::authorizationManager[]
+    @Bean
+    fun adminAuthorization(): RequiredAuthoritiesAuthorizationManager<Object> {
+        // <1>
+        val authorities = MapRequiredAuthoritiesRepository()
+        authorities.saveRequiredAuthorities("admin", List.of(
+            GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+            GrantedAuthorities.FACTOR_OTT_AUTHORITY)
+        )
+        // <2>
+        return RequiredAuthoritiesAuthorizationManager(authorities)
+    }
+    // end::authorizationManager[]
+
+
+    // tag::authorizationManagerFactory[]
+    @Bean
+    fun authorizationManagerFactory(admins: RequiredAuthoritiesAuthorizationManager<Object>): AuthorizationManagerFactory<Object> {
+        val defaults = DefaultAuthorizationManagerFactory<Object>()
+        // <1>
+        defaults.setAdditionalAuthorization(admins)
+        // <2>
+        return defaults
+    }
+    // end::authorizationManagerFactory[]
+
+    @Bean
+    fun users(): UserDetailsService {
+        return InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin())
+    }
+
+    @Bean
+    fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
+        return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
+    }
+}

+ 100 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfigurationTests.kt

@@ -0,0 +1,100 @@
+/*
+ * 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.raammfa
+
+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 RequiredAuthoritiesAuthorizationManagerConfigurationTests {
+
+    @JvmField
+    val spring: SpringTestContext = SpringTestContext(this)
+
+    @Autowired
+    var mockMvc: MockMvc? = null
+
+    @Test
+    @WithMockUser(username = "admin")
+    @Throws(Exception::class)
+    fun getWhenAdminThenRedirectsToOtt() {
+        this.spring.register(RequiredAuthoritiesAuthorizationManagerConfiguration::class.java, Http200Controller::class.java)
+            .autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+        .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+    		// @formatter:on
+    }
+
+    @Test
+    @WithMockUser
+    @Throws(Exception::class)
+    fun getWhenNotAdminThenAllows() {
+        this.spring.register(RequiredAuthoritiesAuthorizationManagerConfiguration::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(
+        username = "admin",
+        authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY]
+    )
+    @Throws(
+        Exception::class
+    )
+    fun getWhenAdminAndHasFactorThenAllows() {
+        this.spring.register(RequiredAuthoritiesAuthorizationManagerConfiguration::class.java, Http200Controller::class.java)
+            .autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+        .andExpect(MockMvcResultMatchers.status().isOk())
+        .andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("admin"))
+    		// @formatter:on
+    }
+
+    @RestController
+    internal class Http200Controller {
+        @GetMapping("/**")
+        fun ok(): String {
+            return "ok"
+        }
+    }
+}

+ 19 - 13
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/UseAuthorizationManagerFactoryConfiguration.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/selectivemfa/SelectiveMfaConfiguration.kt

@@ -1,4 +1,4 @@
-package org.springframework.security.kt.docs.servlet.authentication.enableglobalmfa
+package org.springframework.security.kt.docs.servlet.authentication.selectivemfa
 
 import org.springframework.context.annotation.Bean
 import org.springframework.context.annotation.Configuration
@@ -17,32 +17,38 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
 
 @EnableWebSecurity
 @Configuration(proxyBeanMethods = false)
-internal class UseAuthorizationManagerFactoryConfiguration {
+internal class SelectiveMfaConfiguration {
     // tag::httpSecurity[]
     @Bean
+    @Throws(Exception::class)
     fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
         // @formatter:off
+        // <1>
+        val mfa: AuthorizationManagerFactory<Any> =
+            DefaultAuthorizationManagerFactory.builder<Any>()
+                .requireAdditionalAuthorities(
+        GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
+                    GrantedAuthorities.FACTOR_OTT_AUTHORITY
+                )
+                .build()
         http {
             authorizeHttpRequests {
-                authorize("/admin/**", hasRole("ADMIN"))
+                // <2>
+                authorize("/admin/**", mfa.hasRole("ADMIN"))
+                // <3>
+                authorize("/user/settings/**", mfa.authenticated())
+                // <4>
                 authorize(anyRequest, authenticated)
             }
+            // <5>
             formLogin { }
-            oneTimeTokenLogin { }
+            oneTimeTokenLogin {  }
         }
         // @formatter:on
         return http.build()
     }
-    // end::httpSecurity[]
-
-    // tag::authorizationManagerFactoryBean[]
-    @Bean
-    fun authz(): AuthorizationManagerFactory<Object> {
-        return DefaultAuthorizationManagerFactory.builder<Object>()
-            .requireAdditionalAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY).build()
-    }
-    // end::authorizationManagerFactoryBean[]
 
+    // end::httpSecurity[]
     @Bean
     fun userDetailsService(): UserDetailsService {
         return InMemoryUserDetailsManager(

+ 127 - 0
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/selectivemfa/SelectiveMfaConfigurationTests.kt

@@ -0,0 +1,127 @@
+/*
+ * 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.selectivemfa
+
+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 SelectiveMfaConfigurationTests {
+    @JvmField
+    val spring: SpringTestContext = SpringTestContext(this)
+
+    @Autowired
+    var mockMvc: MockMvc? = null
+
+    @Test
+    @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_ADMIN"])
+    @Throws(Exception::class)
+    fun adminWhenMissingOttThenRequired() {
+        this.spring.register(
+            SelectiveMfaConfiguration::class.java, Http200Controller::class.java
+        ).autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/admin/"))
+        .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+        .andExpect(MockMvcResultMatchers.redirectedUrlPattern("http://localhost/login?*"))
+    		// @formatter:on
+    }
+
+    @Test
+    @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN"])
+    @Throws(
+        Exception::class
+    )
+    fun adminWhenMfaThenAllowed() {
+        this.spring.register(
+            SelectiveMfaConfiguration::class.java, Http200Controller::class.java
+        ).autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/admin/"))
+        .andExpect(MockMvcResultMatchers.status().isOk())
+        .andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
+    		// @formatter:on
+    }
+
+    @Test
+    @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_ADMIN"])
+    @Throws(Exception::class)
+    fun userSettingsRequiresMfa() {
+        this.spring.register(
+            SelectiveMfaConfiguration::class.java, Http200Controller::class.java
+        ).autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/admin/"))
+        .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+        .andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"))
+    		// @formatter:on
+    }
+
+    @Test
+    @WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_USER"])
+    @Throws(Exception::class)
+    fun userSettingsWhenMissingOttThenRequired() {
+        this.spring.register(
+            SelectiveMfaConfiguration::class.java, Http200Controller::class.java
+        ).autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/user/settings/"))
+        .andExpect(MockMvcResultMatchers.status().is3xxRedirection())
+        .andExpect(MockMvcResultMatchers.redirectedUrlPattern("http://localhost/login?*"))
+    		// @formatter:on
+    }
+
+    @Test
+    @WithMockUser(roles = ["USER"])
+    @Throws(Exception::class)
+    fun rootDoesNotRequireMfa() {
+        this.spring.register(
+            SelectiveMfaConfiguration::class.java, Http200Controller::class.java
+        ).autowire()
+        // @formatter:off
+        this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
+        .andExpect(MockMvcResultMatchers.status().isOk())
+    		// @formatter:on
+    }
+
+    @RestController
+    internal class Http200Controller {
+        @GetMapping("/**")
+        fun ok(): String {
+            return "ok"
+        }
+    }
+}