Janeiro 2026 · ~16 min

JWT em Go: Autenticação Completa na Prática

Aprenda JWT em Go com golang-jwt/v5: gere e valide tokens HS256, exija expiração, issuer e audience e proteja rotas com middleware. Código e testes práticos.

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ãoEscolha padrãoQuando mudar
Bibliotecagolang-jwt/jwt/v5Nunca use dgrijalva/jwt-go (arquivada)
AlgoritmoHS256 (segredo compartilhado)RS256/EdDSA quando serviços diferentes só validam
Access token15 minutos neste exemploAjuste à sensibilidade da API e à política de sessão
Refresh token7 dias, rotacionadoSessão em servidor se precisa revogar na hora
Onde guardar no browserCookie HttpOnly + SameSiteAuthorization: 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:

  1. Cliente envia credenciais (email + senha)
  2. Servidor valida e retorna um JWT
  3. Cliente envia o JWT no header Authorization: Bearer <token> em cada requisição
  4. 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

  1. Usar HS256 com chave fraca: use pelo menos 32 bytes aleatórios
  2. Não verificar o algoritmo: permite ataques de troca de algoritmo
  3. Tokens sem expiração: sempre defina exp e mantenha curto (15-30 min)
  4. Armazenar dados sensíveis no payload: o JWT não é criptografado
  5. Ignorar HTTPS: tokens podem ser interceptados em conexões HTTP
  6. Segredos fixos no código: use variáveis de ambiente ou um cofre de segredos
  7. Usar dgrijalva/jwt-go: a biblioteca está arquivada; migre para golang-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, iss e aud nas 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.

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

Próximos passos

  1. Autorização por papéis — depois de autenticar, decida o que cada usuário pode fazer: autenticação e autorização em APIs Go.
  2. Login social e SSO — para delegar a identidade ao Google ou Keycloak, veja OAuth 2.0 e OpenID Connect em Go.
  3. Middleware bem estruturado — organize o encadeamento de handlers com o guia de middleware em Go.
  4. 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.
  5. Proteja o endpoint de login — aplique rate limiting na API para conter ataques de força bruta.
  6. Persistência real — troque o mapa em memória por PostgreSQL seguindo Go com PostgreSQL.
  7. Testes — cubra os handlers e o middleware com testes em Go e httptest.
  8. 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.

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. Se um projeto antigo ainda importa dgrijalva/jwt-go, migrar para golang-jwt/jwt/v5 deve ser a primeira tarefa.

Como validar um JWT em Go com segurança?

Use jwt.ParseWithClaims com jwt.WithValidMethods para aceitar apenas o algoritmo esperado, jwt.WithExpirationRequired, jwt.WithIssuer e jwt.WithAudience. Separe as audiences de access e refresh tokens e confira a identidade antes de autorizar. golang-jwt/v5 já rejeita alg: none por padrão; a lista de algoritmos restringe o contrato da aplicação.

Qual o tempo de expiração ideal de um token JWT?

Access token curto (5 a 15 minutos) e refresh token longo (7 a 30 dias). O access token curto limita o estrago de um vazamento; o refresh token, guardado em cookie HttpOnly e rotacionado a cada uso, mantém a sessão sem exigir novo login.

JWT é criptografado?

Não. O payload de um JWT assinado é apenas Base64URL: qualquer pessoa com o token consegue ler o conteúdo. A assinatura garante integridade, não confidencialidade. Nunca coloque senha, CPF, dados de cartão ou segredos no payload.

Como fazer logout com JWT em Go?

JWT é stateless, então o token continua válido até expirar. Para logout imediato mantenha uma denylist (Redis, com TTL igual ao tempo restante do token) consultada pelo middleware, ou use apenas access tokens muito curtos e revogue o refresh token no banco.

JWT ou sessão com cookie: qual escolher?

Sessão em servidor é mais simples e revoga na hora, ideal para aplicação web de um domínio só. JWT compensa quando há vários serviços que precisam validar o usuário sem consultar um estado central, como APIs públicas, mobile e microsserviços.