Autenticación multifactor con Spring Boot 2 y Spring Security 5

11

Quiero agregar autenticación multifactor con tokens de software TOTP a una aplicación Angular & Spring, manteniendo todo lo más cerca posible de los valores predeterminados de Spring Boot Security Starter .

La validación de token se realiza localmente (con la biblioteca aerogear-otp-java), sin proveedor de API de terceros.

Configurar tokens para un usuario funciona, pero validarlos aprovechando Spring Security Authentication Manager / Providers no lo hace.

TL; DR

  • ¿Cuál es la forma oficial de integrar un AuthenticationProvider adicional en un sistema configurado Spring Boot Security Starter ?
  • ¿Cuáles son las formas recomendadas para prevenir ataques de repetición?

Versión larga

La API tiene un punto final /auth/tokendesde el cual la interfaz puede obtener un token JWT al proporcionar un nombre de usuario y contraseña. La respuesta también incluye un estado de autenticación, que puede ser AUTHENTICATED o PRE_AUTHENTICATED_MFA_REQUIRED .

Si el usuario requiere MFA, el token se emite con una única autoridad otorgada PRE_AUTHENTICATED_MFA_REQUIREDy un tiempo de vencimiento de 5 minutos. Esto permite al usuario acceder al punto final /auth/mfa-tokendonde puede proporcionar el código TOTP desde su aplicación Authenticator y obtener el token totalmente autenticado para acceder al sitio.

Proveedor y token

He creado mi costumbre 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);
    }

Y una OneTimePasswordAuthenticationTokenque se extiende AbstractAuthenticationTokenpara contener el nombre de usuario (tomado del JWT firmado) y el código OTP.

Config

Tengo mi costumbre WebSecurityConfigurerAdapter, donde agrego mi AuthenticationProvidervía personalizada http.authenticationProvider(). Según JavaDoc, este parece ser el lugar correcto:

Permite agregar un AuthenticationProvider adicional para ser utilizado

Las partes relevantes de mi se SecurityConfigven así.

    @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

El AuthControllertiene el AuthenticationManagerBuilderinyectado y lo está juntando todo.

@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

Sin embargo, publicar en contra /auth/mfa-tokenconduce a este error:

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

¿Por qué Spring Security no recoge mi proveedor de autenticación? La depuración del controlador me muestra que DaoAuthenticationProvideres el único proveedor de autenticación en AuthenticationProviderManager.

Si expongo mi MfaAuthenticationProvidercomo bean, es el único proveedor que está registrado, por lo que obtengo lo contrario:

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

Entonces, ¿cómo consigo ambos?

Mi pregunta

¿Cuál es la forma recomendada de integrar un sistema adicional AuthenticationProvideren un sistema configurado Spring Boot Security Starter , de modo que obtenga tanto DaoAuthenticationProvidermi como mi propia costumbre MfaAuthenticationProvider? Quiero mantener los valores predeterminados de Spring Boot Scurity Starter y tener mi propio proveedor adicionalmente.

Prevención del ataque de repetición

Sé que el algoritmo OTP no protege por sí solo contra ataques de repetición dentro del intervalo de tiempo en el que el código es válido; RFC 6238 deja esto claro

El verificador NO DEBE aceptar el segundo intento de la OTP después de que la validación exitosa se haya emitido para la primera OTP, lo que garantiza el uso único de una OTP.

Me preguntaba si hay una forma recomendada de implementar la protección. Dado que los tokens OTP están basados ​​en el tiempo, estoy pensando en almacenar el último inicio de sesión exitoso en el modelo del usuario y asegurarme de que solo haya un inicio de sesión exitoso por cada 30 segundos. Por supuesto, esto significa sincronización en el modelo de usuario. ¿Algún mejor enfoque?

Gracias.

-

PD: dado que esta es una pregunta sobre seguridad, estoy buscando una respuesta basada en fuentes confiables y / o oficiales. Gracias.

phisch
fuente

Respuestas:

0

Para responder a mi propia pregunta, así es como lo implementé, después de una mayor investigación.

Tengo un proveedor como pojo que implementa AuthenticationProvider. Deliberadamente no es un Bean / Componente. De lo contrario, Spring lo registraría como el único proveedor.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

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

En mi SecurityConfig, dejo que Spring conecte AuthenticationManagerBuilderautomáticamente e inyecteMfaAuthenticationProvider

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

Después de la autenticación estándar, si el usuario tiene MFA habilitado, se autentica previamente con una autorización otorgada de PRE_AUTHENTICATED_MFA_REQUIRED . Esto les permite acceder a un único criterio de valoración, /auth/mfa-token. Este punto final toma el nombre de usuario del JWT válido y el TOTP provisto y lo envía al authenticate()método de autenticaciónManagerBuilder, que elige el MfaAuthenticationProviderque puede manejar OneTimePasswordAuthenticationToken.

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