Pular para o conteúdo

Trilha de Go · parte 24 de 37

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

Tamanho da fonte:
Largura da coluna:

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:

Fluxo de Autenticação e Autorização com JWT
figura 1 · Fluxo de Autenticação e Autorização com JWT

Etapas da Arquitetura:

  1. Login (Handshake Inicial): O cliente envia email e senha via POST /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.
  2. Armazenamento e Envio: O cliente armazena o token e o anexa a todas as requisições subsequentes no header HTTP Authorization: Bearer <token>.
  3. 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).
  4. Context Injection: O middleware extrai as Claims do token e as injeta no context.Context da requisição Go de forma fortemente tipada.
  5. 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:

Criptografia Simétrica vs Assimétrica
figura 2 · Criptografia Simétrica vs Assimétrica

Comparativo Arquitetural:

CritérioSimétrica (HS256 / HMAC-SHA256)Assimétrica (RS256 / ES256 - RSA/ECDSA)
ChavesUma única chave secreta compartilhadaPar: Chave Privada (Private Key) + Chave Pública (Public Key)
Quem assina?Quem possui o segredoSomente o Auth Service (com a chave privada)
Quem valida?Quem possui o segredoQualquer microsserviço (apenas com a chave pública)
Risco de VazamentoCrítico: se um microsserviço vazar a chave, ele pode forjar tokens válidosBaixo: expor a chave pública não permite forjar tokens
PerformanceExtremamente rápida (~10x mais rápida que RSA)Mais lenta para assinar/verificar
Cenário RecomendadoMonólitos e APIs fechadas com poucos serviçosEcossistemas 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?

  1. 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.
  2. Token Versioning (Invalidação Global de Sessões):
    • Adicione um campo token_version: int no 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.
  3. 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.

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.Signature codificadas 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/v5 e sempre valide explicitamente o método de assinatura (t.Method.(*jwt.SigningMethodHMAC)) para prevenir ataques de downgrade de algoritmo.
  • Armazene as claims no context.Context com 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.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?