mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-10-21 01:38:50 +00:00
215 lines
14 KiB
Plaintext
215 lines
14 KiB
Plaintext
= 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[].
|
|
For example, when a user authenticates using a password a `FactorGrantedAuthority` with the `authority` of `FactorGrantedAuthority.PASSWORD_AUTHORITY` 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 `AuthorizationManagerFactories` 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]
|