diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 6fc89dfcf3..186b024a23 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/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] diff --git a/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc b/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc deleted file mode 100644 index 22446f4776..0000000000 --- a/docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc +++ /dev/null @@ -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] diff --git a/docs/modules/ROOT/pages/servlet/authentication/mfa.adoc b/docs/modules/ROOT/pages/servlet/authentication/mfa.adoc new file mode 100644 index 0000000000..4807e3feba --- /dev/null +++ b/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] diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index cf1b34c442..520c6e4ae8 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/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 diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java index 23d2b30f12..dc4cbfcab8 100644 --- a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.java @@ -39,7 +39,11 @@ class UseAuthorizationManagerFactoryConfiguration { @Bean AuthorizationManagerFactory 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[] diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java similarity index 83% rename from docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java rename to docs/src/test/java/org/springframework/security/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java index eb569078dc..4a24b03be5 100644 --- a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationConfiguration.java +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/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(); } diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationTests.java similarity index 98% rename from docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java rename to docs/src/test/java/org/springframework/security/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationTests.java index 34f0a81fca..e17c8b096a 100644 --- a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/enableglobalmfa/EnableGlobalMultiFactorAuthenticationTests.java +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/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 diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/ListAuthoritiesConfiguration.java similarity index 90% rename from docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.java rename to docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/ListAuthoritiesConfiguration.java index 80322a4c82..3f6e70d52f 100644 --- a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.java +++ b/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 diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultiFactorAuthenticationTests.java similarity index 99% rename from docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java rename to docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultiFactorAuthenticationTests.java index 8ba5b7cf3e..02e4905c02 100644 --- a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.java +++ b/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; diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfiguration.java new file mode 100644 index 0000000000..f480652100 --- /dev/null +++ b/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"); + } +} + diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfigurationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfigurationTests.java new file mode 100644 index 0000000000..a1238c276e --- /dev/null +++ b/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"; + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfiguration.java similarity index 82% rename from docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.java rename to docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfiguration.java index b23a41dd9e..e2417f36bd 100644 --- a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.java +++ b/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 { + class AdminMfaAuthorizationManager implements AuthorizationManager { @Override public AuthorizationResult authorize(Supplier authentication, Object context) { if ("admin".equals(authentication.get().getName())) { - return AuthorityAuthorizationManager.hasAuthority(GrantedAuthorities.FACTOR_OTT_AUTHORITY) - .authorize(authentication, context); + AuthorizationManager 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 authorizationManagerFactory(UserBasedOttAuthorizationManager optIn) { + AuthorizationManagerFactory authorizationManagerFactory( + AdminMfaAuthorizationManager admins) { DefaultAuthorizationManagerFactory defaults = new DefaultAuthorizationManagerFactory<>(); - defaults.setAdditionalAuthorization(optIn); + // <1> + defaults.setAdditionalAuthorization(admins); + // <2> return defaults; } // end::authorizationManagerFactory[] diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.java similarity index 85% rename from docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java rename to docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.java index 872ad9e69d..a8b958b9d5 100644 --- a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java +++ b/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()) diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfiguration.java new file mode 100644 index 0000000000..8a77f82015 --- /dev/null +++ b/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 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 authorizationManagerFactory( + RequiredAuthoritiesAuthorizationManager admins) { + DefaultAuthorizationManagerFactory 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"); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfigurationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfigurationTests.java new file mode 100644 index 0000000000..4846a63560 --- /dev/null +++ b/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"; + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/selectivemfa/SelectiveMfaConfiguration.java similarity index 68% rename from docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.java rename to docs/src/test/java/org/springframework/security/docs/servlet/authentication/selectivemfa/SelectiveMfaConfiguration.java index 42fc88dbba..ead92fe462 100644 --- a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.java +++ b/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 mfa = + DefaultAuthorizationManagerFactory.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 diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/selectivemfa/SelectiveMfaConfigurationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/selectivemfa/SelectiveMfaConfigurationTests.java new file mode 100644 index 0000000000..3fd0c1106d --- /dev/null +++ b/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"; + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.kt index 0cce4c130b..cf4058c4aa 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/UseAuthorizationManagerFactoryConfiguration.kt +++ b/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 { 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[] diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/ListAuthoritiesEverywhereConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfiguration.kt similarity index 76% rename from docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/ListAuthoritiesEverywhereConfiguration.kt rename to docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfiguration.kt index 7c00ede58d..0f3ad02465 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/ListAuthoritiesEverywhereConfiguration.kt +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/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( diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfigurationTests.kt similarity index 84% rename from docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt rename to docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/egmfa/EnableGlobalMultiFactorAuthenticationConfigurationTests.kt index c2303f1f3e..e765aa1a34 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/AuthorizationManagerFactoryTests.kt +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/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()) diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/ListAuthoritiesConfiguration.kt similarity index 87% rename from docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.kt rename to docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/ListAuthoritiesConfiguration.kt index 1056c3a230..27a4d17d87 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/ListAuthoritiesConfiguration.kt +++ b/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 { } } diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultiFactorAuthenticationTests.kt similarity index 99% rename from docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt rename to docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultiFactorAuthenticationTests.kt index 3de64b1064..d6d479c3f6 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/multifactorauthentication/MultiFactorAuthenticationTests.kt +++ b/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 diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfiguration.kt similarity index 74% rename from docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.kt rename to docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfiguration.kt index cd61b1df7b..60e8a9651f 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/authorizationmanagerfactory/ListAuthoritiesEverywhereConfiguration.kt +++ b/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 { } } diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfigurationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/hasallauthorities/MultipleAuthorizationRulesConfigurationTests.kt new file mode 100644 index 0000000000..fcdadd5b61 --- /dev/null +++ b/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" + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfiguration.kt similarity index 77% rename from docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.kt rename to docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfiguration.kt index efea2598b4..45634fee12 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactory.kt +++ b/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 { + internal open class AdminMfaAuthorizationManager : AuthorizationManager { override fun authorize( authentication: Supplier, context: Object): AuthorizationResult { return if ("admin" == authentication.get().name) { - AuthorityAuthorizationManager.hasAuthority(GrantedAuthorities.FACTOR_OTT_AUTHORITY) - .authorize(authentication, context) + var admins = + AllAuthoritiesAuthorizationManager.hasAllAuthorities( + 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 { + fun authorizationManagerFactory(admins: AdminMfaAuthorizationManager): AuthorizationManagerFactory { val defaults = DefaultAuthorizationManagerFactory() - defaults.setAdditionalAuthorization(optIn) + // <1> + defaults.setAdditionalAuthorization(admins) + // <2> return defaults } // end::authorizationManagerFactory[] diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.kt similarity index 86% rename from docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt rename to docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.kt index b182cbd007..650c66fde1 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt +++ b/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()) diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfiguration.kt new file mode 100644 index 0000000000..52e0ea23f3 --- /dev/null +++ b/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 { + // <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): AuthorizationManagerFactory { + val defaults = DefaultAuthorizationManagerFactory() + // <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") + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfigurationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/raammfa/RequiredAuthoritiesAuthorizationManagerConfigurationTests.kt new file mode 100644 index 0000000000..404b0805b7 --- /dev/null +++ b/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" + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/UseAuthorizationManagerFactoryConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/selectivemfa/SelectiveMfaConfiguration.kt similarity index 74% rename from docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/UseAuthorizationManagerFactoryConfiguration.kt rename to docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/selectivemfa/SelectiveMfaConfiguration.kt index f037dd8023..d95ca5f37a 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/enableglobalmfa/UseAuthorizationManagerFactoryConfiguration.kt +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/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 = + DefaultAuthorizationManagerFactory.builder() + .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 { - return DefaultAuthorizationManagerFactory.builder() - .requireAdditionalAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY).build() - } - // end::authorizationManagerFactoryBean[] + // end::httpSecurity[] @Bean fun userDetailsService(): UserDetailsService { return InMemoryUserDetailsManager( diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/selectivemfa/SelectiveMfaConfigurationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/selectivemfa/SelectiveMfaConfigurationTests.kt new file mode 100644 index 0000000000..60d6e9f6dd --- /dev/null +++ b/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" + } + } +}