Autenticazione a due fattori con Spring Security

1. Panoramica

In questo tutorial, implementeremo la funzionalità di autenticazione a due fattori con un soft token e Spring Security.

Aggiungeremo la nuova funzionalità a un semplice flusso di accesso esistente e utilizzeremo l'app Google Authenticator per generare i token.

In poche parole, l'autenticazione a due fattori è un processo di verifica che segue il noto principio di "qualcosa che l'utente conosce e qualcosa che l'utente ha".

Pertanto, gli utenti forniscono un ulteriore "token di verifica" durante l'autenticazione: un codice di verifica della password una tantum basato sull'algoritmo TOTP della password monouso basato sul tempo.

2. Configurazione Maven

Innanzitutto, per poter utilizzare Google Authenticator nella nostra app dobbiamo:

  • Genera chiave segreta
  • Fornisci la chiave segreta all'utente tramite codice QR
  • Verifica il token immesso dall'utente utilizzando questa chiave segreta.

Useremo una semplice libreria lato server per generare / verificare la password monouso aggiungendo la seguente dipendenza al nostro pom.xml :

 org.jboss.aerogear aerogear-otp-java 1.0.0 

3. Entità utente

Successivamente, modificheremo la nostra entità utente per contenere informazioni aggiuntive, come segue:

@Entity public class User { ... private boolean isUsing2FA; private String secret; public User() { super(); this.secret = Base32.random(); ... } }

Nota che:

  • Salviamo un codice segreto casuale per ogni utente da utilizzare in seguito nella generazione del codice di verifica
  • La nostra verifica in due passaggi è facoltativa

4. Parametro di accesso aggiuntivo

Innanzitutto, dovremo regolare la nostra configurazione di sicurezza per accettare un parametro aggiuntivo: il token di verifica. Possiamo farlo utilizzando AuthenticationDetailsSource personalizzato :

Ecco il nostro CustomWebAuthenticationDetailsSource :

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

ed ecco CustomWebAuthenticationDetails :

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails { private String verificationCode; public CustomWebAuthenticationDetails(HttpServletRequest request) { super(request); verificationCode = request.getParameter("code"); } public String getVerificationCode() { return verificationCode; } }

E la nostra configurazione di sicurezza:

@Configuration @EnableWebSecurity public class LssSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomWebAuthenticationDetailsSource authenticationDetailsSource; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .authenticationDetailsSource(authenticationDetailsSource) ... } }

E infine aggiungi il parametro extra al nostro modulo di accesso:

 Google Authenticator Verification Code  

Nota: dobbiamo impostare la nostra AuthenticationDetailsSource personalizzata nella nostra configurazione di sicurezza.

5. Fornitore di autenticazione personalizzato

Successivamente, avremo bisogno di un AuthenticationProvider personalizzato per gestire la convalida dei parametri extra:

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

Tieni presente che, dopo aver verificato il codice di verifica della password monouso, abbiamo semplicemente delegato l'autenticazione a valle.

Ecco il nostro bean di provider di autenticazione

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

6. Processo di registrazione

Ora, affinché gli utenti possano utilizzare l'applicazione per generare i token, dovranno impostare correttamente le cose al momento della registrazione.

Pertanto, dovremo apportare alcune semplici modifiche al processo di registrazione, per consentire agli utenti che hanno scelto di utilizzare la verifica in due passaggi di scansionare il codice QR di cui hanno bisogno per accedere in un secondo momento .

Per prima cosa, aggiungiamo questo semplice input al nostro modulo di registrazione:

Use Two step verification 

Quindi, nel nostro RegistrationController , reindirizziamo gli utenti in base alle loro scelte dopo aver confermato la registrazione:

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

Ed ecco il nostro metodo 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"); }

Ed ecco il nostro qrcode.html :

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

Go to login page

Nota che:

  • Il metodo generateQRUrl () viene utilizzato per generare l'URL del codice QR
  • Questo codice QR verrà scansionato dai telefoni cellulari degli utenti utilizzando l'app Google Authenticator
  • L'app genererà un codice a 6 cifre valido per soli 30 secondi, che è il codice di verifica desiderato
  • Questo codice di verifica verrà verificato durante l'accesso utilizzando il nostro AuthenticationProvider personalizzato

7. Abilitare la verifica in due passaggi

Successivamente, ci assicureremo che gli utenti possano modificare le proprie preferenze di accesso in qualsiasi momento, come segue:

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

Ed ecco 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; }

Ed ecco il 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. Conclusione

In questo breve tutorial, abbiamo illustrato come eseguire un'implementazione dell'autenticazione a due fattori utilizzando un soft token con Spring Security.

Il codice sorgente completo può essere trovato - come sempre - su GitHub.