DEV Community

Cover image for Gerenciamento seguro e eficiente de tokens com Go e Redis
Carolina Vila-Nova
Carolina Vila-Nova

Posted on

Gerenciamento seguro e eficiente de tokens com Go e Redis

Publicado originalmente no Medium

Carolina Vila-Nova

Em um cenário de microsserviços e APIs expostas, o gerenciamento seguro e eficiente de tokens de autenticação é crucial para garantir a integridade e o desempenho do sistema.

Recentemente, tive a oportunidade de trabalhar na implementação de uma abordagem que combina cache com refresh automático de tokens, tirando proveito do ecossistema robusto do Go e da alta performance do Redis.

A seguir, detalho essa solução, explorando os desafios iniciais, as estratégias de design e os benefícios alcançados.

gopher with a lock

Nosso ponto de partida foi a necessidade de autenticação junto a uma API externa para poder gerar — no ambiente desse cliente mas em situações específicas definidas por nossa regra de negócio — mensagens informativas aos usuários.

Uma questão que logo se impôs foi onde armazenar o token obtido, levando em consideração o alto volume de tráfego na nossa API. Armazená-lo diretamente em uma instância do serviço não era uma opção viável, pois colocaria em risco a segurança e a escalabilidade do sistema.

A ideia central foi armazenar o token em um cache distribuído, com um tempo de vida (TTL) equivalente ao tempo de validade do próprio token. Algumas características fizeram com que a escolha do Redis como sistema de cache para esse caso de uso não fosse aleatória, entre elas:

  • Desempenho: com armazenamento em memória, o Redis oferece tempos de resposta extremamente rápidos, da ordem de milissegundos. Isso é essencial quando se precisa acessar e validar o token de forma rápida em cada requisição. Um cache lento impactaria diretamente a performance da aplicação.

  • Suporte nativo a TTL: o Redis se encarrega de expirar o token automaticamente após o tempo definido, garantindo que ele seja renovado antes de expirar e prevenindo erros de autenticação. Essa funcionalidade integrada simplifica a lógica de refresh e garante sua confiabilidade.

  • Atomicidade: as operações acontecem como um todo ou não acontecem, garantindo a consistência dos dados no cache. Isso é importante no contexto do cache de tokens para evitar condições de corrida quando múltiplas instâncias do serviço tentam acessar ou atualizar o token simultaneamente.

  • Escalabilidade horizontal: o suporte ao clustering permite escalar horizontalmente a capacidade do cache conforme a demanda aumenta sem comprometer a performance e evitando gargalos

A solução

Antes de realizar qualquer requisição à API protegida, a função verifica se existe um token válido no cache.

func GetToken(key string) (string, error) {  
 cacheToken, err := cache.Get(key)  
 if err != nil {  
  return "", err  
 }  
 return cacheToken, nil  
}
Enter fullscreen mode Exit fullscreen mode

O sistema checa também se esse token ainda é válido, considerando uma margem de segurança (um “buffer”) para evitar requisições com tokens prestes a expirar.

Bibliotecas como a github.com/golang-jwt/jwt/v5 nos permitem decodificar o JWT de forma segura a partir de suas Claims e verificar a data de expiração.

import (  
    "time"  
    "github.com/golang-jwt/jwt/v5"  
)  

func ExpiredToken(tokenString string, buffer time.Duration) (bool, error) {  
    token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})  
    if err != nil {  
        return true, err // Considera expirado em caso de erro na leitura  
    }  

    claims, ok := token.Claims.(jwt.MapClaims)  
    if !ok {  
        return true, err  // Considera expirado se não conseguir extrair claims  
    }  

    exp, ok := claims["exp"].(float64)  
    if !ok {  
       return true, err // Considera expirado se "exp" não estiver presente  
    }  

    expirationTime := time.Unix(int64(exp), 0)  
 return time.Now().Add(buffer).After(expirationTime), nil  
}  

//Nota: essa biblioteca oferece mais opções de validação. Nesta implementação,  
// usamos ParseUnverified apenas para extrair a data de expiração.   
//O token é validado de fato pela API de autenticação
Enter fullscreen mode Exit fullscreen mode

Caso o token não seja encontrado no cache ou tenha expirado (dentro do "buffer" de segurança), o sistema solicita um novo token à API de autenticação e o armazena no cache com o TTL apropriado.

Pode-se ainda usar a biblioteca crypto/rand do Go para gerar um ID único para cada token (um "nonce"), aumentando a segurança e rastreabilidade.

func GetNewToken() (string, error) {  
 // Lógica para obter novo token da API de autenticação  
 // ...  
 return "newToken", nil  
}  

func StoreTokenCache(key string, token string, ttl time.Duration) error {  
 err := cache.Set(key, token, ttl)  
 if err != nil {  
  return err  
 }  
 return nil  
}
Enter fullscreen mode Exit fullscreen mode

O TTL do cache garante que o token seja automaticamente atualizado antes de expirar, evitando a necessidade de tentativas repetidas ("retries") em caso de falha na autenticação. A margem de segurança garante que o token seja renovado antes mesmo de expirar, prevenindo erros. Goroutines são utilizadas para realizar o refresh de forma assíncrona, sem bloquear o fluxo principal da aplicação.

func stageRefresh(key string, timeToRefresg time.Duration) {  
 go func() { // Cria uma goroutine para o refresh  
  time.Sleep(timeToRefresh) // espera o tempo determinado  

  newToken, err := GetNewToken()  
  if err != nil {  
   fmt.Println("Failed to renew token:", err)  
   return  
  }  

  err = StoreTokenCache(key, newToken, tokenTtl)  
  if err != nil {  
   fmt.Println("Failed to store new token in cache:", err)  
   return  
  }  

  fmt.Println("Token refreshed")  
 }()  
}  

// Chamada quando o token é gerado/obtido pela primeira vez  
stageRefresh(tokenKey, timeToRefresh)
Enter fullscreen mode Exit fullscreen mode

Benefícios da abordagem adotada

  • Segurança: O token não fica armazenado localmente por tempo indeterminado, reduzindo a janela de oportunidade para ataques.

  • Escalabilidade: O cache distribuído permite que múltiplas instâncias do serviço compartilhem o mesmo token, eliminando a necessidade de autenticações redundantes.

  • Resiliência: O refresh automático garante que o token esteja sempre disponível, minimizando interrupções no serviço.

  • Performance: Evita retries, otimizando o tempo de resposta das requisições.

Em resumo, o uso do cache com refresh automático nos permitiu criar uma solução robusta, segura e escalável para o gerenciamento de tokens, garantindo a disponibilidade e a integridade do serviço.

Fontes

github.com/go-redis/redis/v8: Para interagir com o Redis e armazenar e/ou invalidar os tokens de maneira eficiente

github.com/golang-jwt/jwt/v5: Para manipulação de tokens JWT, incluindo decodificação, validação e, opcionalmente, geração (embora a geração do token seja tipicamente feita pelo servidor de autenticação)

Top comments (0)