123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- = 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]
|