Authentification multifacteur avec Spring Boot 2 et Spring Security 5

11

Je veux ajouter une authentification multifacteur avec des jetons logiciels TOTP à une application Angular & Spring, tout en gardant le plus près possible des valeurs par défaut de Spring Boot Security Starter .

La validation de jeton se produit localement (avec la bibliothèque aerogear-otp-java), aucun fournisseur d'API tiers.

La configuration des jetons pour un utilisateur fonctionne, mais pas leur validation en utilisant Spring Security Authentication Manager / Providers.

TL; DR

  • Quelle est la manière officielle d'intégrer un AuthenticationProvider supplémentaire dans un système configuré Spring Boot Security Starter ?
  • Quels sont les moyens recommandés pour empêcher les attaques par rejeu?

Version longue

L'API a un point /auth/tokende terminaison à partir duquel le frontend peut obtenir un jeton JWT en fournissant un nom d'utilisateur et un mot de passe. La réponse comprend également un état d'authentification, qui peut être AUTHENTICATED ou PRE_AUTHENTICATED_MFA_REQUIRED .

Si l'utilisateur a besoin de l'authentification multifacteur, le jeton est émis avec une seule autorisation accordée PRE_AUTHENTICATED_MFA_REQUIREDet un délai d'expiration de 5 minutes. Cela permet à l'utilisateur d'accéder au point de terminaison /auth/mfa-tokenoù il peut fournir le code TOTP à partir de son application Authenticator et obtenir le jeton entièrement authentifié pour accéder au site.

Fournisseur et jeton

J'ai créé ma coutume MfaAuthenticationProviderqui implémente AuthenticationProvider:

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

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

Et un OneTimePasswordAuthenticationTokenqui s'étend AbstractAuthenticationTokenpour contenir le nom d'utilisateur (tiré du JWT signé) et le code OTP.

Config

J'ai ma coutume WebSecurityConfigurerAdapter, où j'ajoute ma coutume AuthenticationProvidervia http.authenticationProvider(). Selon le JavaDoc, cela semble être le bon endroit:

Permet d'ajouter un fournisseur d'authentification supplémentaire à utiliser

Les parties pertinentes de mon SecurityConfigapparence ressemblent à ceci.

    @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

Manette

Le AuthControllera AuthenticationManagerBuilderinjecté et tire tout cela ensemble.

@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

Cependant, la publication contre /auth/mfa-tokenconduit à cette erreur:

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

Pourquoi Spring Security ne récupère pas mon fournisseur d'authentification? Le débogage du contrôleur me montre que DaoAuthenticationProviderc'est le seul fournisseur d'authentification dans AuthenticationProviderManager.

Si j'expose mon MfaAuthenticationProvideras bean, c'est le seul fournisseur enregistré, donc j'obtiens le contraire:

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

Alors, comment puis-je obtenir les deux?

Ma question

Quelle est la méthode recommandée pour intégrer un élément supplémentaire AuthenticationProviderdans un système configuré Spring Boot Security Starter , afin que j'obtienne à la fois la DaoAuthenticationProvideret ma propre personnalisation MfaAuthenticationProvider? Je veux conserver les valeurs par défaut de Spring Boot Scurity Starter et avoir mon propre fournisseur en plus.

Prévention des attaques par rejeu

Je sais que l'algorithme OTP ne protège pas à lui seul contre les attaques de rejeu dans la tranche de temps dans laquelle le code est valide; La RFC 6238 le montre clairement

Le vérificateur NE DOIT PAS accepter la deuxième tentative de l'OTP après que la validation réussie a été émise pour le premier OTP, ce qui garantit une utilisation unique et unique d'un OTP.

Je me demandais s'il existe un moyen recommandé de mettre en œuvre la protection. Étant donné que les jetons OTP sont basés sur le temps, je pense à stocker la dernière connexion réussie sur le modèle de l'utilisateur et à m'assurer qu'il n'y a qu'une seule connexion réussie par tranche de 30 secondes. Cela signifie bien sûr une synchronisation sur le modèle utilisateur. De meilleures approches?

Je vous remercie.

-

PS: puisqu'il s'agit d'une question de sécurité je recherche une réponse tirée de sources crédibles et / ou officielles. Je vous remercie.

phisch
la source

Réponses:

0

Pour répondre à ma propre question, voici comment je l'ai implémenté, après de nouvelles recherches.

J'ai un fournisseur en tant que pojo qui implémente AuthenticationProvider. Ce n'est délibérément pas un Bean / Component. Sinon, Spring l'enregistrerait comme le seul fournisseur.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

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

Dans mon SecurityConfig, je laisse Spring câbler AuthenticationManagerBuilderautomatiquement et injecter manuellement monMfaAuthenticationProvider

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

Après l'authentification standard, si l'utilisateur a activé l'authentification MFA, il est pré-authentifié avec une autorisation accordée de PRE_AUTHENTICATED_MFA_REQUIRED . Cela leur permet d'accéder à un seul point final, /auth/mfa-token. Ce point de terminaison prend le nom d'utilisateur du JWT valide et du TOTP fourni et l'envoie à la authenticate()méthode du authenticationManagerBuilder, qui choisit le MfaAuthenticationProvidercomme il peut le gérer OneTimePasswordAuthenticationToken.

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