JWT em Go: Autenticação Completa na Prática
Como usar JWT em Go (Golang)? Instale github.com/golang-jwt/jwt/v5, crie claims com jwt.RegisteredClaims, assine com jwt.NewWithClaims e valide com jwt.ParseWithClaims. Para HS256, use uma chave aleatória de pelo menos 32 bytes e configure jwt.WithValidMethods, jwt.WithExpirationRequired, jwt.WithIssuer e jwt.WithAudience. O exemplo abaixo separa access e refresh tokens e protege rotas com middleware net/http.
A biblioteca golang-jwt verifica a assinatura; sua aplicação define quais claims e algoritmos aceita. Essas opções seguem a documentação do parser v5 e as recomendações da RFC 8725, especialmente a separação entre tipos de token.
| Decisão | Escolha padrão | Quando mudar |
|---|---|---|
| Biblioteca | golang-jwt/jwt/v5 | Nunca use dgrijalva/jwt-go (arquivada) |
| Algoritmo | HS256 (segredo compartilhado) | RS256/EdDSA quando serviços diferentes só validam |
| Access token | 15 minutos neste exemplo | Ajuste à sensibilidade da API e à política de sessão |
| Refresh token | 7 dias, rotacionado | Sessão em servidor se precisa revogar na hora |
| Onde guardar no browser | Cookie HttpOnly + SameSite | Authorization: Bearer em mobile e integrações |
Neste tutorial vamos construir uma API didática com login, tokens de acesso, refresh tokens e middleware de proteção de rotas. O exemplo retorna tokens em JSON e não implementa cookies, rotação nem persistência de sessão. Antes de produção, adicione revogação, rotação de refresh tokens, rate limiting e armazenamento real de usuários; as seções finais explicam essas limitações.
O que é JWT e Como Funciona?
Um JWT é uma string composta por três partes separadas por pontos:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
|___ Header ___|.___ Payload ___|._______ Signature _______|
Header: algoritmo de assinatura e tipo do token
{
"alg": "HS256",
"typ": "JWT"
}
Payload: dados do usuário (claims)
{
"sub": "1234567890",
"nome": "João",
"admin": true,
"exp": 1700000000
}
Signature: garante que o token não foi alterado
HMAC-SHA256(base64(header) + "." + base64(payload), secret)
O fluxo funciona assim:
- Cliente envia credenciais (email + senha)
- Servidor valida e retorna um JWT
- Cliente envia o JWT no header
Authorization: Bearer <token>em cada requisição - Servidor valida o token e libera (ou bloqueia) o acesso
Setup do Projeto
mkdir auth-api
cd auth-api
go mod init auth-api
go get github.com/golang-jwt/jwt/v5
go get golang.org/x/crypto/bcrypt
Estrutura do projeto:
auth-api/
├── main.go
├── auth/
│ ├── jwt.go # Criação e validação de tokens
│ └── middleware.go # Middleware de autenticação
├── handler/
│ └── handler.go # Handlers HTTP
├── model/
│ └── user.go # Modelo de usuário
├── go.mod
└── go.sum
Modelo de Usuário
// model/user.go
package model
import "time"
// Usuario representa um usuário do sistema
type Usuario struct {
ID int64 `json:"id"`
Nome string `json:"nome"`
Email string `json:"email"`
Senha string `json:"-"` // Nunca retorna a senha no JSON
Ativo bool `json:"ativo"`
CriadoEm time.Time `json:"criado_em"`
}
// LoginRequest representa o corpo da requisição de login
type LoginRequest struct {
Email string `json:"email"`
Senha string `json:"senha"`
}
// TokenResponse é a resposta retornada após login bem-sucedido
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
// ErrorResponse padroniza respostas de erro
type ErrorResponse struct {
Erro string `json:"erro"`
Codigo int `json:"codigo"`
}
Criando e Validando Tokens JWT
Aqui está o coração do sistema de autenticação. Usamos aud: auth-api:access nas rotas protegidas e aud: auth-api:refresh na renovação: um refresh token não pode autenticar uma chamada à API, nem um access token pode renovar a sessão. Isso aplica as regras de validação mutuamente exclusivas da RFC 8725, seção 3.12.
exp é opcional por padrão na biblioteca; jwt.WithExpirationRequired() o torna obrigatório. jwt.WithIssuer e jwt.WithAudience exigem os respectivos claims. jwt.WithValidMethods([]string{"HS256"}) restringe o algoritmo exato, em vez de aceitar toda a família HMAC (HS256, HS384 e HS512).
// auth/jwt.go
package auth
import (
"encoding/base64"
"errors"
"fmt"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
// Claims personalizados que incluem dados do usuário
type Claims struct {
UsuarioID int64 `json:"usuario_id"`
Email string `json:"email"`
Nome string `json:"nome"`
jwt.RegisteredClaims
}
// RefreshClaims para o refresh token (dados mínimos)
type RefreshClaims struct {
UsuarioID int64 `json:"usuario_id"`
jwt.RegisteredClaims
}
// Durações dos tokens
const (
AccessTokenDuracao = 15 * time.Minute // Access token: 15 minutos
RefreshTokenDuracao = 7 * 24 * time.Hour // Refresh token: 7 dias
tokenIssuer = "auth-api"
accessAudience = "auth-api:access"
refreshAudience = "auth-api:refresh"
)
// Erros comuns de autenticação
var (
ErrTokenExpirado = errors.New("token expirado")
ErrTokenInvalido = errors.New("token inválido")
ErrSecretAusente = errors.New("JWT_SECRET não configurado")
)
// getSecret retorna a chave secreta para assinar tokens
func getSecret() ([]byte, error) {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
return nil, ErrSecretAusente
}
// JWT_SECRET contém Base64 de pelo menos 32 bytes aleatórios.
key, err := base64.StdEncoding.DecodeString(secret)
if err != nil || len(key) < 32 {
return nil, errors.New("JWT_SECRET deve conter Base64 de pelo menos 32 bytes")
}
return key, nil
}
// GerarAccessToken cria um novo access token para o usuário
func GerarAccessToken(usuarioID int64, email, nome string) (string, error) {
secret, err := getSecret()
if err != nil {
return "", err
}
agora := time.Now()
claims := Claims{
UsuarioID: usuarioID,
Email: email,
Nome: nome,
RegisteredClaims: jwt.RegisteredClaims{
// Emissor do token
Issuer: tokenIssuer,
Audience: jwt.ClaimStrings{accessAudience},
// Identidade do usuário
Subject: fmt.Sprintf("%d", usuarioID),
// Quando o token foi emitido
IssuedAt: jwt.NewNumericDate(agora),
// Quando o token expira
ExpiresAt: jwt.NewNumericDate(agora.Add(AccessTokenDuracao)),
// Não usar antes desta data
NotBefore: jwt.NewNumericDate(agora),
},
}
// Cria o token com o algoritmo HS256
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Assina o token com o secret
tokenString, err := token.SignedString(secret)
if err != nil {
return "", fmt.Errorf("erro ao assinar token: %w", err)
}
return tokenString, nil
}
// GerarRefreshToken cria um refresh token com vida longa
func GerarRefreshToken(usuarioID int64) (string, error) {
secret, err := getSecret()
if err != nil {
return "", err
}
agora := time.Now()
claims := RefreshClaims{
UsuarioID: usuarioID,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: tokenIssuer,
Audience: jwt.ClaimStrings{refreshAudience},
Subject: fmt.Sprintf("%d", usuarioID),
IssuedAt: jwt.NewNumericDate(agora),
ExpiresAt: jwt.NewNumericDate(agora.Add(RefreshTokenDuracao)),
NotBefore: jwt.NewNumericDate(agora),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}
// ValidarAccessToken verifica e decodifica um access token
func ValidarAccessToken(tokenString string) (*Claims, error) {
secret, err := getSecret()
if err != nil {
return nil, err
}
// Valida assinatura, algoritmo exato e claims obrigatórios.
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
}, jwt.WithValidMethods([]string{"HS256"}),
jwt.WithExpirationRequired(),
jwt.WithIssuer(tokenIssuer),
jwt.WithAudience(accessAudience))
if err != nil {
// Verifica se o erro é de expiração
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrTokenExpirado
}
return nil, ErrTokenInvalido
}
// Extrai os claims do token
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid || claims.UsuarioID <= 0 || claims.Subject != fmt.Sprintf("%d", claims.UsuarioID) {
return nil, ErrTokenInvalido
}
return claims, nil
}
// ValidarRefreshToken verifica e decodifica um refresh token
func ValidarRefreshToken(tokenString string) (*RefreshClaims, error) {
secret, err := getSecret()
if err != nil {
return nil, err
}
token, err := jwt.ParseWithClaims(tokenString, &RefreshClaims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
}, jwt.WithValidMethods([]string{"HS256"}),
jwt.WithExpirationRequired(),
jwt.WithIssuer(tokenIssuer),
jwt.WithAudience(refreshAudience))
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrTokenExpirado
}
return nil, ErrTokenInvalido
}
claims, ok := token.Claims.(*RefreshClaims)
if !ok || !token.Valid || claims.UsuarioID <= 0 || claims.Subject != fmt.Sprintf("%d", claims.UsuarioID) {
return nil, ErrTokenInvalido
}
return claims, nil
}
Middleware de Autenticação
O middleware intercepta requisições e verifica se o token é válido:
// auth/middleware.go
package auth
import (
"context"
"encoding/json"
"net/http"
"strings"
)
// Chave para armazenar claims no contexto da requisição
type contextKey string
const ClaimsContextKey contextKey = "claims"
// Middleware verifica o JWT em cada requisição protegida
func Middleware(proximo http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Extrai o token do header Authorization
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
responderErro(w, "Token de autenticação não fornecido", http.StatusUnauthorized)
return
}
// O formato esperado é: "Bearer <token>"
partes := strings.SplitN(authHeader, " ", 2)
if len(partes) != 2 || !strings.EqualFold(partes[0], "bearer") {
responderErro(w, "Formato do token inválido. Use: Bearer <token>", http.StatusUnauthorized)
return
}
tokenString := partes[1]
// Valida o token
claims, err := ValidarAccessToken(tokenString)
if err != nil {
switch err {
case ErrTokenExpirado:
responderErro(w, "Token expirado. Faça refresh ou login novamente", http.StatusUnauthorized)
default:
responderErro(w, "Token inválido", http.StatusUnauthorized)
}
return
}
// Adiciona os claims ao contexto da requisição
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
// Passa para o próximo handler com o contexto atualizado
proximo.ServeHTTP(w, r.WithContext(ctx))
})
}
// ClaimsDaRequisicao extrai os claims do contexto
func ClaimsDaRequisicao(r *http.Request) (*Claims, bool) {
claims, ok := r.Context().Value(ClaimsContextKey).(*Claims)
return claims, ok
}
// responderErro envia uma resposta de erro padronizada
func responderErro(w http.ResponseWriter, mensagem string, statusCode int) {
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"erro": mensagem,
"codigo": statusCode,
})
}
Handlers HTTP: Login, Refresh e Rotas Protegidas
// handler/handler.go
package handler
import (
"encoding/json"
"net/http"
"auth-api/auth"
"auth-api/model"
"golang.org/x/crypto/bcrypt"
)
// Simulação de banco de dados em memória
var usuarios = map[string]model.Usuario{
"[email protected]": {
ID: 1,
Nome: "João Silva",
Email: "[email protected]",
// Senha: "senha123" hasheada com bcrypt
Senha: "$2a$10$dGN5xku2zDyrkeKZagodWOw1xVD.AsAUI0nGYRfN4dHh5X3nkCphO",
Ativo: true,
},
}
// Login autentica o usuário e retorna os tokens
func Login(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Aceita apenas POST
if r.Method != http.MethodPost {
http.Error(w, `{"erro":"método não permitido"}`, http.StatusMethodNotAllowed)
return
}
// Decodifica o corpo da requisição
var req model.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Corpo da requisição inválido",
Codigo: http.StatusBadRequest,
})
return
}
// Validação básica
if req.Email == "" || req.Senha == "" {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Email e senha são obrigatórios",
Codigo: http.StatusBadRequest,
})
return
}
// Busca o usuário (em produção, busque no banco de dados)
usuario, existe := usuarios[req.Email]
if !existe {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Credenciais inválidas",
Codigo: http.StatusUnauthorized,
})
return
}
// Verifica a senha com bcrypt e recusa usuários inativos
if !usuario.Ativo {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(model.ErrorResponse{Erro: "Credenciais inválidas", Codigo: http.StatusUnauthorized})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(usuario.Senha), []byte(req.Senha)); err != nil {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Credenciais inválidas",
Codigo: http.StatusUnauthorized,
})
return
}
// Gera os tokens
accessToken, err := auth.GerarAccessToken(usuario.ID, usuario.Email, usuario.Nome)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Erro ao gerar token",
Codigo: http.StatusInternalServerError,
})
return
}
refreshToken, err := auth.GerarRefreshToken(usuario.ID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Erro ao gerar refresh token",
Codigo: http.StatusInternalServerError,
})
return
}
// Retorna os tokens
resp := model.TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: 900, // 15 minutos em segundos
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}
// Refresh gera um novo access token usando o refresh token
func Refresh(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
http.Error(w, `{"erro":"método não permitido"}`, http.StatusMethodNotAllowed)
return
}
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Corpo da requisição inválido",
Codigo: http.StatusBadRequest,
})
return
}
// Valida o refresh token
claims, err := auth.ValidarRefreshToken(req.RefreshToken)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Refresh token inválido ou expirado",
Codigo: http.StatusUnauthorized,
})
return
}
// Busca o usuário para obter dados atualizados
// Em produção, busque no banco pelo ID
var usuario model.Usuario
for _, u := range usuarios {
if u.ID == claims.UsuarioID {
usuario = u
break
}
}
// Não renove a sessão de um usuário inexistente ou desativado.
if usuario.ID == 0 || !usuario.Ativo {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(model.ErrorResponse{Erro: "Refresh token inválido", Codigo: http.StatusUnauthorized})
return
}
// Gera novo access token
novoAccessToken, err := auth.GerarAccessToken(usuario.ID, usuario.Email, usuario.Nome)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Erro ao gerar novo token",
Codigo: http.StatusInternalServerError,
})
return
}
json.NewEncoder(w).Encode(model.TokenResponse{
AccessToken: novoAccessToken,
TokenType: "Bearer",
ExpiresIn: 900,
})
}
// Perfil retorna os dados do usuário autenticado (rota protegida)
func Perfil(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Extrai os claims do contexto (populados pelo middleware)
claims, ok := auth.ClaimsDaRequisicao(r)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(model.ErrorResponse{
Erro: "Não autenticado",
Codigo: http.StatusUnauthorized,
})
return
}
// Retorna dados do usuário
json.NewEncoder(w).Encode(map[string]interface{}{
"usuario_id": claims.UsuarioID,
"nome": claims.Nome,
"email": claims.Email,
"mensagem": "Bem-vindo à área protegida!",
})
}
Juntando Tudo: main.go
// main.go
package main
import (
"log"
"net/http"
"auth-api/auth"
"auth-api/handler"
)
func main() {
mux := http.NewServeMux()
// ========================================
// Rotas públicas (sem autenticação)
// ========================================
mux.HandleFunc("/login", handler.Login)
mux.HandleFunc("/refresh", handler.Refresh)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
// ========================================
// Rotas protegidas (com JWT middleware)
// ========================================
rotasProtegidas := http.NewServeMux()
rotasProtegidas.HandleFunc("/perfil", handler.Perfil)
rotasProtegidas.HandleFunc("/dashboard", func(w http.ResponseWriter, r *http.Request) {
claims, _ := auth.ClaimsDaRequisicao(r)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"mensagem":"Dados do dashboard para ` + claims.Nome + `"}`))
})
// Aplica o middleware de autenticação nas rotas protegidas
mux.Handle("/api/", http.StripPrefix("/api", auth.Middleware(rotasProtegidas)))
log.Println("Servidor rodando em http://localhost:8080")
log.Println("Rotas disponíveis:")
log.Println(" POST /login - Autenticação")
log.Println(" POST /refresh - Renovar token")
log.Println(" GET /api/perfil - Perfil (protegida)")
log.Println(" GET /api/dashboard - Dashboard (protegida)")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal("Erro ao iniciar servidor:", err)
}
}
Testando a API
# 1. Definir o secret (obrigatório!)
export JWT_SECRET="$(openssl rand -base64 32)"
# 2. Iniciar o servidor
go run main.go
# 3. Fazer login
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","senha":"senha123"}'
# Resposta:
# {
# "access_token": "eyJhbGciOiJIUzI1...",
# "refresh_token": "eyJhbGciOiJIUzI1...",
# "token_type": "Bearer",
# "expires_in": 900
# }
# 4. Acessar rota protegida
curl http://localhost:8080/api/perfil \
-H "Authorization: Bearer eyJhbGciOiJIUzI1..."
# 5. Renovar token
curl -X POST http://localhost:8080/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"eyJhbGciOiJIUzI1..."}'
# 6. Testar sem token (deve retornar 401)
curl http://localhost:8080/api/perfil
# {"erro":"Token de autenticação não fornecido","codigo":401}
Teste a Separação de Access e Refresh Tokens
Salve em auth/jwt_test.go e rode go test ./.... A chave fixa abaixo existe somente no teste; nunca a use no servidor.
// auth/jwt_test.go
package auth
import (
"encoding/base64"
"strings"
"testing"
)
func TestSeparacaoDosTokens(t *testing.T) {
t.Setenv("JWT_SECRET", base64.StdEncoding.EncodeToString([]byte(strings.Repeat("k", 32))))
access, err := GerarAccessToken(1, "[email protected]", "João")
if err != nil {
t.Fatal(err)
}
refresh, err := GerarRefreshToken(1)
if err != nil {
t.Fatal(err)
}
if _, err := ValidarAccessToken(access); err != nil {
t.Fatalf("access válido rejeitado: %v", err)
}
if _, err := ValidarRefreshToken(refresh); err != nil {
t.Fatalf("refresh válido rejeitado: %v", err)
}
if _, err := ValidarAccessToken(refresh); err == nil {
t.Fatal("refresh aceito como access token")
}
if _, err := ValidarRefreshToken(access); err == nil {
t.Fatal("access aceito como refresh token")
}
}
Amplie a suíte com tokens sem exp, expirados, assinados com outra chave, com iss ou aud incorretos e com HS384 no lugar de HS256. Todos devem ser rejeitados; token expirado deve retornar ErrTokenExpirado.
Estratégia de Refresh Token
A ideia do refresh token é manter o usuário logado sem expor o access token por muito tempo:
┌──────────┐ ┌──────────┐
│ Cliente │ │ Servidor │
└─────┬────┘ └────┬─────┘
│ │
│ POST /login (email+senha) │
│─────────────────────────────>│
│ │
│ access_token (15min) │
│ refresh_token (7 dias) │
│<─────────────────────────────│
│ │
│ GET /api/dados │
│ Authorization: Bearer <AT> │
│─────────────────────────────>│
│ 200 OK │
│<─────────────────────────────│
│ │
│ ... 15 minutos depois ... │
│ │
│ GET /api/dados │
│ Authorization: Bearer <AT> │
│─────────────────────────────>│
│ 401 Token expirado │
│<─────────────────────────────│
│ │
│ POST /refresh (refresh_token)│
│─────────────────────────────>│
│ Novo access_token (15min) │
│<─────────────────────────────│
│ │
Boas Práticas de Segurança
1. Use Secrets Fortes
# Gere uma chave aleatória com OpenSSL
openssl rand -base64 32
# Resultado: K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
2. Nunca Armazene Tokens no localStorage
No frontend, prefira cookies HttpOnly:
// Definir token como cookie HttpOnly (mais seguro que localStorage)
http.SetCookie(w, &http.Cookie{
Name: "access_token",
Value: accessToken,
HttpOnly: true, // JavaScript não pode acessar
Secure: true, // Apenas HTTPS
SameSite: http.SameSiteStrictMode,
Path: "/",
MaxAge: 900, // 15 minutos
})
3. Restrinja o Algoritmo e Exija Claims
No exemplo, os dois parsers usam jwt.WithValidMethods([]string{"HS256"}), jwt.WithExpirationRequired(), jwt.WithIssuer(tokenIssuer) e uma audience específica. Não basta verificar se o método é *jwt.SigningMethodHMAC: isso também aceita HS384 e HS512.
A biblioteca rejeita alg: none por padrão, a menos que o programa forneça explicitamente jwt.UnsafeAllowNoneSignatureType como chave. Não habilite essa opção para autenticação. A lista de algoritmos continua necessária para limitar a validação ao contrato definido pela aplicação.
4. Não Coloque Dados Sensíveis no Payload
O payload do JWT é codificado em Base64URL, não é criptografado. Qualquer pessoa pode decodificar e ler o conteúdo. Nunca inclua senhas, dados de cartão de crédito ou informações confidenciais.
5. Implemente Blacklist para Logout
// Em produção, use Redis para armazenar tokens revogados
var tokensRevogados = make(map[string]bool)
func Logout(w http.ResponseWriter, r *http.Request) {
// Extrai o token do header
tokenString := extrairToken(r)
// Adiciona à blacklist (em produção, use Redis com TTL)
tokensRevogados[tokenString] = true
json.NewEncoder(w).Encode(map[string]string{
"mensagem": "Logout realizado com sucesso",
})
}
// No middleware, verifique a blacklist antes de validar
func estaRevogado(token string) bool {
return tokensRevogados[token]
}
Armadilhas Comuns
- Usar
HS256com chave fraca: use pelo menos 32 bytes aleatórios - Não verificar o algoritmo: permite ataques de troca de algoritmo
- Tokens sem expiração: sempre defina
expe mantenha curto (15-30 min) - Armazenar dados sensíveis no payload: o JWT não é criptografado
- Ignorar HTTPS: tokens podem ser interceptados em conexões HTTP
- Segredos fixos no código: use variáveis de ambiente ou um cofre de segredos
- Usar
dgrijalva/jwt-go: a biblioteca está arquivada; migre paragolang-jwt/jwt/v5
Conclusão
JWT com Go é uma combinação eficiente e segura para autenticação de APIs. Os pontos essenciais:
- Use a biblioteca
golang-jwt/jwt/v5(mantida e atualizada) - Access tokens curtos (15 min) + refresh tokens longos (7 dias)
- Restrinja o algoritmo e exija
exp,isseaudnas opções de parse - Middleware centraliza a lógica de autenticação
- Nunca armazene dados sensíveis no payload do JWT
- Use HTTPS em produção e cookies HttpOnly quando possível
- Implemente blacklist para suportar logout
Esse sistema é uma base de estudo, não uma implementação pronta para produção. O refresh token pode ser reutilizado até expirar, pois o handler não o rotaciona nem consulta um registro de sessão revogada. Em produção, persista a sessão, revogue refresh tokens no logout e faça rotação com detecção de reutilização. O exemplo de denylist acima é apenas ilustrativo: um mapa global sem sincronização não é seguro para handlers concorrentes e não funciona entre réplicas.
Se usar cookies, adapte o handler para lê-los e configure proteção contra CSRF: HttpOnly impede a leitura do cookie por JavaScript, mas não impede o navegador de enviá-lo em requisições. Siga o guia de CSRF e SameSite em Go antes de mudar o transporte.
Perguntas frequentes
Qual biblioteca JWT usar em Go?
Use github.com/golang-jwt/jwt/v5. Ela é a continuação mantida da antiga dgrijalva/jwt-go, que está arquivada e não recebe correções de segurança.
JWT é criptografado?
Não. O payload de um JWT assinado é apenas Base64URL e pode ser lido por qualquer um que tenha o token. A assinatura garante integridade, não confidencialidade.
Como fazer logout com JWT?
JWT é stateless, então o token continua válido até expirar. Para logout imediato, mantenha uma denylist em Redis com TTL igual ao tempo restante do token, ou use access tokens muito curtos e revogue o refresh token no banco.
JWT ou sessão com cookie?
Sessão em servidor é mais simples e revoga na hora — boa escolha para aplicação web de um domínio só. JWT compensa quando vários serviços precisam validar o usuário sem consultar estado central.
Referências técnicas
- golang-jwt/jwt/v5 no pkg.go.dev: API de criação, parsing, claims e opções de validação.
- Documentação de validação do golang-jwt: configuração do parser e claims obrigatórios.
- RFC 8725 — JWT Best Current Practices: algoritmos (§3.1), chaves com entropia suficiente (§3.5), emissor (§3.8), audience (§3.9) e separação de tipos de token (§3.12).
- RFC 7518, seção 3.2: chave HMAC de tamanho igual ou superior ao tamanho da saída do hash, ou seja, pelo menos 256 bits para HS256.
Próximos passos
- Autorização por papéis — depois de autenticar, decida o que cada usuário pode fazer: autenticação e autorização em APIs Go.
- Login social e SSO — para delegar a identidade ao Google ou Keycloak, veja OAuth 2.0 e OpenID Connect em Go.
- Middleware bem estruturado — organize o encadeamento de handlers com o guia de middleware em Go.
- CORS e cookies entre domínios — se o frontend está em outro domínio, configure CORS em Go e proteja formulários com CSRF e SameSite.
- Proteja o endpoint de login — aplique rate limiting na API para conter ataques de força bruta.
- Persistência real — troque o mapa em memória por PostgreSQL seguindo Go com PostgreSQL.
- Testes — cubra os handlers e o middleware com testes em Go e
httptest. - Checklist de produção — antes do deploy, passe pelo checklist de API Go em produção e pela API REST em Go.
Procurando vaga backend? Veja as vagas Go no Brasil e as perguntas de entrevista Go — autenticação é tema recorrente.