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