Autenticacao JWT em Rust

Construa um sistema completo de autenticacao JWT em Rust com Axum, registro de usuarios, login, tokens e rotas protegidas com middleware.

Neste projeto vamos construir um sistema completo de autenticacao com JWT (JSON Web Tokens) usando Axum. O sistema vai incluir registro de usuarios, login com geracao de tokens, validacao de tokens em middleware e rotas protegidas que exigem autenticacao. JWT e o padrao mais utilizado para autenticacao em APIs modernas e entender sua implementacao e fundamental para qualquer desenvolvedor backend.

Autenticacao e um dos pilares de qualquer aplicacao web. Ao construir este projeto, voce vai entender como tokens funcionam, como proteger rotas e como estruturar um fluxo de autenticacao seguro em Rust.

O Que Vamos Construir

Um sistema de autenticacao com as seguintes funcionalidades:

  • POST /api/registrar – Criar nova conta de usuario
  • POST /api/login – Autenticar e receber token JWT
  • GET /api/perfil – Rota protegida que retorna dados do usuario
  • GET /api/admin – Rota protegida que exige role de administrador
  • Hash de senhas com bcrypt
  • Geracao e validacao de tokens JWT com expiracao
  • Middleware de autenticacao reutilizavel
  • Armazenamento em memoria com HashMap

Estrutura do Projeto

autenticacao-jwt/
├── Cargo.toml
└── src/
    └── main.rs

Configurando o Projeto

cargo new autenticacao-jwt
cd autenticacao-jwt

Configure o Cargo.toml:

[package]
name = "autenticacao-jwt"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
axum-extra = { version = "0.9", features = ["typed-header"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
jsonwebtoken = "9"
bcrypt = "0.16"
chrono = "0.4"
uuid = { version = "1", features = ["v4"] }

Usamos jsonwebtoken para criar e validar JWTs, bcrypt para hash de senhas e axum-extra para extrair headers de autorizacao.

Passo 1: Definindo Modelos e Estado

Vamos definir as estruturas de dados para usuarios, tokens e requisicoes:

use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;

// Segredo usado para assinar os tokens (em producao, use variavel de ambiente)
const SEGREDO_JWT: &str = "meu_segredo_super_secreto_em_producao_use_env";

// Representa um usuario armazenado no sistema
#[derive(Debug, Clone, Serialize)]
pub struct Usuario {
    pub id: String,
    pub nome: String,
    pub email: String,
    #[serde(skip_serializing)]
    pub senha_hash: String,
    pub role: String,
}

// Dados para registro de novo usuario
#[derive(Debug, Deserialize)]
pub struct DadosRegistro {
    pub nome: String,
    pub email: String,
    pub senha: String,
}

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

// Claims do token JWT
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,       // ID do usuario
    pub email: String,
    pub role: String,
    pub exp: usize,        // Expiracao (timestamp Unix)
    pub iat: usize,        // Emitido em (timestamp Unix)
}

// Resposta com o token
#[derive(Debug, Serialize)]
pub struct RespostaToken {
    pub token: String,
    pub tipo: String,
    pub expira_em: String,
}

// Estado compartilhado: email -> Usuario
pub type EstadoApp = Arc<Mutex<HashMap<String, Usuario>>>;

Os Claims sao os dados codificados dentro do JWT. Incluimos o ID do usuario, email, role e timestamps de criacao e expiracao.

Passo 2: Funcoes de Token e Hash

Implementamos as funcoes para gerar tokens JWT e fazer hash de senhas:

// Gera um token JWT para o usuario
fn gerar_token(usuario: &Usuario) -> Result<String, jsonwebtoken::errors::Error> {
    let agora = Utc::now();
    let expiracao = agora + Duration::hours(24);

    let claims = Claims {
        sub: usuario.id.clone(),
        email: usuario.email.clone(),
        role: usuario.role.clone(),
        exp: expiracao.timestamp() as usize,
        iat: agora.timestamp() as usize,
    };

    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(SEGREDO_JWT.as_bytes()),
    )
}

// Valida e decodifica um token JWT
fn validar_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let dados = decode::<Claims>(
        token,
        &DecodingKey::from_secret(SEGREDO_JWT.as_bytes()),
        &Validation::default(),
    )?;
    Ok(dados.claims)
}

// Gera hash bcrypt da senha
fn hash_senha(senha: &str) -> Result<String, bcrypt::BcryptError> {
    bcrypt::hash(senha, bcrypt::DEFAULT_COST)
}

// Verifica se a senha corresponde ao hash
fn verificar_senha(senha: &str, hash: &str) -> bool {
    bcrypt::verify(senha, hash).unwrap_or(false)
}

O token e assinado com HMAC-SHA256 (padrao do jsonwebtoken) e expira em 24 horas. O bcrypt garante que as senhas sejam armazenadas de forma segura com salt automatico.

Passo 3: Handlers e Middleware de Autenticacao

Vamos implementar os handlers de registro, login e as rotas protegidas:

use axum::{
    extract::{Request, State},
    http::{header, StatusCode},
    middleware::Next,
    response::IntoResponse,
    Json,
};

// POST /api/registrar
async fn registrar(
    State(estado): State<EstadoApp>,
    Json(dados): Json<DadosRegistro>,
) -> Result<(StatusCode, Json<serde_json::Value>), (StatusCode, Json<serde_json::Value>)> {
    let mut usuarios = estado.lock().unwrap();

    // Verifica se o email ja esta cadastrado
    if usuarios.contains_key(&dados.email) {
        return Err((
            StatusCode::CONFLICT,
            Json(serde_json::json!({"erro": "Email ja cadastrado"})),
        ));
    }

    // Valida dados minimos
    if dados.senha.len() < 6 {
        return Err((
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({"erro": "Senha deve ter pelo menos 6 caracteres"})),
        ));
    }

    let senha_hash = hash_senha(&dados.senha).map_err(|_| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(serde_json::json!({"erro": "Erro ao processar senha"})),
        )
    })?;

    let usuario = Usuario {
        id: Uuid::new_v4().to_string(),
        nome: dados.nome,
        email: dados.email.clone(),
        senha_hash,
        role: "usuario".to_string(),
    };

    usuarios.insert(dados.email, usuario);

    Ok((
        StatusCode::CREATED,
        Json(serde_json::json!({"mensagem": "Usuario registrado com sucesso"})),
    ))
}

// POST /api/login
async fn login(
    State(estado): State<EstadoApp>,
    Json(dados): Json<DadosLogin>,
) -> Result<Json<RespostaToken>, (StatusCode, Json<serde_json::Value>)> {
    let usuarios = estado.lock().unwrap();

    let usuario = usuarios.get(&dados.email).ok_or((
        StatusCode::UNAUTHORIZED,
        Json(serde_json::json!({"erro": "Email ou senha incorretos"})),
    ))?;

    if !verificar_senha(&dados.senha, &usuario.senha_hash) {
        return Err((
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({"erro": "Email ou senha incorretos"})),
        ));
    }

    let token = gerar_token(usuario).map_err(|_| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(serde_json::json!({"erro": "Erro ao gerar token"})),
        )
    })?;

    Ok(Json(RespostaToken {
        token,
        tipo: "Bearer".to_string(),
        expira_em: "24 horas".to_string(),
    }))
}

// Middleware de autenticacao: extrai e valida o token do header Authorization
async fn middleware_auth(
    mut req: Request,
    next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
    let header_auth = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .ok_or((
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({"erro": "Token de autorizacao ausente"})),
        ))?;

    let token = header_auth
        .strip_prefix("Bearer ")
        .ok_or((
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({"erro": "Formato de token invalido. Use: Bearer <token>"})),
        ))?;

    let claims = validar_token(token).map_err(|_| {
        (
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({"erro": "Token invalido ou expirado"})),
        )
    })?;

    // Insere os claims no request para os handlers usarem
    req.extensions_mut().insert(claims);

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

// GET /api/perfil -- Rota protegida
async fn perfil(req: Request) -> impl IntoResponse {
    let claims = req.extensions().get::<Claims>().unwrap();
    Json(serde_json::json!({
        "id": claims.sub,
        "email": claims.email,
        "role": claims.role,
        "mensagem": "Voce esta autenticado!"
    }))
}

// GET /api/admin -- Rota protegida com verificacao de role
async fn admin(req: Request) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
    let claims = req.extensions().get::<Claims>().unwrap();

    if claims.role != "admin" {
        return Err((
            StatusCode::FORBIDDEN,
            Json(serde_json::json!({"erro": "Acesso negado. Requer role de administrador."})),
        ));
    }

    Ok(Json(serde_json::json!({
        "mensagem": "Bem-vindo, administrador!",
        "dados_sensiveis": "Informacoes restritas"
    })))
}

O middleware extrai o token do header Authorization, valida-o e insere os claims decodificados nas extensoes do request. Os handlers protegidos podem entao acessar os claims para identificar o usuario.

Passo 4: Montando o main.rs Completo

Aqui esta o codigo completo do src/main.rs:

use axum::{
    extract::{Request, State},
    http::{header, StatusCode},
    middleware::{self, Next},
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;

// === Constantes ===

const SEGREDO_JWT: &str = "meu_segredo_super_secreto_em_producao_use_env";

// === Modelos ===

#[derive(Debug, Clone, Serialize)]
pub struct Usuario {
    pub id: String,
    pub nome: String,
    pub email: String,
    #[serde(skip_serializing)]
    pub senha_hash: String,
    pub role: String,
}

#[derive(Debug, Deserialize)]
pub struct DadosRegistro {
    pub nome: String,
    pub email: String,
    pub senha: String,
}

#[derive(Debug, Deserialize)]
pub struct DadosLogin {
    pub email: String,
    pub senha: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,
    pub email: String,
    pub role: String,
    pub exp: usize,
    pub iat: usize,
}

#[derive(Debug, Serialize)]
pub struct RespostaToken {
    pub token: String,
    pub tipo: String,
    pub expira_em: String,
}

pub type EstadoApp = Arc<Mutex<HashMap<String, Usuario>>>;

// === Funcoes auxiliares ===

fn gerar_token(usuario: &Usuario) -> Result<String, jsonwebtoken::errors::Error> {
    let agora = Utc::now();
    let expiracao = agora + Duration::hours(24);
    let claims = Claims {
        sub: usuario.id.clone(),
        email: usuario.email.clone(),
        role: usuario.role.clone(),
        exp: expiracao.timestamp() as usize,
        iat: agora.timestamp() as usize,
    };
    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(SEGREDO_JWT.as_bytes()),
    )
}

fn validar_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let dados = decode::<Claims>(
        token,
        &DecodingKey::from_secret(SEGREDO_JWT.as_bytes()),
        &Validation::default(),
    )?;
    Ok(dados.claims)
}

fn hash_senha(senha: &str) -> Result<String, bcrypt::BcryptError> {
    bcrypt::hash(senha, bcrypt::DEFAULT_COST)
}

fn verificar_senha(senha: &str, hash: &str) -> bool {
    bcrypt::verify(senha, hash).unwrap_or(false)
}

// === Handlers ===

async fn registrar(
    State(estado): State<EstadoApp>,
    Json(dados): Json<DadosRegistro>,
) -> Result<(StatusCode, Json<serde_json::Value>), (StatusCode, Json<serde_json::Value>)> {
    let mut usuarios = estado.lock().unwrap();

    if usuarios.contains_key(&dados.email) {
        return Err((
            StatusCode::CONFLICT,
            Json(serde_json::json!({"erro": "Email ja cadastrado"})),
        ));
    }

    if dados.senha.len() < 6 {
        return Err((
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({"erro": "Senha deve ter pelo menos 6 caracteres"})),
        ));
    }

    let senha_hash = hash_senha(&dados.senha).map_err(|_| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(serde_json::json!({"erro": "Erro ao processar senha"})),
        )
    })?;

    let usuario = Usuario {
        id: Uuid::new_v4().to_string(),
        nome: dados.nome,
        email: dados.email.clone(),
        senha_hash,
        role: "usuario".to_string(),
    };

    usuarios.insert(dados.email, usuario);

    Ok((
        StatusCode::CREATED,
        Json(serde_json::json!({"mensagem": "Usuario registrado com sucesso"})),
    ))
}

async fn login(
    State(estado): State<EstadoApp>,
    Json(dados): Json<DadosLogin>,
) -> Result<Json<RespostaToken>, (StatusCode, Json<serde_json::Value>)> {
    let usuarios = estado.lock().unwrap();

    let usuario = usuarios.get(&dados.email).ok_or((
        StatusCode::UNAUTHORIZED,
        Json(serde_json::json!({"erro": "Email ou senha incorretos"})),
    ))?;

    if !verificar_senha(&dados.senha, &usuario.senha_hash) {
        return Err((
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({"erro": "Email ou senha incorretos"})),
        ));
    }

    let token = gerar_token(usuario).map_err(|_| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(serde_json::json!({"erro": "Erro ao gerar token"})),
        )
    })?;

    Ok(Json(RespostaToken {
        token,
        tipo: "Bearer".to_string(),
        expira_em: "24 horas".to_string(),
    }))
}

async fn middleware_auth(
    mut req: Request,
    next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
    let header_auth = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .ok_or((
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({"erro": "Token de autorizacao ausente"})),
        ))?;

    let token = header_auth.strip_prefix("Bearer ").ok_or((
        StatusCode::UNAUTHORIZED,
        Json(serde_json::json!({"erro": "Formato de token invalido"})),
    ))?;

    let claims = validar_token(token).map_err(|_| {
        (
            StatusCode::UNAUTHORIZED,
            Json(serde_json::json!({"erro": "Token invalido ou expirado"})),
        )
    })?;

    req.extensions_mut().insert(claims);
    Ok(next.run(req).await)
}

async fn perfil(req: Request) -> impl IntoResponse {
    let claims = req.extensions().get::<Claims>().unwrap();
    Json(serde_json::json!({
        "id": claims.sub,
        "email": claims.email,
        "role": claims.role,
        "mensagem": "Voce esta autenticado!"
    }))
}

async fn admin(
    req: Request,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
    let claims = req.extensions().get::<Claims>().unwrap();
    if claims.role != "admin" {
        return Err((
            StatusCode::FORBIDDEN,
            Json(serde_json::json!({"erro": "Acesso negado. Requer role de administrador."})),
        ));
    }
    Ok(Json(serde_json::json!({
        "mensagem": "Bem-vindo, administrador!",
        "dados_sensiveis": "Informacoes restritas"
    })))
}

// === Main ===

#[tokio::main]
async fn main() {
    let estado: EstadoApp = Arc::new(Mutex::new(HashMap::new()));

    // Rotas publicas
    let rotas_publicas = Router::new()
        .route("/api/registrar", post(registrar))
        .route("/api/login", post(login));

    // Rotas protegidas (com middleware de autenticacao)
    let rotas_protegidas = Router::new()
        .route("/api/perfil", get(perfil))
        .route("/api/admin", get(admin))
        .layer(middleware::from_fn(middleware_auth));

    let app = Router::new()
        .merge(rotas_publicas)
        .merge(rotas_protegidas)
        .with_state(estado);

    let endereco = "0.0.0.0:3000";
    println!("Servidor de autenticacao JWT rodando em http://{}", endereco);

    let listener = tokio::net::TcpListener::bind(endereco).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

A separacao entre rotas publicas e protegidas garante que o middleware de autenticacao so seja aplicado onde necessario. O Router::merge combina os dois conjuntos de rotas.

Como Executar

Compile e inicie o servidor:

cargo run

Teste o fluxo completo com curl:

# 1. Registrar um usuario
curl -X POST http://localhost:3000/api/registrar \
  -H "Content-Type: application/json" \
  -d '{"nome": "Maria Silva", "email": "maria@email.com", "senha": "senha123"}'
# {"mensagem":"Usuario registrado com sucesso"}

# 2. Fazer login
curl -X POST http://localhost:3000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email": "maria@email.com", "senha": "senha123"}'
# {"token":"eyJ0eXAiOi...","tipo":"Bearer","expira_em":"24 horas"}

# 3. Acessar rota protegida (substitua pelo token recebido)
curl http://localhost:3000/api/perfil \
  -H "Authorization: Bearer eyJ0eXAiOi..."
# {"id":"...","email":"maria@email.com","role":"usuario","mensagem":"Voce esta autenticado!"}

# 4. Tentar acessar sem token
curl http://localhost:3000/api/perfil
# {"erro":"Token de autorizacao ausente"}

Desafios para Expandir

  1. Refresh tokens – Implemente um sistema de refresh tokens com expiracao mais longa, permitindo renovar o access token sem refazer login, armazenando refresh tokens em um HashMap separado.

  2. Logout e blacklist – Crie um endpoint POST /api/logout que adiciona o token a uma blacklist, impedindo seu uso mesmo antes da expiracao natural.

  3. Recuperacao de senha – Adicione um fluxo de “esqueci minha senha” que gera um token temporario de reset e um endpoint para definir nova senha.

  4. Rate limiting no login – Limite tentativas de login por IP e por email para prevenir ataques de forca bruta, bloqueando temporariamente apos 5 tentativas falhas.

  5. Persistencia com banco de dados – Migre o armazenamento para SQLite com sqlx, adicionando migrations para a tabela de usuarios e indices no campo email.

Veja Tambem