Autenticação e JWT em Go: Tokens, Assinatura e RBAC
Como emitir e validar JWT com assinatura, aplicar RBAC nas rotas e tratar expiração e revogação em uma API Go stateless.
Publicada em · 7 min de leitura
Autenticação e JWT em Go: Tokens, Assinatura e RBAC
Em arquiteturas modernas de microsserviços e APIs REST, o JSON Web Token (JWT) estabeleceu-se como o padrão da indústria para representação de credenciais e troca de informações seguras entre partes de forma stateless (RFC 7519).
Diferente de sessões tradicionais baseadas em cookies de servidor que dependem de consultas constantes a bancos de dados ou clusters Redis a cada requisição, um token JWT contém em si mesmo as informações de identidade do usuário (chamadas de Claims) e uma assinatura criptográfica gerada por uma chave secreta ou par assimétrico de chaves.
1. O Ciclo de Vida da Autenticação com JWT
O fluxo padrão de autenticação desacopla o momento em que as credenciais são validadas da verificação contínua das requisições subsequentes:
Etapas da Arquitetura:
- Login (Handshake Inicial): O cliente envia
emailesenhaviaPOST /api/v1/auth/login. O servidor valida as credenciais contra o banco de dados (ex: hash bcrypt) e, se válidas, gera o Access Token JWT assinado. - Armazenamento e Envio: O cliente armazena o token e o anexa a todas as requisições subsequentes no header HTTP
Authorization: Bearer <token>. - Interceptação por Middleware: O Middleware de Autenticação intercepta a chamada, valida a integridade matemática da assinatura e a data de expiração (
exp). - Context Injection: O middleware extrai as Claims do token e as injeta no
context.Contextda requisição Go de forma fortemente tipada. - Decisão de Autorização: Os handlers de endpoint consomem as informações do usuário a partir do contexto sem efetuar nenhuma chamada ao banco de dados para validar a identidade.
2. Criptografia Simétrica (HS256) vs Assimétrica (RS256)
A escolha do algoritmo de assinatura define como as chaves criptográficas circulam entre seus serviços:
Comparativo Arquitetural:
| Critério | Simétrica (HS256 / HMAC-SHA256) | Assimétrica (RS256 / ES256 - RSA/ECDSA) |
|---|---|---|
| Chaves | Uma única chave secreta compartilhada | Par: Chave Privada (Private Key) + Chave Pública (Public Key) |
| Quem assina? | Quem possui o segredo | Somente o Auth Service (com a chave privada) |
| Quem valida? | Quem possui o segredo | Qualquer microsserviço (apenas com a chave pública) |
| Risco de Vazamento | Crítico: se um microsserviço vazar a chave, ele pode forjar tokens válidos | Baixo: expor a chave pública não permite forjar tokens |
| Performance | Extremamente rápida (~10x mais rápida que RSA) | Mais lenta para assinar/verificar |
| Cenário Recomendado | Monólitos e APIs fechadas com poucos serviços | Ecossistemas de microsserviços, OAuth2, OpenID Connect |
3. Implementação Prática com golang-jwt/jwt/v5
A biblioteca oficial e auditada para manipulação de tokens no Go moderno é o pacote github.com/golang-jwt/jwt/v5.
3.1 Modelagem de Custom Claims
Evite armazenar tipos primitivos soltos. Modele uma struct para encapsular as informações de negócio e embutir a struct jwt.RegisteredClaims:
package auth
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
// CustomClaims representa a identidade do usuário autenticado no sistema
type CustomClaims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Roles []string `json:"roles"`
TenantID string `json:"tenant_id,omitempty"`
jwt.RegisteredClaims
}
3.2 O Gerenciador de Tokens (TokenManager)
O TokenManager centraliza a geração e a verificação matemática dos tokens, protegendo a aplicação contra o famoso ataque “alg: none” (onde um invasor altera o cabeçalho do token alegando que nenhuma assinatura foi usada):
package auth
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrInvalidToken = errors.New("token inválido ou corrompido")
ErrExpiredToken = errors.New("token expirado")
)
type TokenManager struct {
secretKey []byte
issuer string
tokenDuration time.Duration
}
func NewTokenManager(secret string, issuer string, duration time.Duration) *TokenManager {
return &TokenManager{
secretKey: []byte(secret),
issuer: issuer,
tokenDuration: duration,
}
}
// GenerateAccessToken emite um token assinado de curta duração
func (tm *TokenManager) GenerateAccessToken(userID, email string, roles []string) (string, error) {
now := time.Now().UTC()
claims := CustomClaims{
UserID: userID,
Email: email,
Roles: roles,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: tm.issuer,
Subject: userID,
Audience: jwt.ClaimStrings{"api"},
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(tm.tokenDuration)),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(tm.secretKey)
}
// ValidateToken decodifica e verifica a assinatura e expiração do token
func (tm *TokenManager) ValidateToken(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(t *jwt.Token) (any, error) {
// Proteção estrita de algoritmo: rejeita qualquer token que não utilize HMAC-SHA256
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("método de assinatura inesperado: %v", t.Header["alg"])
}
return tm.secretKey, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(*CustomClaims)
if !ok || !token.Valid {
return nil, ErrInvalidToken
}
return claims, nil
}
4. O Middleware de Autenticação
Construído sobre o padrão examinado em Middlewares em Go, este middleware extrai o token do header Authorization, valida-o e injeta o usuário autenticado no context.Context:
package middleware
import (
"context"
"net/http"
"strings"
"meu-projeto/internal/auth"
)
// Chave privada para isolar o context
type userCtxKey struct{}
func Authenticate(tm *auth.TokenManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, `{"error":"cabeçalho de autorização ausente"}`, http.StatusUnauthorized)
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
http.Error(w, `{"error":"formato de autorização inválido (esperado: Bearer <token>)"}`, http.StatusUnauthorized)
return
}
claims, err := tm.ValidateToken(parts[1])
if err != nil {
http.Error(w, `{"error":"token inválido ou expirado"}`, http.StatusUnauthorized)
return
}
// Injeta as claims no contexto da requisição
ctx := context.WithValue(r.Context(), userCtxKey{}, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// UserFromContext extrai as claims do usuário com segurança e tipagem estrita
func UserFromContext(ctx context.Context) (*auth.CustomClaims, bool) {
claims, ok := ctx.Value(userCtxKey{}).(*auth.CustomClaims)
return claims, ok
}
5. Autorização Baseada em Perfis (RBAC - Role-Based Access Control)
Após identificar quem é o usuário (Autenticação), precisamos garantir o que ele tem permissão para fazer (Autorização).
package middleware
import (
"net/http"
"slices"
)
// RequireRoles bloqueia o acesso se o usuário não possuir pelo menos um dos papéis informados
func RequireRoles(allowedRoles ...string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := UserFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"não autenticado"}`, http.StatusUnauthorized)
return
}
// Verifica se alguma role do usuário está na lista permitida
hasPermission := false
for _, role := range claims.Roles {
if slices.Contains(allowedRoles, role) {
hasPermission = true
break
}
}
if !hasPermission {
http.Error(w, `{"error":"acesso negado: permissões insuficientes"}`, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
Exemplo de Uso no Servidor:
mux := http.NewServeMux()
// Rota pública de login
mux.HandleFunc("POST /api/v1/login", handleLogin)
// Rota protegida: apenas usuários autenticados
pedidosHandler := http.HandlerFunc(handleListOrders)
mux.Handle("GET /api/v1/orders", middleware.Authenticate(tm)(pedidosHandler))
// Rota restrita: exige perfil de "admin"
adminHandler := http.HandlerFunc(handleDeleteDatabase)
protectedAdmin := middleware.Chain(
middleware.Authenticate(tm),
middleware.RequireRoles("admin"),
)
mux.Handle("DELETE /api/v1/admin/purge", protectedAdmin(adminHandler))
6. Estratégias de Revogação de Tokens e Refresh Tokens
Um dos maiores desafios conceituais de segurança em tokens stateless é a revogação: uma vez emitido, um token JWT é matematicamente válido até que seu prazo ExpiresAt expire.
Como resolver o problema de revogação em produção?
- Access Tokens de Curta Duração + Refresh Tokens:
- O Access Token (JWT) tem validade curta (ex: 15 minutos).
- O Refresh Token é um hash aleatório opaco de longa duração (ex: 7 dias) salvo no banco de dados com relação 1:1 com o usuário.
- Quando o Access Token expira, o cliente usa o Refresh Token para obter um novo par. Se o usuário for bloqueado ou fizer logout, basta revogar o Refresh Token no banco.
- Token Versioning (Invalidação Global de Sessões):
- Adicione um campo
token_version: intno banco do usuário e nas Claims do token. - Ao alterar senha ou forçar logout em todos os dispositivos, incremente
user.token_version = user.token_version + 1. - O middleware de autenticação verifica essa versão apenas se necessário ou em rotas críticas.
- Adicione um campo
- Blocklist em Memória / Redis:
- Ao deslogar antes do prazo, armazena-se o ID do token (
jti - JWT ID) em uma chave Redis com TTL igual ao tempo restante de vida do token.
- Ao deslogar antes do prazo, armazena-se o ID do token (
7. Testes de Integração com httptest
func TestAuthMiddlewareIntegration(t *testing.T) {
tm := auth.NewTokenManager("chave-de-teste-secreta-32-bytes!", "auth-service", time.Minute*10)
handler := middleware.Authenticate(tm)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, _ := middleware.UserFromContext(r.Context())
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Olá " + user.Email))
}))
// 1. Cenário sem Token -> 401
req := httptest.NewRequest(http.MethodGet, "/secure", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
// 2. Cenário com Token Válido -> 200
token, err := tm.GenerateAccessToken("usr_123", "joao@empresa.com", []string{"editor"})
require.NoError(t, err)
reqAuth := httptest.NewRequest(http.MethodGet, "/secure", nil)
reqAuth.Header.Set("Authorization", "Bearer "+token)
recAuth := httptest.NewRecorder()
handler.ServeHTTP(recAuth, reqAuth)
assert.Equal(t, http.StatusOK, recAuth.Code)
assert.Equal(t, "Olá joao@empresa.com", recAuth.Body.String())
}
8. Resumo Conceitual
- O JWT é estruturado em três partes:
Header.Payload.Signaturecodificadas em Base64URL. - HS256 utiliza chave secreta compartilhada; RS256/ES256 utiliza chaves pública e privada (ideal para arquiteturas de microsserviços distribuídos).
- Use
golang-jwt/jwt/v5e sempre valide explicitamente o método de assinatura (t.Method.(*jwt.SigningMethodHMAC)) para prevenir ataques de downgrade de algoritmo. - Armazene as claims no
context.Contextcom chaves privadas (type userCtxKey struct{}) para desacoplar a camada de transporte dos use cases da sua Clean Architecture em Go. - Implemente RBAC combinando middlewares em cadeia com o helper
Chain(). - Use Access Tokens curtos e Refresh Tokens persistidos para contornar o caráter stateless do JWT em cenários de revogação e encerramento de sessões.