Two Factor Auth avec Spring Security

1. Vue d'ensemble

Dans ce didacticiel, nous allons implémenter la fonctionnalité d'authentification à deux facteurs avec un jeton logiciel et Spring Security.

Nous allons ajouter la nouvelle fonctionnalité à un flux de connexion existant et simple et utiliser l'application Google Authenticator 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 quelque chose que l'utilisateur possède».

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 Password TOTP.

2. Configuration Maven

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

  • Générer une clé secrète
  • Fournir la clé secrète à l'utilisateur via le code QR
  • Vérifiez le jeton entré par l'utilisateur à l'aide de cette clé secrète.

Nous utiliserons une simple bibliothèque côté serveur pour générer / vérifier un mot de passe à usage unique en ajoutant la dépendance suivante à notre pom.xml :

 org.jboss.aerogear aerogear-otp-java 1.0.0 

3. Entité utilisateur

Ensuite, nous modifierons notre entité utilisateur pour contenir 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 enregistrons un code secret aléatoire pour chaque utilisateur à utiliser plus tard dans la génération du code de vérification
  • Notre vérification en 2 étapes est facultative

4. Paramètre de connexion supplémentaire

Tout d'abord, nous devrons ajuster notre configuration de sécurité pour accepter un paramètre supplémentaire - le jeton de vérification. Nous pouvons accomplir cela en utilisant AuthenticationDetailsSource personnalisé :

Voici notre CustomWebAuthenticationDetailsSource :

@Component public class CustomWebAuthenticationDetailsSource implements AuthenticationDetailsSource { @Override public WebAuthenticationDetails buildDetails(HttpServletRequest context) { return new CustomWebAuthenticationDetails(context); } }

et voici CustomWebAuthenticationDetails :

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 notre AuthenticationDetailsSource personnalisée dans notre configuration de sécurité.

5. Fournisseur d'authentification personnalisé

Ensuite, nous aurons besoin d'un AuthenticationProvider personnalisé pour gérer la validation de 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 qu'après avoir vérifié le code de vérification du mot de passe à usage 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 les choses correctement 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 vérification en 2 étapes de scanner le code QR dont ils ont besoin pour se connecter plus tard .

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

Use Two step verification 

Ensuite, dans notre RegistrationController - nous redirigeons les utilisateurs en fonction de leurs choix après avoir confirmé l'inscription:

@GetMapping("/registrationConfirm") 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éthode generateQRUrl () :

public static String QR_PREFIX = "//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 notre qrcode.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éthode generateQRUrl () 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 pendant seulement 30 secondes, code de vérification souhaité
  • Ce code de vérification sera vérifié lors de la connexion à l'aide de notre fournisseur d' authentification personnalisé

7. Activer la vérification en deux étapes

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

@PostMapping("/user/update/2fa") 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 voici updateUser2FA () :

@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

Scan this Barcode using Google Authenticator app on your phone

function enable2FA(){ set2FA(true); } function disable2FA(){ set2FA(false); } function set2FA(use2FA){ $.post( "/user/update/2fa", { use2FA: use2FA } , function( data ) { if(use2FA){ $("#qr").append('').show(); }else{ window.location.reload(); } }); }

8. Conclusion

Dans ce tutoriel rapide, nous avons illustré comment effectuer une implémentation d'authentification à deux facteurs à l'aide d'un jeton logiciel avec Spring Security.

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