Rust

Autenticação JWT — Protegendo sua API com Tokens Já leu

18 min de leitura

Autenticação JWT — Protegendo sua API com Tokens
Nos dois artigos anteriores construímos uma API REST com persistência real. Mas qualquer pessoa com acesso ao servidor pode criar, modificar e deletar tarefas. É hora de proteger nossa API com autenticação. JWT — JSON We

 

Nos dois artigos anteriores construímos uma API REST com persistência real. Mas qualquer pessoa com acesso ao servidor pode criar, modificar e deletar tarefas. É hora de proteger nossa API com autenticação.

JWT — JSON Web Token é o padrão mais usado para autenticação em APIs REST modernas. Um JWT é um token assinado digitalmente que carrega informações sobre o usuário. O servidor gera o token no login, e o cliente o envia em cada requisição subsequente. O servidor verifica a assinatura — sem consultar banco de dados a cada request.


Como JWT funciona

Um JWT tem três partes separadas por pontos:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.    ← Header (Base64)
eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTcwMH0.  ← Payload (Base64)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← Assinatura
  • Header: algoritmo de assinatura (HS256, RS256...)
  • Payload: claims — dados sobre o usuário e o token (id, expiração, roles...)
  • Assinatura: HMAC do header+payload com a chave secreta

O servidor verifica a assinatura com sua chave secreta. Se válida, os dados do payload são confiáveis — sem consulta ao banco.


Configurando o projeto

Adicione ao Cargo.toml:

[dependencies]
# ... dependências anteriores ...
jsonwebtoken = "9"
bcrypt = "0.15"
axum-extra = { version = "0.9", features = ["typed-header"] }
headers = "0.4"

Estrutura adicional

src/
├── main.rs
├── erro.rs
├── modelo.rs
├── estado.rs
├── repositorio.rs
├── auth/
│   ├── mod.rs
│   ├── jwt.rs
│   ├── middleware.rs
│   └── handlers.rs
└── rotas/
    ├── mod.rs
    └── tarefas.rs

O modelo de usuário

src/modelo.rs (adições):

use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usuario {
    pub id: Uuid,
    pub nome: String,
    pub email: String,
    #[serde(skip_serializing)] // nunca serializa o hash
    pub senha_hash: String,
    pub criado_em: DateTime<Utc>,
}

impl Usuario {
    pub fn novo(nome: String, email: String, senha_hash: String) -> Self {
        Usuario {
            id: Uuid::new_v4(),
            nome,
            email,
            senha_hash,
            criado_em: Utc::now(),
        }
    }
}

// Payload para registro
#[derive(Debug, Deserialize)]
pub struct RegistrarUsuario {
    pub nome: String,
    pub email: String,
    pub senha: String,
}

// Payload para login
#[derive(Debug, Deserialize)]
pub struct LoginUsuario {
    pub email: String,
    pub senha: String,
}

// Resposta de autenticação
#[derive(Debug, Serialize)]
pub struct RespostaAuth {
    pub token: String,
    pub token_type: String,
    pub expira_em: u64,
    pub usuario: UsuarioPublico,
}

// Dados públicos do usuário (sem senha)
#[derive(Debug, Serialize, Clone)]
pub struct UsuarioPublico {
    pub id: Uuid,
    pub nome: String,
    pub email: String,
}

impl From<&Usuario> for UsuarioPublico {
    fn from(u: &Usuario) -> Self {
        UsuarioPublico {
            id: u.id,
            nome: u.nome.clone(),
            email: u.email.clone(),
        }
    }
}

Migration para usuários

migrations/20240302000000_criar_usuarios.sql:

CREATE TABLE IF NOT EXISTS usuarios (
    id          TEXT     PRIMARY KEY NOT NULL,
    nome        TEXT     NOT NULL,
    email       TEXT     NOT NULL UNIQUE,
    senha_hash  TEXT     NOT NULL,
    criado_em   TEXT     NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_usuarios_email
    ON usuarios(email);

-- Adiciona coluna usuario_id na tabela tarefas
ALTER TABLE tarefas ADD COLUMN usuario_id TEXT REFERENCES usuarios(id);

O módulo JWT

src/auth/jwt.rs:

use chrono::{Duration, Utc};
use jsonwebtoken::{
    decode, encode, errors::Error as JwtError,
    Algorithm, DecodingKey, EncodingKey, Header, Validation,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

// Claims são os dados dentro do JWT
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub sub: String,        // subject — id do usuário
    pub email: String,      // email do usuário
    pub nome: String,       // nome do usuário
    pub exp: usize,         // expiration — timestamp Unix
    pub iat: usize,         // issued at — quando foi emitido
}

impl Claims {
    pub fn usuario_id(&self) -> Result<Uuid, crate::erro::ErroApi> {
        Uuid::parse_str(&self.sub)
            .map_err(|_| crate::erro::ErroApi::ErroInterno)
    }
}

pub struct GerenciadorJwt {
    chave_encoding: EncodingKey,
    chave_decoding: DecodingKey,
    duracao_horas: i64,
}

impl GerenciadorJwt {
    pub fn novo(segredo: &str, duracao_horas: i64) -> Self {
        GerenciadorJwt {
            chave_encoding: EncodingKey::from_secret(segredo.as_bytes()),
            chave_decoding: DecodingKey::from_secret(segredo.as_bytes()),
            duracao_horas,
        }
    }

    pub fn gerar_token(
        &self,
        usuario_id: &Uuid,
        email: &str,
        nome: &str,
    ) -> Result<(String, u64), JwtError> {
        let agora = Utc::now();
        let expiracao = agora + Duration::hours(self.duracao_horas);

        let claims = Claims {
            sub: usuario_id.to_string(),
            email: email.to_string(),
            nome: nome.to_string(),
            exp: expiracao.timestamp() as usize,
            iat: agora.timestamp() as usize,
        };

        let token = encode(
            &Header::new(Algorithm::HS256),
            &claims,
            &self.chave_encoding,
        )?;

        Ok((token, expiracao.timestamp() as u64))
    }

    pub fn verificar_token(&self, token: &str) -> Result<Claims, JwtError> {
        let mut validacao = Validation::new(Algorithm::HS256);
        validacao.validate_exp = true;

        let dados = decode::<Claims>(
            token,
            &self.chave_decoding,
            &validacao,
        )?;

        Ok(dados.claims)
    }
}

O middleware de autenticação

src/auth/middleware.rs:

use axum::{
    extract::{Request, State},
    http::header::AUTHORIZATION,
    middleware::Next,
    response::Response,
};
use std::sync::Arc;

use crate::{
    auth::jwt::GerenciadorJwt,
    erro::ErroApi,
};

// Extensão inserida no request após autenticação
#[derive(Debug, Clone)]
pub struct UsuarioAutenticado {
    pub id: uuid::Uuid,
    pub email: String,
    pub nome: String,
}

pub async fn middleware_auth(
    State(jwt): State<Arc<GerenciadorJwt>>,
    mut request: Request,
    next: Next,
) -> Result<Response, ErroApi> {
    // Extrai o header Authorization
    let header_auth = request
        .headers()
        .get(AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .ok_or(ErroApi::NaoAutorizado)?;

    // Verifica formato "Bearer <token>"
    let token = header_auth
        .strip_prefix("Bearer ")
        .ok_or(ErroApi::FormatoTokenInvalido)?;

    // Verifica e decodifica o token
    let claims = jwt
        .verificar_token(token)
        .map_err(|e| {
            if e.kind() == &jsonwebtoken::errors::ErrorKind::ExpiredSignature {
                ErroApi::TokenExpirado
            } else {
                ErroApi::TokenInvalido
            }
        })?;

    let usuario_id = claims.usuario_id()?;

    // Insere os dados do usuário no request para os handlers
    request.extensions_mut().insert(UsuarioAutenticado {
        id: usuario_id,
        email: claims.email,
        nome: claims.nome,
    });

    Ok(next.run(request).await)
}

Atualizando o sistema de erros

src/erro.rs (adições):

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ErroApi {
    // ... erros anteriores ...

    #[error("Não autorizado — faça login primeiro")]
    NaoAutorizado,

    #[error("Token JWT inválido")]
    TokenInvalido,

    #[error("Token JWT expirado")]
    TokenExpirado,

    #[error("Formato de token inválido — use 'Bearer <token>'")]
    FormatoTokenInvalido,

    #[error("Credenciais inválidas")]
    CredenciaisInvalidas,

    #[error("Email já cadastrado")]
    EmailJaCadastrado,

    #[error("Senha deve ter no mínimo 8 caracteres")]
    SenhaMuitoCurta,

    #[error("Acesso negado — recurso pertence a outro usuário")]
    AcessoNegado,

    #[error("Tarefa não encontrada: {0}")]
    NaoEncontrada(String),

    #[error("Dados inválidos: {0}")]
    DadosInvalidos(String),

    #[error("Título não pode ser vazio")]
    TituloVazio,

    #[error("Conflito: {0}")]
    Conflito(String),

    #[error("Erro interno do servidor")]
    ErroInterno,
}

impl IntoResponse for ErroApi {
    fn into_response(self) -> Response {
        let (status, mensagem) = match &self {
            ErroApi::NaoAutorizado         => (StatusCode::UNAUTHORIZED, self.to_string()),
            ErroApi::TokenInvalido         => (StatusCode::UNAUTHORIZED, self.to_string()),
            ErroApi::TokenExpirado         => (StatusCode::UNAUTHORIZED, self.to_string()),
            ErroApi::FormatoTokenInvalido  => (StatusCode::UNAUTHORIZED, self.to_string()),
            ErroApi::CredenciaisInvalidas  => (StatusCode::UNAUTHORIZED, self.to_string()),
            ErroApi::AcessoNegado          => (StatusCode::FORBIDDEN,    self.to_string()),
            ErroApi::NaoEncontrada(msg)    => (StatusCode::NOT_FOUND,    msg.clone()),
            ErroApi::EmailJaCadastrado     => (StatusCode::CONFLICT,     self.to_string()),
            ErroApi::DadosInvalidos(msg)   => (StatusCode::BAD_REQUEST,  msg.clone()),
            ErroApi::TituloVazio           => (StatusCode::BAD_REQUEST,  self.to_string()),
            ErroApi::SenhaMuitoCurta       => (StatusCode::BAD_REQUEST,  self.to_string()),
            ErroApi::Conflito(msg)         => (StatusCode::CONFLICT,     msg.clone()),
            ErroApi::ErroInterno           => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
        };

        let corpo = json!({
            "erro": mensagem,
            "codigo": status.as_u16(),
        });

        (status, Json(corpo)).into_response()
    }
}

pub type ResultadoApi<T> = Result<T, ErroApi>;

Os handlers de autenticação

src/auth/handlers.rs:

use axum::{extract::State, Json};
use std::sync::Arc;
use tracing::info;

use crate::{
    auth::{jwt::GerenciadorJwt, middleware::UsuarioAutenticado},
    erro::{ErroApi, ResultadoApi},
    modelo::{
        LoginUsuario, RegistrarUsuario, RespostaAuth,
        Usuario, UsuarioPublico,
    },
    repositorio::RepositorioUsuarios,
};

#[derive(Clone)]
pub struct EstadoAuth {
    pub repo_usuarios: RepositorioUsuarios,
    pub jwt: Arc<GerenciadorJwt>,
}

pub async fn registrar(
    State(estado): State<EstadoAuth>,
    Json(payload): Json<RegistrarUsuario>,
) -> ResultadoApi<Json<UsuarioPublico>> {
    // Validações
    if payload.nome.trim().is_empty() {
        return Err(ErroApi::DadosInvalidos("Nome não pode ser vazio".to_string()));
    }

    if !payload.email.contains('@') {
        return Err(ErroApi::DadosInvalidos("Email inválido".to_string()));
    }

    if payload.senha.len() < 8 {
        return Err(ErroApi::SenhaMuitoCurta);
    }

    // Verificar se email já existe
    if estado.repo_usuarios.buscar_por_email(&payload.email).await.is_ok() {
        return Err(ErroApi::EmailJaCadastrado);
    }

    // Hash da senha com bcrypt
    let senha_hash = bcrypt::hash(&payload.senha, bcrypt::DEFAULT_COST)
        .map_err(|_| ErroApi::ErroInterno)?;

    let usuario = Usuario::novo(
        payload.nome.trim().to_string(),
        payload.email.to_lowercase(),
        senha_hash,
    );

    estado.repo_usuarios.criar(&usuario).await?;

    info!(id = %usuario.id, email = %usuario.email, "Usuário registrado");

    Ok(Json(UsuarioPublico::from(&usuario)))
}

pub async fn login(
    State(estado): State<EstadoAuth>,
    Json(payload): Json<LoginUsuario>,
) -> ResultadoApi<Json<RespostaAuth>> {
    // Buscar usuário pelo email
    let usuario = estado.repo_usuarios
        .buscar_por_email(&payload.email.to_lowercase())
        .await
        .map_err(|_| ErroApi::CredenciaisInvalidas)?;

    // Verificar senha
    let senha_valida = bcrypt::verify(&payload.senha, &usuario.senha_hash)
        .map_err(|_| ErroApi::ErroInterno)?;

    if !senha_valida {
        return Err(ErroApi::CredenciaisInvalidas);
    }

    // Gerar token JWT
    let (token, expira_em) = estado.jwt
        .gerar_token(&usuario.id, &usuario.email, &usuario.nome)
        .map_err(|_| ErroApi::ErroInterno)?;

    info!(id = %usuario.id, "Login realizado");

    Ok(Json(RespostaAuth {
        token,
        token_type: "Bearer".to_string(),
        expira_em,
        usuario: UsuarioPublico::from(&usuario),
    }))
}

pub async fn perfil_atual(
    usuario: axum::Extension<UsuarioAutenticado>,
) -> ResultadoApi<Json<serde_json::Value>> {
    Ok(Json(serde_json::json!({
        "id": usuario.id,
        "email": usuario.email,
        "nome": usuario.nome,
    })))
}

pub async fn renovar_token(
    State(estado): State<EstadoAuth>,
    usuario: axum::Extension<UsuarioAutenticado>,
) -> ResultadoApi<Json<RespostaAuth>> {
    // Busca dados atuais do usuário
    let dados = estado.repo_usuarios
        .buscar_por_id(&usuario.id)
        .await?;

    let (token, expira_em) = estado.jwt
        .gerar_token(&dados.id, &dados.email, &dados.nome)
        .map_err(|_| ErroApi::ErroInterno)?;

    Ok(Json(RespostaAuth {
        token,
        token_type: "Bearer".to_string(),
        expira_em,
        usuario: UsuarioPublico::from(&dados),
    }))
}

Repositório de usuários

src/repositorio.rs (adições):

use crate::modelo::Usuario;
use sqlx::SqlitePool;
use uuid::Uuid;
use crate::erro::{ErroApi, ResultadoApi};

#[derive(Debug, sqlx::FromRow)]
struct UsuarioRow {
    id: String,
    nome: String,
    email: String,
    senha_hash: String,
    criado_em: String,
}

impl TryFrom<UsuarioRow> for Usuario {
    type Error = ErroApi;

    fn try_from(row: UsuarioRow) -> Result<Self, Self::Error> {
        let id = Uuid::parse_str(&row.id)
            .map_err(|_| ErroApi::ErroInterno)?;

        let criado_em = chrono::DateTime::parse_from_rfc3339(&row.criado_em)
            .map_err(|_| ErroApi::ErroInterno)?
            .with_timezone(&chrono::Utc);

        Ok(Usuario {
            id,
            nome: row.nome,
            email: row.email,
            senha_hash: row.senha_hash,
            criado_em,
        })
    }
}

#[derive(Clone)]
pub struct RepositorioUsuarios {
    pool: SqlitePool,
}

impl RepositorioUsuarios {
    pub fn novo(pool: SqlitePool) -> Self {
        RepositorioUsuarios { pool }
    }

    pub async fn criar(&self, usuario: &Usuario) -> ResultadoApi<()> {
        let id_str    = usuario.id.to_string();
        let criado_em = usuario.criado_em.to_rfc3339();

        sqlx::query!(
            "INSERT INTO usuarios (id, nome, email, senha_hash, criado_em)
             VALUES (?, ?, ?, ?, ?)",
            id_str,
            usuario.nome,
            usuario.email,
            usuario.senha_hash,
            criado_em
        )
        .execute(&self.pool)
        .await
        .map_err(|e| {
            if e.to_string().contains("UNIQUE constraint failed") {
                ErroApi::EmailJaCadastrado
            } else {
                ErroApi::ErroInterno
            }
        })?;

        Ok(())
    }

    pub async fn buscar_por_id(&self, id: &Uuid) -> ResultadoApi<Usuario> {
        let id_str = id.to_string();

        let row = sqlx::query_as!(
            UsuarioRow,
            "SELECT id, nome, email, senha_hash, criado_em
             FROM usuarios WHERE id = ?",
            id_str
        )
        .fetch_optional(&self.pool)
        .await
        .map_err(|_| ErroApi::ErroInterno)?
        .ok_or_else(|| ErroApi::NaoEncontrada(
            format!("Usuário {id} não encontrado")
        ))?;

        Usuario::try_from(row)
    }

    pub async fn buscar_por_email(&self, email: &str) -> ResultadoApi<Usuario> {
        let row = sqlx::query_as!(
            UsuarioRow,
            "SELECT id, nome, email, senha_hash, criado_em
             FROM usuarios WHERE email = ?",
            email
        )
        .fetch_optional(&self.pool)
        .await
        .map_err(|_| ErroApi::ErroInterno)?
        .ok_or_else(|| ErroApi::NaoEncontrada(
            format!("Usuário com email {email} não encontrado")
        ))?;

        Usuario::try_from(row)
    }
}

Protegendo as rotas de tarefas

Agora os handlers de tarefas precisam verificar que o usuário só acessa suas próprias tarefas:

src/rotas/tarefas.rs (trecho atualizado):

use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    Extension,
    Json,
};
use uuid::Uuid;

use crate::{
    auth::middleware::UsuarioAutenticado,
    erro::{ErroApi, ResultadoApi},
    modelo::{AtualizarTarefa, CriarTarefa, FiltroTarefas, ListaTarefas, Tarefa},
    repositorio::RepositorioTarefas,
};

pub async fn listar_tarefas(
    State(repo): State<RepositorioTarefas>,
    Extension(usuario): Extension<UsuarioAutenticado>,
    Query(filtro): Query<FiltroTarefas>,
) -> ResultadoApi<Json<ListaTarefas>> {
    let (tarefas, total) = repo
        .listar_por_usuario(&usuario.id, &filtro)
        .await?;

    Ok(Json(ListaTarefas {
        tarefas,
        total: total as usize,
        pagina: filtro.pagina,
        por_pagina: filtro.por_pagina,
    }))
}

pub async fn criar_tarefa(
    State(repo): State<RepositorioTarefas>,
    Extension(usuario): Extension<UsuarioAutenticado>,
    Json(payload): Json<CriarTarefa>,
) -> ResultadoApi<(StatusCode, Json<Tarefa>)> {
    if payload.titulo.trim().is_empty() {
        return Err(ErroApi::TituloVazio);
    }

    let mut tarefa = Tarefa::nova(
        payload.titulo.trim().to_string(),
        payload.descricao,
        payload.prioridade,
    );

    tarefa.usuario_id = Some(usuario.id);

    repo.criar(&tarefa).await?;

    Ok((StatusCode::CREATED, Json(tarefa)))
}

pub async fn buscar_tarefa(
    State(repo): State<RepositorioTarefas>,
    Extension(usuario): Extension<UsuarioAutenticado>,
    Path(id): Path<Uuid>,
) -> ResultadoApi<Json<Tarefa>> {
    let tarefa = repo.buscar_por_id(&id).await?;

    // Verifica que a tarefa pertence ao usuário
    if tarefa.usuario_id != Some(usuario.id) {
        return Err(ErroApi::AcessoNegado);
    }

    Ok(Json(tarefa))
}

Configurando as rotas com middleware

src/rotas/mod.rs:

use axum::{
    middleware,
    routing::{delete, get, post, put},
    Router,
};
use std::sync::Arc;

use crate::{
    auth::{
        handlers::{registrar, login, perfil_atual, renovar_token, EstadoAuth},
        jwt::GerenciadorJwt,
        middleware::middleware_auth,
    },
    repositorio::{RepositorioTarefas, RepositorioUsuarios},
};

pub fn criar_rotas(
    repo_tarefas: RepositorioTarefas,
    repo_usuarios: RepositorioUsuarios,
    jwt: Arc<GerenciadorJwt>,
) -> Router {
    let estado_auth = EstadoAuth {
        repo_usuarios: repo_usuarios.clone(),
        jwt: jwt.clone(),
    };

    // Rotas públicas — sem autenticação
    let rotas_publicas = Router::new()
        .route("/auth/registrar", post(registrar))
        .route("/auth/login", post(login))
        .with_state(estado_auth.clone());

    // Rotas protegidas — exigem JWT válido
    let rotas_protegidas = Router::new()
        .route("/auth/perfil", get(perfil_atual))
        .route("/auth/renovar", post(renovar_token))
        .route("/tarefas", get(tarefas::listar_tarefas).post(tarefas::criar_tarefa))
        .route("/tarefas/:id",
            get(tarefas::buscar_tarefa)
            .put(tarefas::atualizar_tarefa)
            .delete(tarefas::deletar_tarefa))
        .route("/tarefas/concluidas", delete(tarefas::limpar_concluidas))
        .route("/tarefas/estatisticas", get(tarefas::estatisticas))
        .layer(middleware::from_fn_with_state(
            jwt.clone(),
            middleware_auth,
        ))
        .with_state(repo_tarefas)
        .merge(
            Router::new()
                .route("/auth/perfil", get(perfil_atual))
                .route("/auth/renovar", post(renovar_token))
                .layer(middleware::from_fn_with_state(jwt, middleware_auth))
                .with_state(estado_auth)
        );

    Router::new()
        .merge(rotas_publicas)
        .merge(rotas_protegidas)
}

mod tarefas {
    pub use crate::rotas::tarefas::*;
}

Testando com curl

# Registrar usuário
curl -X POST http://localhost:3000/auth/registrar \
  -H "Content-Type: application/json" \
  -d '{
    "nome": "Ana Silva",
    "email": "ana@exemplo.com",
    "senha": "senha123"
  }'

# Resposta:
# { "id": "uuid...", "nome": "Ana Silva", "email": "ana@exemplo.com" }

# Login
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ana@exemplo.com",
    "senha": "senha123"
  }'

# Resposta:
# { "token": "eyJ...", "token_type": "Bearer", "expira_em": 1234567890, ... }

# Guardar o token
TOKEN="eyJ..."

# Criar tarefa (autenticado)
curl -X POST http://localhost:3000/tarefas \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"titulo": "Estudar JWT"}'

# Listar tarefas (autenticado)
curl http://localhost:3000/tarefas \
  -H "Authorization: Bearer $TOKEN"

# Sem token — 401 Unauthorized
curl http://localhost:3000/tarefas

# Token expirado ou inválido — 401 Unauthorized
curl http://localhost:3000/tarefas \
  -H "Authorization: Bearer token_invalido"

Testes automatizados de autenticação

tests/auth.rs:

use axum::{
    body::Body,
    http::{Request, StatusCode},
};
use serde_json::{json, Value};
use tower::ServiceExt;

async fn fazer_post(app: axum::Router, caminho: &str, corpo: Value) -> (StatusCode, Value) {
    let request = Request::builder()
        .method("POST")
        .uri(caminho)
        .header("Content-Type", "application/json")
        .body(Body::from(corpo.to_string()))
        .unwrap();

    let response = app.oneshot(request).await.unwrap();
    let status = response.status();
    let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
        .await.unwrap();
    let json: Value = serde_json::from_slice(&bytes).unwrap_or(json!(null));

    (status, json)
}

async fn fazer_get_autenticado(
    app: axum::Router,
    caminho: &str,
    token: &str,
) -> (StatusCode, Value) {
    let request = Request::builder()
        .method("GET")
        .uri(caminho)
        .header("Authorization", format!("Bearer {token}"))
        .body(Body::empty())
        .unwrap();

    let response = app.oneshot(request).await.unwrap();
    let status = response.status();
    let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
        .await.unwrap();
    let json: Value = serde_json::from_slice(&bytes).unwrap_or(json!(null));

    (status, json)
}

#[tokio::test]
async fn registro_e_login_funcionam() {
    let app = criar_app_teste().await;

    // Registrar
    let (status, _) = fazer_post(app.clone(), "/auth/registrar", json!({
        "nome": "Teste",
        "email": "teste@exemplo.com",
        "senha": "senha123"
    })).await;
    assert_eq!(status, StatusCode::OK);

    // Login
    let (status, resposta) = fazer_post(app, "/auth/login", json!({
        "email": "teste@exemplo.com",
        "senha": "senha123"
    })).await;
    assert_eq!(status, StatusCode::OK);
    assert!(resposta["token"].is_string());
    assert_eq!(resposta["token_type"], "Bearer");
}

#[tokio::test]
async fn login_senha_errada_retorna_401() {
    let app = criar_app_teste().await;

    fazer_post(app.clone(), "/auth/registrar", json!({
        "nome": "Teste",
        "email": "teste2@exemplo.com",
        "senha": "correta123"
    })).await;

    let (status, _) = fazer_post(app, "/auth/login", json!({
        "email": "teste2@exemplo.com",
        "senha": "errada456"
    })).await;

    assert_eq!(status, StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn rota_protegida_sem_token_retorna_401() {
    let app = criar_app_teste().await;

    let request = Request::builder()
        .method("GET")
        .uri("/tarefas")
        .body(Body::empty())
        .unwrap();

    let response = app.oneshot(request).await.unwrap();
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn rota_protegida_com_token_valido_funciona() {
    let app = criar_app_teste().await;

    // Registrar e fazer login
    fazer_post(app.clone(), "/auth/registrar", json!({
        "nome": "Teste",
        "email": "teste3@exemplo.com",
        "senha": "senha123"
    })).await;

    let (_, resposta_login) = fazer_post(app.clone(), "/auth/login", json!({
        "email": "teste3@exemplo.com",
        "senha": "senha123"
    })).await;

    let token = resposta_login["token"].as_str().unwrap();

    // Acessar rota protegida
    let (status, _) = fazer_get_autenticado(app, "/tarefas", token).await;
    assert_eq!(status, StatusCode::OK);
}

#[tokio::test]
async fn email_duplicado_retorna_409() {
    let app = criar_app_teste().await;

    let payload = json!({
        "nome": "Teste",
        "email": "duplicado@exemplo.com",
        "senha": "senha123"
    });

    fazer_post(app.clone(), "/auth/registrar", payload.clone()).await;
    let (status, _) = fazer_post(app, "/auth/registrar", payload).await;

    assert_eq!(status, StatusCode::CONFLICT);
}

#[tokio::test]
async fn senha_curta_retorna_400() {
    let app = criar_app_teste().await;

    let (status, corpo) = fazer_post(app, "/auth/registrar", json!({
        "nome": "Teste",
        "email": "teste@exemplo.com",
        "senha": "123"
    })).await;

    assert_eq!(status, StatusCode::BAD_REQUEST);
    assert!(corpo["erro"].as_str().unwrap().contains("8 caracteres"));
}

// Helper para criar app de teste com banco em memória
async fn criar_app_teste() -> axum::Router {
    use std::sync::Arc;
    use sqlx::SqlitePool;
    use api_tarefas::auth::jwt::GerenciadorJwt;
    use api_tarefas::repositorio::{RepositorioTarefas, RepositorioUsuarios};

    let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
    sqlx::migrate!("./migrations").run(&pool).await.unwrap();

    let jwt = Arc::new(GerenciadorJwt::novo("segredo_teste_123", 1));
    let repo_tarefas  = RepositorioTarefas::novo(pool.clone());
    let repo_usuarios = RepositorioUsuarios::novo(pool);

    api_tarefas::rotas::criar_rotas(repo_tarefas, repo_usuarios, jwt)
}

Considerações de segurança

Implementar JWT corretamente exige atenção a alguns pontos críticos:

Segredo forte. O segredo JWT deve ser longo e aleatório — nunca use strings simples em produção. Use variáveis de ambiente e gere com ferramentas como openssl rand -hex 64.

Expiração curta. Tokens devem expirar. Use tokens de acesso com vida curta (15-60 minutos) e tokens de refresh com vida longa se necessário. Nossa implementação usa 24 horas — adequado para muitos casos, mas ajuste conforme o risco.

HTTPS sempre. JWT não cifra o payload — apenas assina. Qualquer pessoa que intercepte o token pode ler seu conteúdo. Use HTTPS em produção para proteger o token em trânsito.

Não armazene dados sensíveis no payload. O payload é Base64 — visível a qualquer um que tenha o token. Armazene apenas o mínimo necessário (id, email, roles).

Revogação. JWT puro não tem revogação — um token válido continua válido até expirar. Para logout imediato, mantenha uma blocklist de tokens revogados em Redis ou banco de dados.


Fontes e leituras recomendadas

  • jsonwebtoken crate — implementação de JWT em Rust — https://docs.rs/jsonwebtoken
  • JWT.io — debugger interativo de tokens JWT — https://jwt.io
  • RFC 7519 — JSON Web Token — especificação oficial — https://datatracker.ietf.org/doc/html/rfc7519
  • bcrypt crate — hashing de senhas — https://docs.rs/bcrypt
  • "OWASP Authentication Cheat Sheet" — boas práticas de autenticação — https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
  • "Zero to Production in Rust" — Luca Palmieri — capítulo sobre autenticação — https://www.zero2prod.com
Comentários

Mais em Rust

Lifetimes — Quando o Compilador Precisa de Mais Informação
Lifetimes — Quando o Compilador Precisa de Mais Informação

&nbsp; Chegamos ao conceito que mais intimida quem est&aacute; aprendendo Ru...

Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos
Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos

Rust — Artigo #46 Rust e Machine Learning — Inferência, ONNX e Aceleração de...

Epílogo — O que Vem Depois, Como Continuar Crescendo, e a Filosofia do Rust
Epílogo — O que Vem Depois, Como Continuar Crescendo, e a Filosofia do Rust

Rust — Artigo #52 Epílogo — O que Vem Depois, Como Continuar Crescendo, e a F...