Autenticação JWT com Go: Guia Prático
JWT (JSON Web Token) é o padrão mais usado para autenticação em APIs modernas. Com Go, implementar JWT é direto e performático. Neste tutorial, vamos construir um sistema completo de autenticação com login, tokens de acesso, refresh tokens e middleware de proteção de rotas.
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
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:
// auth/jwt.go
package auth
import (
"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
)
// 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
}
// Em produção, use pelo menos 32 caracteres aleatórios
return []byte(secret), 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: "auth-api",
// Para quem o token foi emitido
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: "auth-api",
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
}
// Faz o parse do token e valida a assinatura
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// Verifica se o algoritmo é o esperado (previne ataques de troca de algoritmo)
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("algoritmo inesperado: %v", token.Header["alg"])
}
return secret, nil
})
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 {
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) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("algoritmo inesperado: %v", token.Header["alg"])
}
return secret, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrTokenExpirado
}
return nil, ErrTokenInvalido
}
claims, ok := token.Claims.(*RefreshClaims)
if !ok || !token.Valid {
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{
"joao@email.com": {
ID: 1,
Nome: "João Silva",
Email: "joao@email.com",
// Senha: "senha123" hasheada com bcrypt
Senha: "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy",
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
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
}
}
// 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="minha-chave-secreta-super-segura-com-32-chars"
# 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":"joao@email.com","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}
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. Valide Sempre o Algoritmo
// SEMPRE verifique o algoritmo no callback de validação
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Isso previne ataques "alg: none"
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("algoritmo inesperado: %v", token.Header["alg"])
}
return secret, nil
})
4. Não Coloque Dados Sensíveis no Payload
O payload do JWT e codificado em Base64, nao e criptografado. Qualquer pessoa pode decodificar e ler o conteudo. Nunca inclua senhas, dados de cartao de credito ou informacoes 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 nao e criptografado
- Ignorar HTTPS: tokens podem ser interceptados em conexoes HTTP
- Hardcoded secrets no codigo: use variaveis de ambiente ou vault
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)
- Sempre valide o algoritmo no callback 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
Com esse sistema, sua API Go está pronta para autenticação em produção. O próximo passo é integrar com um banco de dados real e adicionar funcionalidades como controle de permissões (RBAC) e autenticação multi-fator (MFA).