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
jsonwebtokencrate — 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
bcryptcrate — 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