Document Multi-Factor Simple to Complex
This reworks the Multi-Factor documentation to start with the simplest scenario and work to progressively more complex requirements. Closes gh-18029
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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 @@
|
||||
= 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]
|
||||
@@ -15,7 +15,7 @@ Each section that follows will indicate the more notable removals as well as the
|
||||
|
||||
== Core
|
||||
|
||||
* Added Support for xref:servlet/authentication/adaptive.adoc[Multi-factor Authentication]
|
||||
* Added Support for xref:servlet/authentication/mfa.adoc[Multi-Factor Authentication]
|
||||
* Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`
|
||||
* Added javadoc:org.springframework.security.authorization.AllAuthoritiesAuthorizationManager[] and javadoc:org.springframework.security.authorization.AllAuthoritiesReactiveAuthorizationManager[] along with corresponding methods for xref:servlet/authorization/authorize-http-requests.adoc#authorize-requests[Authorizing `HttpServletRequests`] and xref:servlet/authorization/method-security.adoc#using-authorization-expression-fields-and-methods[method security expressions].
|
||||
* Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components
|
||||
|
||||
+5
-1
@@ -39,7 +39,11 @@ class UseAuthorizationManagerFactoryConfiguration {
|
||||
@Bean
|
||||
AuthorizationManagerFactory<Object> authz() {
|
||||
return DefaultAuthorizationManagerFactory.builder()
|
||||
.requireAdditionalAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY).build();
|
||||
.requireAdditionalAuthorities(
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
)
|
||||
.build();
|
||||
}
|
||||
// end::authorizationManagerFactoryBean[]
|
||||
|
||||
|
||||
+12
-9
@@ -1,4 +1,4 @@
|
||||
package org.springframework.security.docs.servlet.authentication.enableglobalmfa;
|
||||
package org.springframework.security.docs.servlet.authentication.egmfa;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -18,8 +18,8 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
// tag::enable-global-mfa[]
|
||||
@EnableGlobalMultiFactorAuthentication(authorities = {
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY })
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY })
|
||||
// end::enable-global-mfa[]
|
||||
public class EnableGlobalMultiFactorAuthenticationConfiguration {
|
||||
|
||||
@@ -28,12 +28,15 @@ public class EnableGlobalMultiFactorAuthenticationConfiguration {
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
// <1>
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
// <2>
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
// <3>
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.enableglobalmfa;
|
||||
package org.springframework.security.docs.servlet.authentication.egmfa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -52,7 +52,7 @@ public class EnableGlobalMultiFactorAuthenticationTests {
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY })
|
||||
@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_USER" })
|
||||
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
+7
-2
@@ -1,4 +1,4 @@
|
||||
package org.springframework.security.docs.servlet.authentication.multifactorauthentication;
|
||||
package org.springframework.security.docs.servlet.authentication.hasallauthorities;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -23,8 +23,13 @@ class ListAuthoritiesConfiguration {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY) // <1>
|
||||
// <1>
|
||||
.anyRequest().hasAllAuthorities(
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
)
|
||||
)
|
||||
// <2>
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.multifactorauthentication;
|
||||
package org.springframework.security.docs.servlet.authentication.hasallauthorities;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.hasallauthorities;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.GrantedAuthorities;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class MultipleAuthorizationRulesConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
// <1>
|
||||
.requestMatchers("/admin/**").hasAllAuthorities(
|
||||
"ROLE_ADMIN",
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
)
|
||||
// <2>
|
||||
.anyRequest().hasAllAuthorities(
|
||||
"ROLE_USER",
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
)
|
||||
)
|
||||
// <3>
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
return new InMemoryUserDetailsManager(
|
||||
User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.authorities("app")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
}
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.hasallauthorities;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.GrantedAuthorities;
|
||||
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class MultipleAuthorizationRulesConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_USER" })
|
||||
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
|
||||
void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = GrantedAuthorities.FACTOR_OTT_AUTHORITY)
|
||||
void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getWhenAuthenticatedThenRedirectsToPassword() throws Exception {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-8
@@ -1,4 +1,4 @@
|
||||
package org.springframework.security.docs.servlet.authentication.customauthorizationmanagerfactory;
|
||||
package org.springframework.security.docs.servlet.authentication.programmaticmfa;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -6,7 +6,7 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationManager;
|
||||
import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||
@@ -27,7 +27,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class CustomAuthorizationManagerFactory {
|
||||
class AdminMfaAuthorizationManagerConfiguration {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
@@ -46,13 +46,19 @@ class CustomAuthorizationManagerFactory {
|
||||
|
||||
// tag::authorizationManager[]
|
||||
@Component
|
||||
class UserBasedOttAuthorizationManager implements AuthorizationManager<Object> {
|
||||
class AdminMfaAuthorizationManager implements AuthorizationManager<Object> {
|
||||
@Override
|
||||
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication, Object context) {
|
||||
if ("admin".equals(authentication.get().getName())) {
|
||||
return AuthorityAuthorizationManager.hasAuthority(GrantedAuthorities.FACTOR_OTT_AUTHORITY)
|
||||
.authorize(authentication, context);
|
||||
AuthorizationManager<Object> admins =
|
||||
AllAuthoritiesAuthorizationManager.hasAllAuthorities(
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY
|
||||
);
|
||||
// <1>
|
||||
return admins.authorize(authentication, context);
|
||||
} else {
|
||||
// <2>
|
||||
return new AuthorizationDecision(true);
|
||||
}
|
||||
}
|
||||
@@ -61,9 +67,12 @@ class CustomAuthorizationManagerFactory {
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
@Bean
|
||||
AuthorizationManagerFactory<Object> authorizationManagerFactory(UserBasedOttAuthorizationManager optIn) {
|
||||
AuthorizationManagerFactory<Object> authorizationManagerFactory(
|
||||
AdminMfaAuthorizationManager admins) {
|
||||
DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
|
||||
defaults.setAdditionalAuthorization(optIn);
|
||||
// <1>
|
||||
defaults.setAdditionalAuthorization(admins);
|
||||
// <2>
|
||||
return defaults;
|
||||
}
|
||||
// end::authorizationManagerFactory[]
|
||||
+6
-6
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.customauthorizationmanagerfactory;
|
||||
package org.springframework.security.docs.servlet.authentication.programmaticmfa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -44,7 +44,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
*/
|
||||
@ExtendWith({SpringExtension.class, SpringTestContextExtension.class})
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class CustomAuthorizationManagerFactoryTests {
|
||||
public class AdminMfaAuthorizationManagerConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@@ -54,7 +54,7 @@ public class CustomAuthorizationManagerFactoryTests {
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void getWhenAdminThenRedirectsToOtt() throws Exception {
|
||||
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
@@ -65,7 +65,7 @@ public class CustomAuthorizationManagerFactoryTests {
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getWhenNotAdminThenAllows() throws Exception {
|
||||
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -74,9 +74,9 @@ public class CustomAuthorizationManagerFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin", authorities = GrantedAuthorities.FACTOR_OTT_AUTHORITY)
|
||||
@WithMockUser(username = "admin", authorities = { GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY })
|
||||
void getWhenAdminAndHasFactorThenAllows() throws Exception {
|
||||
this.spring.register(CustomAuthorizationManagerFactory.class, Http200Controller.class).autowire();
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package org.springframework.security.docs.servlet.authentication.raammfa;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
|
||||
import org.springframework.security.authorization.MapRequiredAuthoritiesRepository;
|
||||
import org.springframework.security.authorization.RequiredAuthoritiesAuthorizationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.GrantedAuthorities;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class RequiredAuthoritiesAuthorizationManagerConfiguration {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManager[]
|
||||
@Bean
|
||||
RequiredAuthoritiesAuthorizationManager<Object> adminAuthorization() {
|
||||
// <1>
|
||||
MapRequiredAuthoritiesRepository authorities = new MapRequiredAuthoritiesRepository();
|
||||
authorities.saveRequiredAuthorities("admin", List.of(
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY)
|
||||
);
|
||||
// <2>
|
||||
return new RequiredAuthoritiesAuthorizationManager<>(authorities);
|
||||
}
|
||||
// end::authorizationManager[]
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
@Bean
|
||||
AuthorizationManagerFactory<Object> authorizationManagerFactory(
|
||||
RequiredAuthoritiesAuthorizationManager admins) {
|
||||
DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
|
||||
// <1>
|
||||
defaults.setAdditionalAuthorization(admins);
|
||||
// <2>
|
||||
return defaults;
|
||||
}
|
||||
// end::authorizationManagerFactory[]
|
||||
|
||||
@Bean
|
||||
public UserDetailsService users() {
|
||||
return new InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin());
|
||||
}
|
||||
|
||||
@Bean
|
||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
|
||||
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.programmaticmfa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.GrantedAuthorities;
|
||||
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith({SpringExtension.class, SpringTestContextExtension.class})
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class RequiredAuthoritiesAuthorizationManagerConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void getWhenAdminThenRedirectsToOtt() throws Exception {
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getWhenNotAdminThenAllows() throws Exception {
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin", authorities = { GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY })
|
||||
void getWhenAdminAndHasFactorThenAllows() throws Exception {
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("admin"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-5
@@ -1,7 +1,9 @@
|
||||
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory;
|
||||
package org.springframework.security.docs.servlet.authentication.selectivemfa;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory;
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -15,17 +17,30 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ListAuthoritiesEverywhereConfiguration {
|
||||
class SelectiveMfaConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
// <1>
|
||||
AuthorizationManagerFactory<Object> mfa =
|
||||
DefaultAuthorizationManagerFactory.<Object>builder()
|
||||
.requireAdditionalAuthorities(
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
)
|
||||
.build();
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/admin/**").hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN") // <1>
|
||||
.anyRequest().hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY)
|
||||
// <2>
|
||||
.requestMatchers("/admin/**").access(mfa.hasRole("ADMIN"))
|
||||
// <3>
|
||||
.requestMatchers("/user/settings/**").access(mfa.authenticated())
|
||||
// <4>
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
// <5>
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.selectivemfa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.GrantedAuthorities;
|
||||
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
|
||||
public class SelectiveMfaConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_ADMIN" })
|
||||
void adminWhenMissingOttThenRequired() throws Exception {
|
||||
this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/admin/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern("http://localhost/login?*"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN" })
|
||||
void adminWhenMfaThenAllowed() throws Exception {
|
||||
this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/admin/"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("user"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_ADMIN" })
|
||||
void userSettingsRequiresMfa() throws Exception {
|
||||
this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/admin/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, "ROLE_USER" })
|
||||
void userSettingsWhenMissingOttThenRequired() throws Exception {
|
||||
this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/user/settings/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern("http://localhost/login?*"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "USER")
|
||||
void rootDoesNotRequireMfa() throws Exception {
|
||||
this.spring.register(SelectiveMfaConfiguration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/"))
|
||||
.andExpect(status().isOk());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -39,7 +39,11 @@ internal class UseAuthorizationManagerFactoryConfiguration {
|
||||
@Bean
|
||||
fun authz(): AuthorizationManagerFactory<Object> {
|
||||
return DefaultAuthorizationManagerFactory.builder<Object>()
|
||||
.requireAdditionalAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY).build()
|
||||
.requireAdditionalAuthorities(
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
)
|
||||
.build()
|
||||
}
|
||||
// end::authorizationManagerFactoryBean[]
|
||||
|
||||
|
||||
+14
-6
@@ -1,7 +1,8 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.enableglobalmfa
|
||||
package org.springframework.security.kt.docs.servlet.authentication.egmfa
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
@@ -15,7 +16,13 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class ListAuthoritiesEverywhereConfiguration {
|
||||
|
||||
// tag::enable-global-mfa[]
|
||||
@EnableGlobalMultiFactorAuthentication( authorities = [
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY])
|
||||
// end::enable-global-mfa[]
|
||||
internal class EnableGlobalMultiFactorAuthenticationConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
@@ -23,9 +30,12 @@ class ListAuthoritiesEverywhereConfiguration {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/admin/**", hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN")) // <1>
|
||||
authorize(anyRequest, hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY))
|
||||
// <1>
|
||||
authorize("/admin/**", hasRole("ADMIN"))
|
||||
// <2>
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
// <3>
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
@@ -34,8 +44,6 @@ class ListAuthoritiesEverywhereConfiguration {
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
|
||||
// end::httpSecurity[]
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(
|
||||
+8
-13
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.enableglobalmfa
|
||||
package org.springframework.security.kt.docs.servlet.authentication.egmfa
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
@@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.RestController
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class AuthorizationManagerFactoryTests {
|
||||
class EnableGlobalMultiFactorAuthenticationConfigurationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@@ -47,11 +47,10 @@ class AuthorizationManagerFactoryTests {
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY])
|
||||
@WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
@@ -63,8 +62,7 @@ class AuthorizationManagerFactoryTests {
|
||||
@WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
@@ -76,8 +74,7 @@ class AuthorizationManagerFactoryTests {
|
||||
@WithMockUser(authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
@@ -89,8 +86,7 @@ class AuthorizationManagerFactoryTests {
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedThenRedirectsToPassword() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
@@ -101,8 +97,7 @@ class AuthorizationManagerFactoryTests {
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun getWhenUnauthenticatedThenRedirectsToBoth() {
|
||||
this.spring.register(UseAuthorizationManagerFactoryConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
this.spring.register(EnableGlobalMultiFactorAuthenticationConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
+7
-2
@@ -1,4 +1,4 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.multifactorauthentication
|
||||
package org.springframework.security.kt.docs.servlet.authentication.hasallauthorities
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@@ -23,8 +23,13 @@ internal class ListAuthoritiesConfiguration {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY))
|
||||
// <1>
|
||||
authorize(anyRequest, hasAllAuthorities(
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
))
|
||||
}
|
||||
// <2>
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.multifactorauthentication
|
||||
package org.springframework.security.kt.docs.servlet.authentication.hasallauthorities
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
+15
-4
@@ -1,4 +1,4 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.authorizationmanagerfactory
|
||||
package org.springframework.security.kt.docs.servlet.authentication.hasallauthorities
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@@ -15,7 +15,7 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class ListAuthoritiesEverywhereConfiguration {
|
||||
internal class MultipleAuthorizationRulesConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
@@ -23,9 +23,20 @@ class ListAuthoritiesEverywhereConfiguration {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/admin/**", hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_ADMIN")) // <1>
|
||||
authorize(anyRequest, hasAllAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY))
|
||||
// <1>
|
||||
authorize("/admin/**", hasAllAuthorities(
|
||||
"ROLE_ADMIN",
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
))
|
||||
// <2>
|
||||
authorize(anyRequest, hasAllAuthorities(
|
||||
"ROLE_USER",
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
))
|
||||
}
|
||||
// <3>
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.hasallauthorities
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.core.GrantedAuthorities
|
||||
import org.springframework.security.test.context.support.WithMockUser
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
|
||||
import org.springframework.test.context.TestExecutionListeners
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class MultipleAuthorizationRulesConfigurationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY, "ROLE_USER"])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordAndOttThenPermits() {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = [GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithPasswordThenRedirectsToOtt() {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=ott&factor.reason=missing"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY])
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedWithOttThenRedirectsToPassword() {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAuthenticatedThenRedirectsToPassword() {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login?factor.type=password&factor.reason=missing"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun getWhenUnauthenticatedThenRedirectsToBoth() {
|
||||
this.spring.register(MultipleAuthorizationRulesConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
fun ok(): String {
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-7
@@ -1,4 +1,4 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.customauthorizationmanagerfactory
|
||||
package org.springframework.security.kt.docs.servlet.authentication.programmaticmfa
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@@ -19,7 +19,7 @@ import java.util.function.Supplier
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
internal class CustomAuthorizationManagerFactory {
|
||||
internal class AdminMfaAuthorizationManagerConfiguration {
|
||||
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
@@ -40,13 +40,19 @@ internal class CustomAuthorizationManagerFactory {
|
||||
|
||||
// tag::authorizationManager[]
|
||||
@Component
|
||||
internal open class UserBasedOttAuthorizationManager : AuthorizationManager<Object> {
|
||||
internal open class AdminMfaAuthorizationManager : AuthorizationManager<Object> {
|
||||
override fun authorize(
|
||||
authentication: Supplier<out Authentication?>, context: Object): AuthorizationResult {
|
||||
return if ("admin" == authentication.get().name) {
|
||||
AuthorityAuthorizationManager.hasAuthority<Object>(GrantedAuthorities.FACTOR_OTT_AUTHORITY)
|
||||
.authorize(authentication, context)
|
||||
var admins =
|
||||
AllAuthoritiesAuthorizationManager.hasAllAuthorities<Any>(
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY
|
||||
)
|
||||
// <1>
|
||||
admins.authorize(authentication, context)
|
||||
} else {
|
||||
// <2>
|
||||
AuthorizationDecision(true)
|
||||
}
|
||||
}
|
||||
@@ -55,9 +61,11 @@ internal class CustomAuthorizationManagerFactory {
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
@Bean
|
||||
fun authorizationManagerFactory(optIn: UserBasedOttAuthorizationManager?): AuthorizationManagerFactory<Object> {
|
||||
fun authorizationManagerFactory(admins: AdminMfaAuthorizationManager): AuthorizationManagerFactory<Object> {
|
||||
val defaults = DefaultAuthorizationManagerFactory<Object>()
|
||||
defaults.setAdditionalAuthorization(optIn)
|
||||
// <1>
|
||||
defaults.setAdditionalAuthorization(admins)
|
||||
// <2>
|
||||
return defaults
|
||||
}
|
||||
// end::authorizationManagerFactory[]
|
||||
+6
-6
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.customauthorizationmanagerfactory
|
||||
package org.springframework.security.kt.docs.servlet.authentication.programmaticmfa
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
@@ -40,7 +40,7 @@ import org.springframework.web.bind.annotation.RestController
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class CustomAuthorizationManagerFactoryTests {
|
||||
class AdminMfaAuthorizationManagerConfigurationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@@ -51,7 +51,7 @@ class CustomAuthorizationManagerFactoryTests {
|
||||
@Throws(Exception::class)
|
||||
@WithMockUser(username = "admin")
|
||||
fun getWhenAdminThenRedirectsToOtt() {
|
||||
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(get("/"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
@@ -63,7 +63,7 @@ class CustomAuthorizationManagerFactoryTests {
|
||||
@Throws(Exception::class)
|
||||
@WithMockUser
|
||||
fun getWhenNotAdminThenAllows() {
|
||||
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -73,9 +73,9 @@ class CustomAuthorizationManagerFactoryTests {
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
@WithMockUser(username = "admin", authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY])
|
||||
@WithMockUser(username = "admin", authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY ])
|
||||
fun getWhenAdminAndHasFactorThenAllows() {
|
||||
this.spring.register(CustomAuthorizationManagerFactory::class.java, Http200Controller::class.java).autowire()
|
||||
this.spring.register(AdminMfaAuthorizationManagerConfiguration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(get("/"))
|
||||
.andExpect(status().isOk())
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.raammfa
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authorization.AuthorizationManagerFactory
|
||||
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory
|
||||
import org.springframework.security.authorization.MapRequiredAuthoritiesRepository
|
||||
import org.springframework.security.authorization.RequiredAuthoritiesAuthorizationManager
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.core.GrantedAuthorities
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||
import java.util.List
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
internal class RequiredAuthoritiesAuthorizationManagerConfiguration {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/admin/**", hasRole("ADMIN"))
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManager[]
|
||||
@Bean
|
||||
fun adminAuthorization(): RequiredAuthoritiesAuthorizationManager<Object> {
|
||||
// <1>
|
||||
val authorities = MapRequiredAuthoritiesRepository()
|
||||
authorities.saveRequiredAuthorities("admin", List.of(
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY)
|
||||
)
|
||||
// <2>
|
||||
return RequiredAuthoritiesAuthorizationManager(authorities)
|
||||
}
|
||||
// end::authorizationManager[]
|
||||
|
||||
|
||||
// tag::authorizationManagerFactory[]
|
||||
@Bean
|
||||
fun authorizationManagerFactory(admins: RequiredAuthoritiesAuthorizationManager<Object>): AuthorizationManagerFactory<Object> {
|
||||
val defaults = DefaultAuthorizationManagerFactory<Object>()
|
||||
// <1>
|
||||
defaults.setAdditionalAuthorization(admins)
|
||||
// <2>
|
||||
return defaults
|
||||
}
|
||||
// end::authorizationManagerFactory[]
|
||||
|
||||
@Bean
|
||||
fun users(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin())
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun tokenGenerationSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
|
||||
return RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.raammfa
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.core.GrantedAuthorities
|
||||
import org.springframework.security.test.context.support.WithMockUser
|
||||
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
|
||||
import org.springframework.test.context.TestExecutionListeners
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringExtension::class, SpringTestContextExtension::class)
|
||||
@TestExecutionListeners(WithSecurityContextTestExecutionListener::class)
|
||||
class RequiredAuthoritiesAuthorizationManagerConfigurationTests {
|
||||
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
@Throws(Exception::class)
|
||||
fun getWhenAdminThenRedirectsToOtt() {
|
||||
this.spring.register(RequiredAuthoritiesAuthorizationManagerConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
@Throws(Exception::class)
|
||||
fun getWhenNotAdminThenAllows() {
|
||||
this.spring.register(RequiredAuthoritiesAuthorizationManagerConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("user"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(
|
||||
username = "admin",
|
||||
authorities = [GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY]
|
||||
)
|
||||
@Throws(
|
||||
Exception::class
|
||||
)
|
||||
fun getWhenAdminAndHasFactorThenAllows() {
|
||||
this.spring.register(RequiredAuthoritiesAuthorizationManagerConfiguration::class.java, Http200Controller::class.java)
|
||||
.autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(MockMvcRequestBuilders.get("/"))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(SecurityMockMvcResultMatchers.authenticated().withUsername("admin"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
fun ok(): String {
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-13
@@ -1,4 +1,4 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.enableglobalmfa
|
||||
package org.springframework.security.kt.docs.servlet.authentication.selectivemfa
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@@ -17,32 +17,38 @@ import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenG
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
internal class UseAuthorizationManagerFactoryConfiguration {
|
||||
internal class SelectiveMfaConfiguration {
|
||||
// tag::httpSecurity[]
|
||||
@Bean
|
||||
@Throws(Exception::class)
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain? {
|
||||
// @formatter:off
|
||||
// <1>
|
||||
val mfa: AuthorizationManagerFactory<Any> =
|
||||
DefaultAuthorizationManagerFactory.builder<Any>()
|
||||
.requireAdditionalAuthorities(
|
||||
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
|
||||
GrantedAuthorities.FACTOR_OTT_AUTHORITY
|
||||
)
|
||||
.build()
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/admin/**", hasRole("ADMIN"))
|
||||
// <2>
|
||||
authorize("/admin/**", mfa.hasRole("ADMIN"))
|
||||
// <3>
|
||||
authorize("/user/settings/**", mfa.authenticated())
|
||||
// <4>
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
// <5>
|
||||
formLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
oneTimeTokenLogin { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
|
||||
// end::httpSecurity[]
|
||||
|
||||
// tag::authorizationManagerFactoryBean[]
|
||||
@Bean
|
||||
fun authz(): AuthorizationManagerFactory<Object> {
|
||||
return DefaultAuthorizationManagerFactory.builder<Object>()
|
||||
.requireAdditionalAuthorities(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY).build()
|
||||
}
|
||||
// end::authorizationManagerFactoryBean[]
|
||||
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
return InMemoryUserDetailsManager(
|
||||
+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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user