Autenticação de dois fatores com segurança de mola

Autenticação de dois fatores com segurança de mola

1. Visão geral

Neste tutorial, vamos implementarTwo Factor Authentication functionality com um Soft Token e Spring Security.

Vamos adicionar a nova funcionalidade eman existing, simple login flowe usarGoogle Authenticator app para gerar os tokens.

Simplificando, a autenticação de dois fatores é um processo de verificação que segue o princípio bem conhecido de "algo que o usuário sabe e algo que ele possui".

E assim, os usuários fornecem um “token de verificação” extra durante a autenticação - um código de verificação de senha única com base no algoritmo de senha única baseada em tempoTOTP.

2. Configuração do Maven

Primeiro, para usar o Google Authenticator em nosso aplicativo, precisamos:

  • Gerar chave secreta

  • Forneça chave secreta ao usuário via código QR

  • Verifique o token inserido pelo usuário usando essa chave secreta.

Usaremos umlibrary simples do lado do servidor para gerar / verificar a senha de uso único, adicionando a seguinte dependência ao nossopom.xml:


    org.jboss.aerogear
    aerogear-otp-java
    1.0.0

3. Entidade do usuário

Em seguida, modificaremos nossa entidade de usuário para armazenar informações extras - da seguinte maneira:

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

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

Observe que:

  • Salvamos um código secreto aleatório para cada usuário para ser usado posteriormente na geração do código de verificação

  • Nossa verificação em duas etapas é opcional

4. Parâmetro de Login Extra

Primeiro, precisaremos ajustar nossa configuração de segurança para aceitar um token de verificação de parâmetro extra. Podemos fazer isso usandoAuthenticationDetailsSource personalizado:

Aqui está nossoCustomWebAuthenticationDetailsSource:

@Component
public class CustomWebAuthenticationDetailsSource implements
  AuthenticationDetailsSource {

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

e aqui está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 nossa configuração de segurança:

@Configuration
@EnableWebSecurity
public class LssSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomWebAuthenticationDetailsSource authenticationDetailsSource;

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

E, finalmente, adicione o parâmetro extra ao nosso formulário de login:


    Google Authenticator Verification Code

Nota: Precisamos definir nossoAuthenticationDetailsSource personalizado em nossa configuração de segurança.

5. Provedor de autenticação personalizado

Em seguida, precisaremos de umAuthenticationProvider personalizado para lidar com a validação de parâmetro 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);
    }
}

Observe que - depois de verificarmos o código de verificação de senha descartável, simplesmente delegamos a autenticação a jusante.

Aqui está nosso bean do Provedor de Autenticação

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

6. Processo de Registro

Agora, para que os usuários possam usar o aplicativo para gerar os tokens, eles precisarão configurar as coisas corretamente ao se registrar.

E, portanto, precisaremos fazer algumas modificações simples no processo de registro - para permitir que os usuários que optaram por usar a verificação em duas etapas parascan the QR-code they need to login later.

Primeiro, adicionamos esta entrada simples ao nosso formulário de registro:

Use Two step verification 

Então, em nossoRegistrationController - redirecionamos os usuários com base em suas escolhas após a confirmação do registro:

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

E aqui está nosso métodogenerateQRUrl():

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

E aqui está nossoqrcode.html:



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

Go to login page

Observe que:

  • O métodogenerateQRUrl() é usado para gerar URL de código QR

  • Este código QR será digitalizado pelos telefones celulares dos usuários usando o aplicativo Google Authenticator

  • O aplicativo irá gerar um código de 6 dígitos válido por apenas 30 segundos, que é o código de verificação desejado

  • Este código de verificação será verificado durante o login usando nossoAuthenticationProvider personalizado

7. Ativar verificação em duas etapas

Em seguida, garantiremos que os usuários possam alterar suas preferências de login a qualquer momento - da seguinte maneira:

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

E aqui está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;
}

E aqui está o front-end:

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

8. Conclusão

Neste tutorial rápido, ilustramos como fazer uma implementação de autenticação de dois fatores usando um Soft Token com Spring Security.

O código-fonte completo pode ser encontrado - como sempre -over on GitHub.