Authentication com JWT e password hashing com argon2
1. Introdução à Autenticação com JWT e Argon2 em Rust
1.1. Visão geral do fluxo de autenticação
A autenticação em aplicações web modernas segue um fluxo bem definido: o usuário se registra fornecendo credenciais (email e senha), que são armazenadas de forma segura. Posteriormente, ao fazer login, as credenciais são verificadas e, se válidas, um token JWT é gerado e retornado ao cliente. Esse token é enviado em requisições subsequentes para acessar rotas protegidas, permitindo que o servidor identifique o usuário sem manter estado de sessão.
1.2. Por que usar JWT para sessões stateless
JWT (JSON Web Tokens) permite que o servidor não precise armazenar sessões em banco de dados ou memória. O token contém todas as informações necessárias sobre o usuário (como ID e permissões) em formato JSON, assinado digitalmente. Isso torna a arquitetura escalável e adequada para microsserviços, pois qualquer serviço pode validar o token sem comunicação com um servidor central de autenticação.
1.3. Por que Argon2 é o algoritmo recomendado
Argon2 é o vencedor do Password Hashing Competition e é amplamente recomendado por especialistas em segurança. Diferente de algoritmos como SHA-256 ou bcrypt, o Argon2 é resistente a ataques com GPU e ASIC, pois consome uma quantidade configurável de memória e tempo de processamento. Isso dificulta ataques de força bruta mesmo que o banco de dados seja comprometido.
2. Configuração do Projeto e Dependências
2.1. Criando um novo projeto Cargo
cargo new auth_jwt_argon2
cd auth_jwt_argon2
Adicione as seguintes dependências ao Cargo.toml:
[dependencies]
jsonwebtoken = "9"
argon2 = "0.5"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
tokio = { version = "1", features = ["full"] }
axum = "0.7"
tower-http = { version = "0.5", features = ["cors"] }
dotenvy = "0.15"
anyhow = "1"
2.2. Estrutura de diretórios sugerida
src/
├── main.rs
├── models.rs
├── handlers.rs
├── middleware.rs
└── utils.rs
2.3. Configuração de variáveis de ambiente
Crie um arquivo .env:
JWT_SECRET=sua_chave_secreta_super_segura_aqui
JWT_EXPIRATION_HOURS=24
3. Implementação do Hashing de Senhas com Argon2
3.1. Função para hash de senha
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use anyhow::Result;
pub fn hash_password(password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)?
.to_string();
Ok(password_hash)
}
3.2. Função para verificação de senha
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
let parsed_hash = PasswordHash::new(hash)?;
let argon2 = Argon2::default();
Ok(argon2
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
3.3. Tratamento de erros e segurança
O Argon2 já implementa proteção contra timing attacks por padrão, pois compara hashes em tempo constante. Além disso, sempre use salts aleatórios gerados pelo sistema (OsRng) para cada senha, garantindo que hashes idênticos não sejam produzidos para senhas iguais.
4. Criação e Validação de Tokens JWT
4.1. Estrutura dos claims
use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // subject (user_id)
pub exp: usize, // expiration timestamp
pub iat: usize, // issued at
pub roles: Vec<String>, // opcional
}
4.2. Função para criar token
pub fn create_token(user_id: &str, roles: Vec<String>, secret: &str) -> Result<String> {
let now = Utc::now();
let expiration_hours = std::env::var("JWT_EXPIRATION_HOURS")
.unwrap_or_else(|_| "24".to_string())
.parse::<i64>()
.unwrap_or(24);
let claims = Claims {
sub: user_id.to_string(),
exp: (now + Duration::hours(expiration_hours)).timestamp() as usize,
iat: now.timestamp() as usize,
roles,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)?;
Ok(token)
}
4.3. Função para validar e decodificar token
pub fn validate_token(token: &str, secret: &str) -> Result<Claims> {
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(),
)?;
Ok(token_data.claims)
}
5. Fluxo de Registro de Usuário
5.1. Handler de registro
use axum::{Json, http::StatusCode, response::IntoResponse};
use serde::Deserialize;
use std::collections::HashMap;
use tokio::sync::Mutex;
#[derive(Deserialize)]
pub struct RegisterRequest {
pub email: String,
pub password: String,
}
#[derive(Clone)]
pub struct AppState {
pub users: std::sync::Arc<Mutex<HashMap<String, User>>>,
}
#[derive(Clone)]
pub struct User {
pub id: String,
pub email: String,
pub password_hash: String,
}
pub async fn register_handler(
state: axum::extract::State<AppState>,
Json(payload): Json<RegisterRequest>,
) -> impl IntoResponse {
// Validação básica
if payload.email.is_empty() || payload.password.len() < 8 {
return (StatusCode::BAD_REQUEST, "Email ou senha inválidos".to_string()).into_response();
}
let mut users = state.users.lock().await;
if users.contains_key(&payload.email) {
return (StatusCode::CONFLICT, "Email já cadastrado".to_string()).into_response();
}
let password_hash = match crate::utils::hash_password(&payload.password) {
Ok(hash) => hash,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Erro ao processar senha".to_string()).into_response(),
};
let user = User {
id: uuid::Uuid::new_v4().to_string(),
email: payload.email.clone(),
password_hash,
};
users.insert(payload.email, user);
(StatusCode::CREATED, "Usuário registrado com sucesso").into_response()
}
6. Fluxo de Login e Geração de JWT
6.1. Handler de login
#[derive(Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
pub async fn login_handler(
state: axum::extract::State<AppState>,
Json(payload): Json<LoginRequest>,
) -> impl IntoResponse {
let users = state.users.lock().await;
let user = match users.get(&payload.email) {
Some(u) => u,
None => return (StatusCode::UNAUTHORIZED, "Credenciais inválidas".to_string()).into_response(),
};
match crate::utils::verify_password(&payload.password, &user.password_hash) {
Ok(true) => {
let secret = std::env::var("JWT_SECRET").unwrap_or_default();
match crate::utils::create_token(&user.id, vec!["user".to_string()], &secret) {
Ok(token) => {
let response = serde_json::json!({
"token": token,
"user_id": user.id
});
(StatusCode::OK, Json(response)).into_response()
}
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Erro ao gerar token".to_string()).into_response(),
}
}
Ok(false) => (StatusCode::UNAUTHORIZED, "Credenciais inválidas".to_string()).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Erro ao verificar senha".to_string()).into_response(),
}
}
7. Middleware de Autenticação para Rotas Protegidas
7.1. Extraindo token do cabeçalho
use axum::{
http::Request,
middleware::Next,
response::Response,
extract::FromRequestParts,
};
pub struct AuthenticatedUser {
pub user_id: String,
pub roles: Vec<String>,
}
pub async fn auth_middleware<B>(
mut req: Request<B>,
next: Next<B>,
) -> Result<Response, axum::response::Response> {
let auth_header = req
.headers()
.get("Authorization")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or_else(|| {
(axum::http::StatusCode::UNAUTHORIZED, "Token não fornecido").into_response()
})?;
let secret = std::env::var("JWT_SECRET").map_err(|_| {
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Erro de configuração").into_response()
})?;
let claims = crate::utils::validate_token(auth_header, &secret).map_err(|_| {
(axum::http::StatusCode::UNAUTHORIZED, "Token inválido ou expirado").into_response()
})?;
let authenticated_user = AuthenticatedUser {
user_id: claims.sub,
roles: claims.roles,
};
req.extensions_mut().insert(authenticated_user);
Ok(next.run(req).await)
}
8. Considerações Finais e Boas Práticas
8.1. Renovação de tokens e invalidação
Para maior segurança, implemente refresh tokens com curta duração (15-30 minutos) e tokens de refresh com duração maior (7-30 dias). Para invalidar tokens antes da expiração, mantenha uma blacklist em Redis ou banco de dados.
8.2. Armazenamento seguro de segredos
Nunca hardcode segredos no código fonte. Use variáveis de ambiente ou serviços como AWS Secrets Manager, HashiCorp Vault ou Azure Key Vault. Em produção, altere o segredo JWT regularmente.
8.3. Testes unitários e de integração
Escreva testes para cada função de utilidade e handlers. Teste cenários como: registro com email duplicado, login com senha incorreta, acesso a rota protegida sem token, e expiração de token.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_password_hashing() {
let password = "minha_senha_segura_123";
let hash = hash_password(password).unwrap();
assert!(verify_password(password, &hash).unwrap());
assert!(!verify_password("senha_errada", &hash).unwrap());
}
#[test]
fn test_jwt_creation_and_validation() {
let secret = "test_secret_key";
let token = create_token("user123", vec!["admin".to_string()], secret).unwrap();
let claims = validate_token(&token, secret).unwrap();
assert_eq!(claims.sub, "user123");
assert!(claims.roles.contains(&"admin".to_string()));
}
}
Referências
- jsonwebtoken crate documentation — Documentação oficial da crate jsonwebtoken com exemplos de criação e validação de tokens JWT
- Argon2 crate documentation — Documentação oficial da implementação Rust do algoritmo Argon2 para hashing de senhas
- OWASP Password Storage Cheat Sheet — Guia oficial OWASP sobre armazenamento seguro de senhas, recomendando Argon2
- JWT.io Debugger — Ferramenta online para decodificar e verificar tokens JWT, útil durante desenvolvimento
- Axum Authentication Middleware Guide — Documentação oficial do Axum sobre criação de middlewares de autenticação
- Rust Web Development with Axum — Exemplos oficiais do Axum incluindo autenticação JWT e middlewares