Autorisation à deux facteurs avec sécurité de printemps

Autorisation à deux facteurs avec sécurité de printemps

1. Vue d'ensemble

Dans ce tutoriel, nous allons implémenterTwo Factor Authentication functionality avec un Soft Token et Spring Security.

Nous allons ajouter la nouvelle fonctionnalité dansan existing, simple login flow et utiliser lesGoogle Authenticator app pour générer les jetons.

En termes simples, l'authentification à deux facteurs est un processus de vérification qui suit le principe bien connu de «quelque chose que l'utilisateur sait et que l'utilisateur a».

Ainsi, les utilisateurs fournissent un «jeton de vérification» supplémentaire lors de l'authentification - un code de vérification de mot de passe à usage unique basé sur l'algorithme Time-based One-time PasswordTOTP.

2. Configuration Maven

Tout d'abord, pour utiliser Google Authenticator dans notre application, nous devons:

  • Générer une clé secrète

  • Fournir une clé secrète à l'utilisateur via un code QR

  • Jeton de vérification saisi par l'utilisateur à l'aide de cette clé secrète.

Nous utiliserons un simplelibrary côté serveur pour générer / vérifier un mot de passe à usage unique en ajoutant la dépendance suivante à nospom.xml:


    org.jboss.aerogear
    aerogear-otp-java
    1.0.0

3. Entité utilisateur

Ensuite, nous allons modifier notre entité utilisateur pour stocker des informations supplémentaires - comme suit:

@Entity
public class User {
    ...
    private boolean isUsing2FA;
    private String secret;

    public User() {
        super();
        this.secret = Base32.random();
        ...
    }
}

Notez que:

  • Nous sauvegardons un code secret aléatoire pour chaque utilisateur afin de l'utiliser ultérieurement lors de la génération du code de vérification.

  • Notre vérification en 2 étapes est facultative

4. Paramètre de connexion supplémentaire

Premièrement, nous devrons ajuster notre configuration de sécurité pour accepter un jeton de paramètre supplémentaire - vérification. Nous pouvons accomplir cela en utilisant desAuthenticationDetailsSource personnalisés:

Voici nosCustomWebAuthenticationDetailsSource:

@Component
public class CustomWebAuthenticationDetailsSource implements
  AuthenticationDetailsSource {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new CustomWebAuthenticationDetails(context);
    }
}

et voiciCustomWebAuthenticationDetails:

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {

    private String verificationCode;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        verificationCode = request.getParameter("code");
    }

    public String getVerificationCode() {
        return verificationCode;
    }
}

Et notre configuration de sécurité:

@Configuration
@EnableWebSecurity
public class LssSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomWebAuthenticationDetailsSource authenticationDetailsSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .authenticationDetailsSource(authenticationDetailsSource)
            ...
    }
}

Et enfin, ajoutez le paramètre supplémentaire à notre formulaire de connexion:


    Google Authenticator Verification Code

Remarque: nous devons définir nosAuthenticationDetailsSource personnalisés dans notre configuration de sécurité.

5. Fournisseur d'authentification personnalisé

Ensuite, nous aurons besoin d'unAuthenticationProvider personnalisé pour gérer la validation des paramètres supplémentaires:

public class CustomAuthenticationProvider extends DaoAuthenticationProvider {

    @Autowired
    private UserRepository userRepository;

    @Override
    public Authentication authenticate(Authentication auth)
      throws AuthenticationException {
        String verificationCode
          = ((CustomWebAuthenticationDetails) auth.getDetails())
            .getVerificationCode();
        User user = userRepository.findByEmail(auth.getName());
        if ((user == null)) {
            throw new BadCredentialsException("Invalid username or password");
        }
        if (user.isUsing2FA()) {
            Totp totp = new Totp(user.getSecret());
            if (!isValidLong(verificationCode) || !totp.verify(verificationCode)) {
                throw new BadCredentialsException("Invalid verfication code");
            }
        }

        Authentication result = super.authenticate(auth);
        return new UsernamePasswordAuthenticationToken(
          user, result.getCredentials(), result.getAuthorities());
    }

    private boolean isValidLong(String code) {
        try {
            Long.parseLong(code);
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

    @Override
    public boolean supports(Class authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

Notez que - après avoir vérifié le code de vérification de mot de passe à utilisation unique, nous avons simplement délégué l'authentification en aval.

Voici notre bean fournisseur d'authentification

@Bean
public DaoAuthenticationProvider authProvider() {
    CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService);
    authProvider.setPasswordEncoder(encoder());
    return authProvider;
}

6. Processus d'inscription

Désormais, pour que les utilisateurs puissent utiliser l'application pour générer les jetons, ils devront configurer correctement les éléments lors de leur inscription.

Nous devrons donc apporter quelques modifications simples au processus d’inscription - pour permettre aux utilisateurs qui ont choisi d’utiliser la validation en deux étapes descan the QR-code they need to login later.

Tout d'abord, nous ajoutons cette simple entrée à notre formulaire d'inscription:

Use Two step verification 

Ensuite, dans nosRegistrationController - nous redirigeons les utilisateurs en fonction de leurs choix après confirmation de l'inscription:

@RequestMapping(value = "/registrationConfirm", method = RequestMethod.GET)
public String confirmRegistration(@RequestParam("token") String token, ...) {
    String result = userService.validateVerificationToken(token);
    if(result.equals("valid")) {
        User user = userService.getUser(token);
        if (user.isUsing2FA()) {
            model.addAttribute("qr", userService.generateQRUrl(user));
            return "redirect:/qrcode.html?lang=" + locale.getLanguage();
        }

        model.addAttribute(
          "message", messages.getMessage("message.accountVerified", null, locale));
        return "redirect:/login?lang=" + locale.getLanguage();
    }
    ...
}

Et voici notre méthodegenerateQRUrl():

public static String QR_PREFIX =
  "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=";

@Override
public String generateQRUrl(User user) {
    return QR_PREFIX + URLEncoder.encode(String.format(
      "otpauth://totp/%s:%s?secret=%s&issuer=%s",
      APP_NAME, user.getEmail(), user.getSecret(), APP_NAME),
      "UTF-8");
}

Et voici nosqrcode.html:



Scan this Barcode using Google Authenticator app on your phone to use it later in login

Go to login page

Notez que:

  • La méthodegenerateQRUrl() est utilisée pour générer une URL de code QR

  • Ce code QR sera scanné par les téléphones mobiles des utilisateurs à l'aide de l'application Google Authenticator

  • L'application générera un code à 6 chiffres valide pour 30 secondes seulement, code de vérification souhaité

  • Ce code de vérification sera vérifié lors de la connexion à l'aide de nosAuthenticationProvider personnalisés

7. Activer la vérification en deux étapes

Ensuite, nous veillerons à ce que les utilisateurs puissent modifier leurs préférences de connexion à tout moment - comme suit:

@RequestMapping(value = "/user/update/2fa", method = RequestMethod.POST)
@ResponseBody
public GenericResponse modifyUser2FA(@RequestParam("use2FA") boolean use2FA)
  throws UnsupportedEncodingException {
    User user = userService.updateUser2FA(use2FA);
    if (use2FA) {
        return new GenericResponse(userService.generateQRUrl(user));
    }
    return null;
}

Et voiciupdateUser2FA():

@Override
public User updateUser2FA(boolean use2FA) {
    Authentication curAuth = SecurityContextHolder.getContext().getAuthentication();
    User currentUser = (User) curAuth.getPrincipal();
    currentUser.setUsing2FA(use2FA);
    currentUser = repository.save(currentUser);

    Authentication auth = new UsernamePasswordAuthenticationToken(
      currentUser, currentUser.getPassword(), curAuth.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(auth);
    return currentUser;
}

Et voici le front-end:

You are using Two-step authentication Disable 2FA
You are not using Two-step authentication Enable 2FA

8. Conclusion

Dans ce rapide tutoriel, nous avons illustré la procédure de mise en œuvre d'une authentification à deux facteurs à l'aide d'un jeton logiciel avec Spring Security.

Le code source complet peut être trouvé - comme toujours -over on GitHub.