Multi-Factor Authentication with Spring Boot 2 and Spring Security 5
Asked Answered
C

1

15

I want to add multi-factor authentication with TOTP soft tokens to an Angular & Spring application, while keeping everything as close as possible to the defaults of Spring Boot Security Starter.

The token-validation happens locally (with the aerogear-otp-java library), no third party API provider.

Setting up tokens for a user works, but validating them by leveraging Spring Security Authentication Manager/Providers does not.

TL;DR

  • What is the official way to integrate an additional AuthenticationProvider into a Spring Boot Security Starter configured system?
  • What are recommended ways to prevent replay attacks?

Long Version

The API has an endpoint /auth/token from which the frontend can get a JWT token by providing username and password. The response also includes an authentication-status, which can be either AUTHENTICATED or PRE_AUTHENTICATED_MFA_REQUIRED.

If the user requires MFA, the token is issued with a single granted authority of PRE_AUTHENTICATED_MFA_REQUIRED and an expiration-time of 5 minutes. This allows the user to access the endpoint /auth/mfa-token where they can provide the TOTP code from their Authenticator app and get the fully authenticated token to access the site.

Provider and Token

I have created my custom MfaAuthenticationProvider which implements AuthenticationProvider:

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // validate the OTP code
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OneTimePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

And a OneTimePasswordAuthenticationToken which extends AbstractAuthenticationToken to hold the username (taken from the signed JWT) and the OTP code.

Config

I have my custom WebSecurityConfigurerAdapter, where I add my custom AuthenticationProvider via http.authenticationProvider(). Accoring to the JavaDoc, this seems to be the right place:

Allows adding an additional AuthenticationProvider to be used

The relevant parts of my SecurityConfig looks like this.

    @Configuration
    @EnableWebSecurity
    @EnableJpaAuditing(auditorAwareRef = "appSecurityAuditorAware")
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        private final TokenProvider tokenProvider;

        public SecurityConfig(TokenProvider tokenProvider) {
            this.tokenProvider = tokenProvider;
        }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authenticationProvider(new MfaAuthenticationProvider());

        http.authorizeRequests()
            // Public endpoints, HTML, Assets, Error Pages and Login
            .antMatchers("/", "favicon.ico", "/asset/**", "/pages/**", "/api/auth/token").permitAll()

            // MFA auth endpoint
            .antMatchers("/api/auth/mfa-token").hasAuthority(ROLE_PRE_AUTH_MFA_REQUIRED)

            // much more config

Controller

The AuthController has the AuthenticationManagerBuilder injected and is pulling it all together.

@RestController
@RequestMapping(AUTH)
public class AuthController {
    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
        this.tokenProvider = tokenProvider;
        this.authenticationManagerBuilder = authenticationManagerBuilder;
    }

    @PostMapping("/mfa-token")
    public ResponseEntity<Token> mfaToken(@Valid @RequestBody OneTimePassword oneTimePassword) {
        var username = SecurityUtils.getCurrentUserLogin().orElse("");
        var authenticationToken = new OneTimePasswordAuthenticationToken(username, oneTimePassword.getCode());
        var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        // rest of class

However, posting against /auth/mfa-token leads to this error:

"error": "Forbidden",
"message": "Access Denied",
"trace": "org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for de.....OneTimePasswordAuthenticationToken

Why does Spring Security not pick up my Authentication Provider? Debugging the controller shows me that DaoAuthenticationProvider is the only Authentication Provider in AuthenticationProviderManager.

If I expose my MfaAuthenticationProvider as bean, it is the only Provider that is registered, so I get the opposite:

No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken. 

So, how do I get both?

My Question

What is the recommended way to integrate an additional AuthenticationProvider into a Spring Boot Security Starter configured system, so that I get both, the DaoAuthenticationProvider and my own custom MfaAuthenticationProvider? I want to keep the defaults of Spring Boot Scurity Starter and have my own Provider additionally.

Prevention of Replay Attack

I know that the OTP algorithm does not by itself protect against replay attacks within the time slice in which the code is valid; RFC 6238 makes this clear

The verifier MUST NOT accept the second attempt of the OTP after the successful validation has been issued for the first OTP, which ensures one-time only use of an OTP.

I was wondering if there is a recommended way to implement protection. Since the OTP tokens are time based I am thinking of storing the last successful login on the user's model and making sure there is only one successful login per 30 seconds time slice. This of course means synchronization on the user model. Any better approaches?

Thank you.

--

PS: since this is a question about security I am looking for an answer drawing from credible and/or official sources. Thank you.

Castello answered 26/1, 2020 at 11:48 Comment(0)
C
6

To answer my own question, this is how I implemented it, after further research.

I have a provider as a pojo that implements AuthenticationProvider. It's deliberately not a Bean/Component. Otherwise Spring would register it as the only Provider.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

    @Override
    public Authentication authenticate(Authentication authentication) {
        // here be code 
        }

In my SecurityConfig, I let Spring autowire the AuthenticationManagerBuilder and manually inject my MfaAuthenticationProvider

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
       private final AuthenticationManagerBuilder authenticationManagerBuilder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // other code  
        authenticationManagerBuilder.authenticationProvider(getMfaAuthenticationProvider());
        // more code
}

// package private for testing purposes. 
MfaAuthenticationProvider getMfaAuthenticationProvider() {
    return new MfaAuthenticationProvider(accountService);
}

After standard authentication, if the user has MFA enabled, they are pre authenticated with a granted authority of PRE_AUTHENTICATED_MFA_REQUIRED. This allows them to access a single endpoint, /auth/mfa-token. This endpoint takes the username from the valid JWT and the provided TOTP and sends it to the authenticate() method of the authenticationManagerBuilder, which chooses the MfaAuthenticationProvider as it can handle OneTimePasswordAuthenticationToken.

    var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
    var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
Castello answered 18/2, 2020 at 12:49 Comment(1)
How do you deal with redirections? Do you just expect clients to implement or do you have a web flow for redirecting pre-authenticated users to a page?Decelerate

© 2022 - 2024 — McMap. All rights reserved.