Browse Source
This reworks the Multi-Factor documentation to start with the simplest scenario and work to progressively more complex requirements. Closes gh-18029pull/18035/head
30 changed files with 1273 additions and 198 deletions
@ -1,109 +0,0 @@
@@ -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] |
||||
@ -0,0 +1,213 @@
@@ -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] |
||||
@ -0,0 +1,79 @@
@@ -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"); |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,115 @@
@@ -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"; |
||||
} |
||||
} |
||||
} |
||||
12
docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.java
12
docs/src/test/java/org/springframework/security/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.java → docs/src/test/java/org/springframework/security/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.java
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
package org.springframework.security.docs.servlet.authentication.raammfa; |
||||
|
||||
import java.util.List; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.security.authorization.AuthorizationManagerFactory; |
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory; |
||||
import org.springframework.security.authorization.MapRequiredAuthoritiesRepository; |
||||
import org.springframework.security.authorization.RequiredAuthoritiesAuthorizationManager; |
||||
import org.springframework.security.config.Customizer; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||
import org.springframework.security.core.GrantedAuthorities; |
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser; |
||||
import org.springframework.security.core.userdetails.UserDetailsService; |
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager; |
||||
import org.springframework.security.web.SecurityFilterChain; |
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; |
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; |
||||
|
||||
@EnableWebSecurity |
||||
@Configuration(proxyBeanMethods = false) |
||||
class RequiredAuthoritiesAuthorizationManagerConfiguration { |
||||
// tag::httpSecurity[]
|
||||
@Bean |
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
||||
// @formatter:off
|
||||
http |
||||
.authorizeHttpRequests((authorize) -> authorize |
||||
.requestMatchers("/admin/**").hasRole("ADMIN") |
||||
.anyRequest().authenticated() |
||||
) |
||||
.formLogin(Customizer.withDefaults()) |
||||
.oneTimeTokenLogin(Customizer.withDefaults()); |
||||
// @formatter:on
|
||||
return http.build(); |
||||
} |
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManager[]
|
||||
@Bean |
||||
RequiredAuthoritiesAuthorizationManager<Object> adminAuthorization() { |
||||
// <1>
|
||||
MapRequiredAuthoritiesRepository authorities = new MapRequiredAuthoritiesRepository(); |
||||
authorities.saveRequiredAuthorities("admin", List.of( |
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, |
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY) |
||||
); |
||||
// <2>
|
||||
return new RequiredAuthoritiesAuthorizationManager<>(authorities); |
||||
} |
||||
// end::authorizationManager[]
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
@Bean |
||||
AuthorizationManagerFactory<Object> authorizationManagerFactory( |
||||
RequiredAuthoritiesAuthorizationManager admins) { |
||||
DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>(); |
||||
// <1>
|
||||
defaults.setAdditionalAuthorization(admins); |
||||
// <2>
|
||||
return defaults; |
||||
} |
||||
// end::authorizationManagerFactory[]
|
||||
|
||||
@Bean |
||||
public UserDetailsService users() { |
||||
return new InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin()); |
||||
} |
||||
|
||||
@Bean |
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() { |
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); |
||||
} |
||||
} |
||||
@ -0,0 +1,94 @@
@@ -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"; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,116 @@
@@ -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"; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,115 @@
@@ -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" |
||||
} |
||||
} |
||||
} |
||||
12
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.kt
12
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/customauthorizationmanagerfactory/CustomAuthorizationManagerFactoryTests.kt → docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/programmaticmfa/AdminMfaAuthorizationManagerConfigurationTests.kt
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.raammfa |
||||
|
||||
import org.springframework.context.annotation.Bean |
||||
import org.springframework.context.annotation.Configuration |
||||
import org.springframework.security.authorization.AuthorizationManagerFactory |
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory |
||||
import org.springframework.security.authorization.MapRequiredAuthoritiesRepository |
||||
import org.springframework.security.authorization.RequiredAuthoritiesAuthorizationManager |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||
import org.springframework.security.config.annotation.web.invoke |
||||
import org.springframework.security.core.GrantedAuthorities |
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser |
||||
import org.springframework.security.core.userdetails.UserDetailsService |
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager |
||||
import org.springframework.security.web.SecurityFilterChain |
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler |
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler |
||||
import java.util.List |
||||
|
||||
@EnableWebSecurity |
||||
@Configuration(proxyBeanMethods = false) |
||||
internal class RequiredAuthoritiesAuthorizationManagerConfiguration { |
||||
// tag::httpSecurity[] |
||||
@Bean |
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? { |
||||
// @formatter:off |
||||
http { |
||||
authorizeHttpRequests { |
||||
authorize("/admin/**", hasRole("ADMIN")) |
||||
authorize(anyRequest, authenticated) |
||||
} |
||||
formLogin { } |
||||
oneTimeTokenLogin { } |
||||
} |
||||
// @formatter:on |
||||
return http.build() |
||||
} |
||||
// end::httpSecurity[] |
||||
|
||||
// tag::authorizationManager[] |
||||
@Bean |
||||
fun adminAuthorization(): RequiredAuthoritiesAuthorizationManager<Object> { |
||||
// <1> |
||||
val authorities = MapRequiredAuthoritiesRepository() |
||||
authorities.saveRequiredAuthorities("admin", List.of( |
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, |
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY) |
||||
) |
||||
// <2> |
||||
return RequiredAuthoritiesAuthorizationManager(authorities) |
||||
} |
||||
// end::authorizationManager[] |
||||
|
||||
|
||||
// tag::authorizationManagerFactory[] |
||||
@Bean |
||||
fun authorizationManagerFactory(admins: RequiredAuthoritiesAuthorizationManager<Object>): AuthorizationManagerFactory<Object> { |
||||
val defaults = DefaultAuthorizationManagerFactory<Object>() |
||||
// <1> |
||||
defaults.setAdditionalAuthorization(admins) |
||||
// <2> |
||||
return defaults |
||||
} |
||||
// end::authorizationManagerFactory[] |
||||
|
||||
@Bean |
||||
fun users(): UserDetailsService { |
||||
return InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin()) |
||||
} |
||||
|
||||
@Bean |
||||
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler { |
||||
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent") |
||||
} |
||||
} |
||||
@ -0,0 +1,100 @@
@@ -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" |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,127 @@
@@ -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" |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue