|
@@ -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]
|