web-dev-qa-db-fra.com

Authentification multifacteur avec Spring Boot 2 et Spring Security 5

Je souhaite ajouter une authentification multifacteur avec des jetons logiciels TOTP à une application Angular & Spring, tout en gardant tout aussi proche que possible des valeurs par défaut de Spring Boot Démarreur de sécurité .

La validation des jetons se produit localement (avec la bibliothèque aerogear-otp-Java), sans fournisseur d'API tiers.

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

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 de relecture?

Version longue

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

Si l'utilisateur a besoin de l'authentification multifacteur, le jeton est émis avec une seule autorité accordée de PRE_AUTHENTICATED_MFA_REQUIRED Et un délai d'expiration de 5 minutes. Cela permet à l'utilisateur d'accéder au point de terminaison /auth/mfa-token Où 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éé mon MfaAuthenticationProvider personnalisé qui 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 OneTimePasswordAuthenticationToken qui étend AbstractAuthenticationToken pour contenir le nom d'utilisateur (tiré du JWT signé) et le code OTP.

Config

J'ai mon WebSecurityConfigurerAdapter personnalisé, où j'ajoute mon AuthenticationProvider personnalisé via http.authenticationProvider(). Selon le JavaDoc, cela semble être le bon endroit:

Permet d'ajouter un AuthenticationProvider supplémentaire à utiliser

Les parties pertinentes de mon SecurityConfig 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 AuthController a le AuthenticationManagerBuilder injecté et le rassemble.

@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, publier contre /auth/mfa-token Conduit à 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-t-il pas mon fournisseur d'authentification? Le débogage du contrôleur me montre que DaoAuthenticationProvider est le seul fournisseur d'authentification dans AuthenticationProviderManager.

Si j'expose mon MfaAuthenticationProvider en tant que bean, c'est le fournisseur uniquement qui est 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 AuthenticationProvider supplémentaire dans un système configuré Spring Boot Security Starter , de sorte que j'obtienne les deux, le DaoAuthenticationProvider et mon propre MfaAuthenticationProvider personnalisé? Je veux conserver les valeurs par défaut de Spring Boot Scurity Starter et avoir mon propre fournisseur en plus.

Prévention des attaques de relecture

Je sais que l'algorithme OTP ne protège pas par lui-même contre les attaques de relecture pendant la tranche de temps pendant laquelle le code est valide; La RFC 6238 le précise

Le vérificateur NE DOIT PAS accepter la deuxième tentative du protocole OTP après que la validation réussie a été émise pour le premier OTP, ce qui garantit une utilisation 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. Des meilleures approches?

Je vous remercie.

-

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

11
phisch

Pour répondre à ma propre question, voici comment je l'ai mis en œuvre, après de nouvelles recherches.

J'ai un fournisseur comme 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 autowire le AuthenticationManagerBuilder et injecter manuellement mon 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);
}

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

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