1
resposta

[Sugestão] Melhor forma de armazenar o RefreshToken

O RefreshToken é uma chave extremamente poderosa, que se vazada, pode acarretar em um grande desastre. Então trago uma sugestão que senti falta durante a aula que é transporta-la via Cookie HTTPOnly. Essa estratégia barra qualquer java script malicioso que tenha acesso ao refresh token ou de forma geral a attacks do tipo XSS.

Durante as aulas foi aprensentada a opção de retornar o access e o refresh tokens no corpo da requisição, o que torna acessível aos ataques mencionados acima. Abaixo mostro um exemplo e sugestão para os próximos alunos usarem ou para que algum instrutor possa atualizar o curso pois considero a segurança a parte mais importante de uma api.

@RestController
@RequestMapping("/authenticate")
public class AuthenticationController {

    @Autowired
    private TokenManager tokenManager;

    @Autowired
    private AuthenticationManager manager;
    
    @Autowired
    private UserRepository userRepository;

    @PostMapping
    public ResponseEntity userAuthentication(@RequestBody @Valid UserLoginDTO userLoginDTO, HttpServletResponse request) {
        
        var token = new UsernamePasswordAuthenticationToken(userLoginDto.getEmail(), userLoginDto.getPassword());
        var authentication = manager.authenticate(token);
        User user = ((SystemUser) authentication.getPrincipal()).getUser();


        // Gera o access token
        TokenDTO tokenJWT = new TokenDTO(tokenManager.generateAccessToken(user));
        
        // Gera o refresh token
        var newRefreshToken = tokenManager.generateToken(user, 24 * ONE_HOUR_IN_MINUTES);

        // Cria o cookie com o refresh token
        ResponseCookie cookie = ResponseCookie.from("refresh_token", newRefreshToken)
                .httpOnly(true)
                .secure(true)
                .sameSite("Strict")
                .path("/authenticate/refresh")
                .maxAge(Duration.ofMinutes(3))
                .build();

        // Salva o cookie no header
        request.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

        // Retorna o access token no corpo da requisição
        return ResponseEntity.ok(tokenJWT);
    }

    @GetMapping("/refresh")
    public ResponseEntity userRefreshAccessToken(@CookieValue("refresh_token") String refreshToken) {
        String userEmail = tokenManager.getSubject(refreshToken);
        User user = userRepository.findByEmail(userEmail).orElseThrow(() -> new UsernameNotFoundException("User subject token invalid."));

        var tokenJWT = new TokenDTO(tokenManager.generateAccessToken(user));

        return ResponseEntity.ok(tokenJWT);
    }
}

No exemplo acima temos o método Post onde o recebe o usuário e senha, gera o access token, gera o refresh token e incorpora ele em um cookie http only seguro e retorna apenas o access token no corpo da requisição.

Abaixo no método Get "/refresh", é usado o cookie para pegar o refresh token e gerar um novo access token e retornar para o client sem que o client se preocupe de manipular o refresh token.

Essa estratégia impede um monte de dor de cabeça, mas ainda não é tudo, existem mais camadas de proteção que podemos implementar para deixar nossa segurança ainda mais robusta, uma delas é o rotation refresh token que deixo como dica para quem quiser se aprofundar mais ainda.

Por último alguns links de referências:
https://dev.to/khaledsaeed18/access-and-refresh-tokens-in-token-based-authentication-2a9o
https://docs.descope.com/security-best-practices/refresh-token-storage

Espero ter contribuido um pouco com esta seleta comunidade e estou aberto a críticas ou sugestões.

1 resposta

Olá, Ulysses! Tudo bem?

Excelente contribuição! É inspirador ver alunos indo além do conteúdo das aulas e trazendo discussões de alto nível sobre segurança cibernética. Você tocou em um ponto nevrálgico: a exposição de credenciais sensíveis no lado do cliente.

Sua sugestão de utilizar Cookies HttpOnly para o RefreshToken é considerada uma das melhores práticas da indústria por vários motivos técnicos que você bem pontuou:

Por que sua sugestão é robusta:

  • Blindagem contra XSS: Ao definir o cookie como httpOnly(true), o navegador impede que scripts (JavaScript) acessem o valor. Se um invasor conseguir injetar um script malicioso na sua página, ele ainda assim não conseguirá "ler" o token para roubá-lo.
  • Configurações de Segurança: O uso de .secure(true) garante que o token só trafegue em conexões criptografadas (HTTPS), e o .sameSite("Strict") é uma defesa poderosa contra ataques do tipo CSRF (Cross-Site Request Forgery).
  • Escopo Restrito: Definir o .path("/authenticate/refresh") é uma ótima prática de princípio do menor privilégio, garantindo que o cookie só seja enviado para o endpoint que realmente precisa dele.

O fluxo de "Silent Refresh"

O que você descreveu no método userRefreshAccessToken cria um fluxo de Silent Refresh muito elegante. O cliente (front-end) nem precisa saber que o RefreshToken existe; ele apenas chama o endpoint de refresh e, se o cookie estiver lá e for válido, recebe um novo AccessToken fresquinho no corpo da resposta.

Dica de Ouro: Refresh Token Rotation

Você mencionou a rotação de tokens, e para quem está lendo, essa técnica consiste em invalidar o RefreshToken antigo e gerar um novo a cada vez que o endpoint de refresh é usado. Isso ajuda a detectar se um token foi roubado (pois se o mesmo token for usado duas vezes, o sistema sabe que algo está errado e desloga o usuário).

Com certeza sua sugestão servirá de guia para muitos alunos que buscam criar APIs com Spring Security em nível de produção.

Espero que possa ter lhe ajudado!