Autenticação multifator com Spring Boot 2 e Spring Security 5

11

Desejo adicionar autenticação multifatorial com tokens flexíveis TOTP a um aplicativo Angular & Spring, mantendo tudo o mais próximo possível dos padrões do Spring Boot Security Starter .

A validação de token ocorre localmente (com a biblioteca aerogear-otp-java), sem provedor de API de terceiros.

A configuração de tokens para um usuário funciona, mas a validação deles aproveitando o Spring Security Authentication Manager / Providers não.

TL; DR

  • Qual é a maneira oficial de integrar um AuthenticationProvider adicional em um sistema configurado do Spring Boot Security Starter ?
  • Quais são as formas recomendadas para evitar ataques de repetição?

Versão longa

A API possui um ponto /auth/tokende extremidade a partir do qual o front-end pode obter um token JWT, fornecendo nome de usuário e senha. A resposta também inclui um status de autenticação, que pode ser AUTHENTICATED ou PRE_AUTHENTICATED_MFA_REQUIRED .

Se o usuário exigir MFA, o token será emitido com uma única autoridade concedida PRE_AUTHENTICATED_MFA_REQUIREDe um tempo de expiração de 5 minutos. Isso permite que o usuário acesse o terminal /auth/mfa-tokenem que pode fornecer o código TOTP a partir do aplicativo Authenticator e obtenha o token totalmente autenticado para acessar o site.

Fornecedor e Token

Eu criei meu costume MfaAuthenticationProviderque implementa AuthenticationProvider:

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

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

E um OneTimePasswordAuthenticationTokenque se estende AbstractAuthenticationTokenpara manter o nome de usuário (retirado do JWT assinado) e o código OTP.

Config

Eu tenho meu costume WebSecurityConfigurerAdapter, onde adiciono meu costume AuthenticationProvidervia http.authenticationProvider(). De acordo com o JavaDoc, este parece ser o lugar certo:

Permite adicionar um AuthenticationProvider adicional a ser usado

As partes relevantes da minha SecurityConfigaparência se parecem com isso.

    @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

Controlador

A AuthControllertem a AuthenticationManagerBuilderinjectada e é puxando-o todos juntos.

@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

No entanto, a publicação contra /auth/mfa-tokenleva a este erro:

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

Por que o Spring Security não seleciona meu provedor de autenticação? A depuração do controlador mostra que DaoAuthenticationProvideré o único provedor de autenticação disponível AuthenticationProviderManager.

Se eu expor meu MfaAuthenticationProvidercomo bean, é o único provedor que está registrado, portanto, obtenho o oposto:

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

Então, como faço para obter os dois?

Minha pergunta

Qual é a maneira recomendada de integrar um adicional AuthenticationProvidera um sistema configurado do Spring Boot Security Starter , para que eu possa obter os dois, o DaoAuthenticationProvidermeu e o meu próprio costume MfaAuthenticationProvider? Desejo manter os padrões do Spring Boot Scurity Starter e ter meu próprio provedor adicionalmente.

Prevenção de Replay Attack

Eu sei que o algoritmo OTP por si só não protege contra ataques de repetição no intervalo de tempo em que o código é válido; A RFC 6238 deixa isso claro

O verificador NÃO DEVE aceitar a segunda tentativa do OTP após a validação bem-sucedida ter sido emitida para o primeiro OTP, o que garante o uso único de um OTP apenas uma vez.

Fiquei me perguntando se existe uma maneira recomendada de implementar a proteção. Como os tokens OTP são baseados no tempo, estou pensando em armazenar o último logon bem-sucedido no modelo do usuário e garantir que haja apenas um logon bem-sucedido por intervalo de 30 segundos. Obviamente, isso significa sincronização no modelo do usuário. Alguma abordagem melhor?

Obrigado.

-

PS: como se trata de uma questão de segurança, estou procurando uma resposta de fontes confiáveis ​​e / ou oficiais. Obrigado.

phisch
fonte

Respostas:

0

Para responder à minha própria pergunta, foi assim que eu a implementei, depois de mais pesquisas.

Eu tenho um provedor como um pojo que implementa AuthenticationProvider. Deliberadamente não é um Bean / Component. Caso contrário, a Spring o registraria como o único provedor.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

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

No meu SecurityConfig, deixo o Spring conectar automaticamente AuthenticationManagerBuildere injetar manualmente meuMfaAuthenticationProvider

@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);
}

Após a autenticação padrão, se o usuário tiver o MFA ativado, eles serão pré-autenticados com uma autoridade concedida PRE_AUTHENTICATED_MFA_REQUIRED . Isso permite que eles acessem um único terminal /auth/mfa-token,. Esse nó de extremidade pega o nome de usuário do JWT válido e do TOTP fornecido e o envia para o authenticate()método do authenticationManagerBuilder, que escolhe o MfaAuthenticationProviderque ele pode manipular OneTimePasswordAuthenticationToken.

    var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
    var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
phisch
fonte