You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
14 KiB
214 lines
14 KiB
= 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]
|
|
|