Go Admin

Autenticação JWT em APIs Go: Geração, Validação e Refresh Tokens: Do Básico ao Avançado Já leu

Entendendo JWT: Fundamentos e Estrutura JSON Web Token (JWT) é um padrão aberto (RFC 7519) que define um método compacto e autossuficiente para transmitir informações entre partes de forma segura. Um JWT é composto por três partes separadas por pontos: header, payload e signature. Quando você vê um token como , cada seção representa um segmento diferente, todos codificados em Base64URL. A beleza do JWT está em sua natureza stateless. Diferentemente de sessões tradicionais, o servidor não precisa armazenar informações sobre o token no banco de dados. Tudo que o servidor precisa saber está contido no próprio token, assinado criptograficamente para garantir que não foi alterado. Isso torna JWTs ideais para APIs escaláveis, especialmente em arquiteturas de microsserviços onde múltiplos servidores precisam validar credenciais sem compartilhar estado. Estrutura e Componentes O header contém informações sobre o tipo de token e o algoritmo de assinatura usado. Por exemplo: . O payload (também chamado de claims) carrega os dados reais, como ID

Entendendo JWT: Fundamentos e Estrutura

JSON Web Token (JWT) é um padrão aberto (RFC 7519) que define um método compacto e autossuficiente para transmitir informações entre partes de forma segura. Um JWT é composto por três partes separadas por pontos: header, payload e signature. Quando você vê um token como eyJhbGc...eyJzdWI...SflKxw..., cada seção representa um segmento diferente, todos codificados em Base64URL.

A beleza do JWT está em sua natureza stateless. Diferentemente de sessões tradicionais, o servidor não precisa armazenar informações sobre o token no banco de dados. Tudo que o servidor precisa saber está contido no próprio token, assinado criptograficamente para garantir que não foi alterado. Isso torna JWTs ideais para APIs escaláveis, especialmente em arquiteturas de microsserviços onde múltiplos servidores precisam validar credenciais sem compartilhar estado.

Estrutura e Componentes

O header contém informações sobre o tipo de token e o algoritmo de assinatura usado. Por exemplo: {"alg":"HS256","typ":"JWT"}. O payload (também chamado de claims) carrega os dados reais, como ID do usuário, email e permissões. O signature é gerado combinando o header e payload codificados com uma chave secreta, garantindo integridade. Nenhuma dessas partes é criptografada — apenas assinada — então nunca coloque informações sensíveis como senhas no payload.

Implementando JWT em Go: Geração de Tokens

Para trabalhar com JWT em Go, usaremos a biblioteca github.com/golang-jwt/jwt/v5, que é a sucessora mantida do projeto original jwt-go. Ela oferece uma API limpa e suporta todos os algoritmos de assinatura principais. Vamos criar uma estrutura de autenticação robusta do zero.

Configuração Inicial e Tipos

Primeiro, definiremos os tipos e configurações que usaremos em toda a aplicação:

package auth

import (
    "errors"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

// Config mantém as configurações de JWT
type Config struct {
    SecretKey       string
    AccessDuration  time.Duration
    RefreshDuration time.Duration
}

// Claims define as informações contidas no JWT
type Claims struct {
    UserID   int    `json:"user_id"`
    Email    string `json:"email"`
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

// TokenResponse é o retorno quando geramos um token
type TokenResponse struct {
    AccessToken  string `json:"access_token"`
    RefreshToken string `json:"refresh_token"`
    ExpiresIn    int64  `json:"expires_in"`
    TokenType    string `json:"token_type"`
}

var ErrInvalidToken = errors.New("token inválido ou expirado")
var ErrExpiredToken = errors.New("token expirado")

Gerando Access Tokens

Agora implementaremos a função que gera o access token. Este token é de curta duração (tipicamente 15 minutos) e contém os dados do usuário:

package auth

import (
    "fmt"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

// Manager gerencia operações com JWT
type Manager struct {
    config Config
}

// NewManager cria uma nova instância do gerenciador
func NewManager(config Config) *Manager {
    return &Manager{config: config}
}

// GenerateAccessToken cria um novo access token
func (m *Manager) GenerateAccessToken(userID int, email, username, role string) (string, error) {
    now := time.Now()
    expiresAt := now.Add(m.config.AccessDuration)

    claims := Claims{
        UserID:   userID,
        Email:    email,
        Username: username,
        Role:     role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expiresAt),
            IssuedAt:  jwt.NewNumericDate(now),
            NotBefore: jwt.NewNumericDate(now),
            Issuer:    "api-app",
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(m.config.SecretKey))
    if err != nil {
        return "", fmt.Errorf("erro ao assinar token: %w", err)
    }

    return tokenString, nil
}

O RegisteredClaims do JWT contém campos padrão como ExpiresAt (quando expira), IssuedAt (quando foi criado) e Issuer (quem emitiu). O algoritmo HS256 (HMAC SHA256) é simétrico — a mesma chave secreta assina e valida.

Validação e Parsing de Tokens

Validar um token é tão importante quanto gerá-lo. Precisamos garantir que o token não foi falsificado e que ainda está válido. Go fornece ferramentas robustas para isso através do método ParseWithClaims.

Função de Validação Completa

package auth

import (
    "fmt"

    "github.com/golang-jwt/jwt/v5"
)

// ValidateToken valida e extrai claims de um token
func (m *Manager) ValidateToken(tokenString string) (*Claims, error) {
    claims := &Claims{}

    token, err := jwt.ParseWithClaims(
        tokenString,
        claims,
        func(token *jwt.Token) (interface{}, error) {
            // Verificar se o algoritmo é o esperado (prevenção contra 'none' algorithm attack)
            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("método de assinatura inesperado: %v", token.Header["alg"])
            }
            return []byte(m.config.SecretKey), nil
        },
    )

    if err != nil {
        return nil, fmt.Errorf("erro ao fazer parse do token: %w", err)
    }

    if !token.Valid {
        return nil, ErrInvalidToken
    }

    // Verificar expiração explicitamente (ParseWithClaims já faz, mas ser explícito é bom)
    if claims.ExpiresAt != nil && claims.ExpiresAt.Before(time.Now()) {
        return nil, ErrExpiredToken
    }

    return claims, nil
}

A verificação if _, ok := token.Method.(*jwt.SigningMethodHMAC) é crucial para segurança. Protege contra o ataque "algorithm substitution" onde um atacante tenta usar alg: "none" para pular a validação. Assim garantimos que apenas HMAC é aceito.

Middleware de Autenticação HTTP

Em uma API real, você precisará validar tokens em cada requisição. Aqui está um middleware padrão:

package auth

import (
    "net/http"
    "strings"
)

// AuthMiddleware valida o JWT em requisições HTTP
func (m *Manager) AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Extrair o token do header Authorization
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "header Authorization ausente", http.StatusUnauthorized)
            return
        }

        // Bearer token format: "Bearer <token>"
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            http.Error(w, "formato de Authorization inválido", http.StatusUnauthorized)
            return
        }

        tokenString := parts[1]
        claims, err := m.ValidateToken(tokenString)
        if err != nil {
            http.Error(w, "token inválido: "+err.Error(), http.StatusUnauthorized)
            return
        }

        // Você pode armazenar claims no contexto para uso posterior
        // ctx := context.WithValue(r.Context(), "claims", claims)
        // next.ServeHTTP(w, r.WithContext(ctx))

        next.ServeHTTP(w, r)
    })
}

Refresh Tokens e Rotação de Credenciais

Access tokens de curta duração melhoram a segurança, mas exigem que usuários façam login novamente frequentemente, prejudicando a experiência. A solução é usar refresh tokens — tokens de longa duração que só servem para obter novos access tokens. Esse padrão permite que o access token seja invalidado no servidor sem deslogar o usuário.

Gerando Refresh Tokens

Um refresh token é gerado junto com o access token, tem duração muito maior (dias ou semanas) e não contém dados sensíveis. Você deve armazená-lo no banco de dados para poder revogá-lo:

package auth

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "time"
)

// RefreshTokenData estrutura para armazenar refresh tokens no BD
type RefreshTokenData struct {
    ID        string    `db:"id"`
    UserID    int       `db:"user_id"`
    Token     string    `db:"token"`
    ExpiresAt time.Time `db:"expires_at"`
    CreatedAt time.Time `db:"created_at"`
    Revoked   bool      `db:"revoked"`
}

// GenerateTokenPair cria um access token e um refresh token
func (m *Manager) GenerateTokenPair(userID int, email, username, role string) (*TokenResponse, error) {
    // Gerar access token
    accessToken, err := m.GenerateAccessToken(userID, email, username, role)
    if err != nil {
        return nil, err
    }

    // Gerar refresh token (string aleatória segura)
    refreshToken, err := generateRandomToken(32)
    if err != nil {
        return nil, fmt.Errorf("erro ao gerar refresh token: %w", err)
    }

    expiresIn := int64(m.config.AccessDuration.Seconds())

    return &TokenResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        ExpiresIn:    expiresIn,
        TokenType:    "Bearer",
    }, nil
}

// generateRandomToken cria um token seguro aleatório
func generateRandomToken(length int) (string, error) {
    b := make([]byte, length)
    _, err := rand.Read(b)
    if err != nil {
        return "", err
    }
    return hex.EncodeToString(b), nil
}

Validação e Renovação de Refresh Tokens

O refresh token deve ser validado no banco de dados (verificar se foi revogado) e então um novo access token é emitido:

package auth

import (
    "database/sql"
    "fmt"
    "time"
)

// TokenStore interface para operações com tokens no BD (você implementa)
type TokenStore interface {
    SaveRefreshToken(data *RefreshTokenData) error
    GetRefreshToken(token string) (*RefreshTokenData, error)
    RevokeRefreshToken(token string) error
    DeleteExpiredTokens() error
}

// RefreshAccessToken valida um refresh token e emite um novo access token
func (m *Manager) RefreshAccessToken(refreshToken string, store TokenStore) (string, error) {
    // Buscar no banco de dados
    tokenData, err := store.GetRefreshToken(refreshToken)
    if err != nil {
        if err == sql.ErrNoRows {
            return "", ErrInvalidToken
        }
        return "", fmt.Errorf("erro ao buscar refresh token: %w", err)
    }

    // Verificar se foi revogado
    if tokenData.Revoked {
        return "", fmt.Errorf("refresh token foi revogado")
    }

    // Verificar expiração
    if time.Now().After(tokenData.ExpiresAt) {
        return "", ErrExpiredToken
    }

    // Emitir novo access token
    newAccessToken, err := m.GenerateAccessToken(
        tokenData.UserID,
        "", // você buscaria email/username do BD
        "",
        "",
    )
    if err != nil {
        return "", err
    }

    return newAccessToken, nil
}

// RevokeToken marca um refresh token como revogado (logout)
func (m *Manager) RevokeToken(refreshToken string, store TokenStore) error {
    return store.RevokeRefreshToken(refreshToken)
}

Exemplo de Implementação do TokenStore com SQLite

Para completar o quadro, aqui está uma implementação real com SQLite:

package auth

import (
    "database/sql"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

type SQLiteTokenStore struct {
    db *sql.DB
}

func NewSQLiteTokenStore(dbPath string) (*SQLiteTokenStore, error) {
    db, err := sql.Open("sqlite3", dbPath)
    if err != nil {
        return nil, err
    }

    // Criar tabela se não existir
    schema := `
    CREATE TABLE IF NOT EXISTS refresh_tokens (
        id TEXT PRIMARY KEY,
        user_id INTEGER NOT NULL,
        token TEXT UNIQUE NOT NULL,
        expires_at TIMESTAMP NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        revoked BOOLEAN DEFAULT FALSE
    );
    `
    if _, err := db.Exec(schema); err != nil {
        return nil, err
    }

    return &SQLiteTokenStore{db: db}, nil
}

func (s *SQLiteTokenStore) SaveRefreshToken(data *RefreshTokenData) error {
    _, err := s.db.Exec(
        `INSERT INTO refresh_tokens (id, user_id, token, expires_at, created_at, revoked)
         VALUES (?, ?, ?, ?, ?, ?)`,
        data.ID, data.UserID, data.Token, data.ExpiresAt, data.CreatedAt, data.Revoked,
    )
    return err
}

func (s *SQLiteTokenStore) GetRefreshToken(token string) (*RefreshTokenData, error) {
    data := &RefreshTokenData{}
    err := s.db.QueryRow(
        `SELECT id, user_id, token, expires_at, created_at, revoked 
         FROM refresh_tokens WHERE token = ?`,
        token,
    ).Scan(&data.ID, &data.UserID, &data.Token, &data.ExpiresAt, &data.CreatedAt, &data.Revoked)

    if err != nil {
        return nil, err
    }
    return data, nil
}

func (s *SQLiteTokenStore) RevokeRefreshToken(token string) error {
    _, err := s.db.Exec(
        `UPDATE refresh_tokens SET revoked = TRUE WHERE token = ?`,
        token,
    )
    return err
}

func (s *SQLiteTokenStore) DeleteExpiredTokens() error {
    _, err := s.db.Exec(
        `DELETE FROM refresh_tokens WHERE expires_at < ?`,
        time.Now(),
    )
    return err
}

Fluxo Completo: Autenticação com Login e Refresh

Colocando tudo junto, aqui está um fluxo realista de autenticação em uma API HTTP:

Handlers de Login e Refresh

package handlers

import (
    "encoding/json"
    "net/http"
    "your-module/auth"
    "crypto/sha256"
    "encoding/hex"
)

type LoginRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

type RefreshRequest struct {
    RefreshToken string `json:"refresh_token"`
}

type AuthHandler struct {
    authManager *auth.Manager
    tokenStore  auth.TokenStore
    userStore   UserStore // você implementa
}

// UserStore interface (exemplo)
type UserStore interface {
    GetUserByEmail(email string) (*User, error)
}

type User struct {
    ID       int
    Email    string
    Username string
    Password string // hash
    Role     string
}

// HandleLogin autentica usuário e retorna token pair
func (h *AuthHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "método não permitido", http.StatusMethodNotAllowed)
        return
    }

    var req LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "request inválido", http.StatusBadRequest)
        return
    }

    // Buscar usuário
    user, err := h.userStore.GetUserByEmail(req.Email)
    if err != nil {
        http.Error(w, "credenciais inválidas", http.StatusUnauthorized)
        return
    }

    // Verificar senha (exemplo simplificado)
    if !verifyPassword(req.Password, user.Password) {
        http.Error(w, "credenciais inválidas", http.StatusUnauthorized)
        return
    }

    // Gerar tokens
    tokenPair, err := h.authManager.GenerateTokenPair(user.ID, user.Email, user.Username, user.Role)
    if err != nil {
        http.Error(w, "erro ao gerar tokens", http.StatusInternalServerError)
        return
    }

    // Salvar refresh token no BD
    tokenData := &auth.RefreshTokenData{
        ID:        generateID(),
        UserID:    user.ID,
        Token:     tokenPair.RefreshToken,
        ExpiresAt: time.Now().AddDate(0, 0, 7), // 7 dias
        CreatedAt: time.Now(),
        Revoked:   false,
    }
    if err := h.tokenStore.SaveRefreshToken(tokenData); err != nil {
        http.Error(w, "erro ao salvar token", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(tokenPair)
}

// HandleRefresh renova o access token usando refresh token
func (h *AuthHandler) HandleRefresh(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "método não permitido", http.StatusMethodNotAllowed)
        return
    }

    var req RefreshRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "request inválido", http.StatusBadRequest)
        return
    }

    // Renovar access token
    newAccessToken, err := h.authManager.RefreshAccessToken(req.RefreshToken, h.tokenStore)
    if err != nil {
        http.Error(w, "refresh token inválido: "+err.Error(), http.StatusUnauthorized)
        return
    }

    response := map[string]string{
        "access_token": newAccessToken,
        "token_type":   "Bearer",
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

// HandleLogout revoga o refresh token
func (h *AuthHandler) HandleLogout(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "método não permitido", http.StatusMethodNotAllowed)
        return
    }

    var req RefreshRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "request inválido", http.StatusBadRequest)
        return
    }

    if err := h.authManager.RevokeToken(req.RefreshToken, h.tokenStore); err != nil {
        http.Error(w, "erro ao logout", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"message": "logout realizado"})
}

// verifyPassword compara senha com hash (use bcrypt em produção!)
func verifyPassword(password, hash string) bool {
    // EXEMPLO: não use SHA256 em produção! Use bcrypt.CompareHashAndPassword
    h := sha256.Sum256([]byte(password))
    return hex.EncodeToString(h[:]) == hash
}

func generateID() string {
    // Implementar geração de ID único
    return "id-" + time.Now().Format("20060102150405")
}

Configuração e Uso na Aplicação Principal

package main

import (
    "net/http"
    "time"
    "your-module/auth"
    "your-module/handlers"
)

func main() {
    // Configurar JWT
    jwtConfig := auth.Config{
        SecretKey:       "sua-chave-secreta-muito-segura-32-caracteres", // Use variável de ambiente!
        AccessDuration:  15 * time.Minute,
        RefreshDuration: 7 * 24 * time.Hour,
    }

    authManager := auth.NewManager(jwtConfig)
    tokenStore, _ := auth.NewSQLiteTokenStore("./tokens.db")

    // Criar handler de autenticação
    authHandler := &handlers.AuthHandler{
        authManager: authManager,
        tokenStore:  tokenStore,
        // userStore: seu userStore implementation
    }

    // Rotas públicas (sem autenticação)
    http.HandleFunc("/login", authHandler.HandleLogin)
    http.HandleFunc("/refresh", authHandler.HandleRefresh)
    http.HandleFunc("/logout", authHandler.HandleLogout)

    // Rotas protegidas (com middleware)
    protectedMux := http.NewServeMux()
    protectedMux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        // Aqui você pode acessar as claims do contexto
        w.Write([]byte(`{"message": "dados protegidos"}`))
    })

    // Aplicar middleware a rotas protegidas
    http.Handle("/profile", authManager.AuthMiddleware(protectedMux))

    http.ListenAndServe(":8080", nil)
}

Conclusão

Implementar autenticação JWT em Go requer entender três pilares: a estrutura e geração de tokens (combinando header, payload e signature com uma chave secreta), a validação robusta que protege contra ataques como algorithm substitution, e o padrão refresh token que equilibra segurança com experiência do usuário. Use sempre bibliotecas mantidas como golang-jwt/jwt/v5, nunca implemente criptografia manualmente, e armazene refresh tokens no banco de dados para permitir revogação — uma prática crucial para logout real e segurança em compromisso de credenciais.

Referências


Artigos relacionados