Authentication & Authorization Guide

Complete guide to authentication and authorization in Armature using armature-auth.

Table of Contents

  1. Overview
  2. Installation
  3. Password Hashing
  4. Authentication Service
  5. User Management
  6. Guards
  7. Authentication Strategies
  8. Complete Example
  9. Best Practices

Overview

armature-auth provides a comprehensive authentication and authorization system inspired by NestJS:

  • Password Hashing: Bcrypt and Argon2 support
  • JWT Integration: Seamless integration with armature-jwt
  • Guards: Route protection with authentication and authorization
  • Role-Based Access Control (RBAC): Role and permission checking
  • Authentication Strategies: Pluggable authentication methods
  • User Context: Type-safe user information extraction

Installation

Add to your Cargo.toml:

[dependencies]
armature-framework = { version = "0.1", features = ["auth"] }

The auth feature automatically includes armature-jwt.

Password Hashing

Supported Algorithms

armature-auth supports two password hashing algorithms:

  1. Argon2 (default, recommended)

    • Modern, memory-hard algorithm
    • Winner of Password Hashing Competition
    • Resistant to GPU cracking attacks
  2. Bcrypt

    • Battle-tested, widely used
    • Good compatibility
    • Slower than some modern alternatives

Basic Usage

use armature_auth::{PasswordHasher, PasswordVerifier};

// Default (Argon2)
let hasher = PasswordHasher::default();
let hash = hasher.hash("my-password")?;

// Verify
let is_valid = hasher.verify("my-password", &hash)?;
assert!(is_valid);

// Specific algorithm
use armature_auth::password::HashAlgorithm;
let bcrypt_hasher = PasswordHasher::new(HashAlgorithm::Bcrypt);
let hash = bcrypt_hasher.hash("my-password")?;

Auto-Detection

The hasher automatically detects the algorithm from the hash format:

let hasher = PasswordHasher::default();

// Can verify both Bcrypt and Argon2 hashes
let bcrypt_hash = "$2b$12$...";
let argon2_hash = "$argon2id$v=19$...";

hasher.verify("password", bcrypt_hash)?; // Works
hasher.verify("password", argon2_hash)?; // Also works

Authentication Service

The AuthService is the central authentication component:

use armature_auth::{AuthService, PasswordHasher};
use armature_jwt::{JwtConfig, JwtManager};

// Basic setup
let auth_service = AuthService::new();

// With JWT
let jwt_config = JwtConfig::new("your-secret".to_string());
let jwt_manager = JwtManager::new(jwt_config)?;
let auth_service = AuthService::with_jwt(jwt_manager);

// Custom password hasher
let hasher = PasswordHasher::new(HashAlgorithm::Bcrypt);
let auth_service = AuthService::new()
    .with_password_hasher(hasher);

Service Methods

// Hash a password
let hash = auth_service.hash_password("password")?;

// Verify a password
let is_valid = auth_service.verify_password("password", &hash)?;

// Validate a user
auth_service.validate(&user)?;

// Access JWT manager
if let Some(jwt) = auth_service.jwt_manager() {
    let token = jwt.sign(&claims)?;
}

User Management

Implementing AuthUser

Define your user type and implement the AuthUser trait:

use armature_auth::AuthUser;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
    id: String,
    email: String,
    password_hash: String,
    roles: Vec<String>,
    permissions: Vec<String>,
    active: bool,
}

impl AuthUser for User {
    fn user_id(&self) -> String {
        self.id.clone()
    }

    fn is_active(&self) -> bool {
        self.active
    }

    fn has_role(&self, role: &str) -> bool {
        self.roles.iter().any(|r| r == role)
    }

    fn has_permission(&self, permission: &str) -> bool {
        self.permissions.iter().any(|p| p == permission)
    }
}

Using UserContext

For simple use cases, use the built-in UserContext:

use armature_auth::UserContext;

let user = UserContext::new("user123".to_string())
    .with_email("user@example.com".to_string())
    .with_roles(vec!["admin".to_string(), "user".to_string()])
    .with_permissions(vec!["read".to_string(), "write".to_string()])
    .with_metadata(serde_json::json!({
        "name": "John Doe",
        "department": "Engineering"
    }));

// UserContext implements AuthUser
assert!(user.has_role("admin"));
assert!(user.has_permission("write"));

Guards

Guards protect routes by enforcing authentication and authorization rules.

Authentication Guard

Ensures the request has a valid token:

use armature_auth::AuthGuard;

let guard = AuthGuard::new();

// Check if request can proceed
if guard.can_activate(&request).await? {
    // Request is authenticated
}

Role Guard

Requires specific roles:

use armature_auth::RoleGuard;

// Require ANY of these roles
let guard = RoleGuard::any(vec!["admin".to_string(), "moderator".to_string()]);

// Require ALL of these roles
let guard = RoleGuard::all(vec!["admin".to_string(), "verified".to_string()]);

// Check user roles
let has_access = guard.check_roles(&user);

// Use in request handler
if guard.can_activate(&request).await? {
    // User has required roles
}

Permission Guard

Requires specific permissions:

use armature_auth::PermissionGuard;

// Require ANY of these permissions
let guard = PermissionGuard::any(vec![
    "posts:read".to_string(),
    "posts:list".to_string()
]);

// Require ALL of these permissions
let guard = PermissionGuard::all(vec![
    "posts:read".to_string(),
    "posts:write".to_string()
]);

let has_access = guard.check_permissions(&user);

Custom Guards

Implement the Guard trait for custom logic:

use armature_auth::Guard;
use async_trait::async_trait;

struct CustomGuard {
    // Your fields
}

#[async_trait]
impl Guard for CustomGuard {
    async fn can_activate(&self, request: &HttpRequest) -> Result<bool> {
        // Your custom logic
        Ok(true)
    }
}

Authentication Strategies

Strategies define how users are authenticated.

Local Strategy

Username/password authentication:

use armature_auth::{LocalStrategy, LocalCredentials};

let strategy = LocalStrategy::<User>::new();

let credentials = LocalCredentials {
    username: "user@example.com".to_string(),
    password: "password123".to_string(),
};

// In your implementation:
// 1. Find user by username
// 2. Verify password
// 3. Return authenticated user

JWT Strategy

Token-based authentication:

use armature_auth::JwtStrategy;
use armature_jwt::JwtManager;

let jwt_manager = JwtManager::new(jwt_config)?;
let strategy = JwtStrategy::<User>::new(jwt_manager);

// Extract token from header
let token = strategy.extract_token("Bearer eyJhbGc...")?;

// Verify and decode token
// Load user from database
// Return authenticated user

Complete Example

User Registration and Login

use armature_auth::{AuthService, AuthUser, UserContext};
use armature_jwt::{Claims, JwtConfig, JwtManager};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct RegisterRequest {
    email: String,
    password: String,
}

#[derive(Deserialize)]
struct LoginRequest {
    email: String,
    password: String,
}

#[derive(Serialize)]
struct AuthResponse {
    access_token: String,
    refresh_token: String,
    user: UserInfo,
}

#[derive(Serialize)]
struct UserInfo {
    id: String,
    email: String,
    roles: Vec<String>,
}

async fn register(
    auth_service: &AuthService,
    req: RegisterRequest,
) -> Result<User, Error> {
    // Hash password
    let password_hash = auth_service.hash_password(&req.password)?;

    // Create user
    let user = User {
        id: generate_id(),
        email: req.email,
        password_hash,
        roles: vec!["user".to_string()],
        active: true,
    };

    // Save to database
    save_user(&user).await?;

    Ok(user)
}

async fn login(
    auth_service: &AuthService,
    req: LoginRequest,
) -> Result<AuthResponse, Error> {
    // Find user
    let user = find_user_by_email(&req.email).await?;

    // Verify password
    if !auth_service.verify_password(&req.password, &user.password_hash)? {
        return Err(Error::InvalidCredentials);
    }

    // Validate user
    auth_service.validate(&user)?;

    // Generate tokens
    let jwt_manager = auth_service.jwt_manager().unwrap();

    let claims = Claims::new(UserContext::new(user.id.clone())
        .with_email(user.email.clone())
        .with_roles(user.roles.clone()))
        .with_subject(user.email.clone())
        .with_expiration(3600);

    let token_pair = jwt_manager.generate_token_pair(&claims)?;

    Ok(AuthResponse {
        access_token: token_pair.access_token,
        refresh_token: token_pair.refresh_token,
        user: UserInfo {
            id: user.id,
            email: user.email,
            roles: user.roles,
        },
    })
}

Protected Routes

use armature_auth::{AuthGuard, RoleGuard};

async fn protected_handler(request: HttpRequest) -> Result<Response, Error> {
    // Check authentication
    let auth_guard = AuthGuard::new();
    if !auth_guard.can_activate(&request).await? {
        return Err(Error::Unauthorized);
    }

    // Extract user
    let user = extract_user_from_request(&request)?;

    // Check roles
    let role_guard = RoleGuard::any(vec!["admin".to_string()]);
    if !role_guard.check_roles(&user) {
        return Err(Error::Forbidden);
    }

    // Handle request
    Ok(Response::ok("Protected resource"))
}

Middleware Pattern

async fn auth_middleware(
    request: HttpRequest,
    next: impl Fn(HttpRequest) -> Future<Output = Result<Response>>,
) -> Result<Response> {
    let guard = AuthGuard::new();

    if !guard.can_activate(&request).await? {
        return Err(Error::Unauthorized);
    }

    next(request).await
}

Best Practices

1. Password Security

// โœ“ Use Argon2 (default)
let hasher = PasswordHasher::default();

// โœ“ Or explicitly choose Argon2
let hasher = PasswordHasher::new(HashAlgorithm::Argon2);

// โš ๏ธ Bcrypt is OK but Argon2 is preferred
let hasher = PasswordHasher::new(HashAlgorithm::Bcrypt);

2. Token Management

// โœ“ Short-lived access tokens
let jwt_config = JwtConfig::new(secret)
    .with_expiration(Duration::from_secs(900)); // 15 minutes

// โœ“ Long-lived refresh tokens
let jwt_config = jwt_config
    .with_refresh_expiration(Duration::from_secs(604800)); // 7 days

// โœ“ Store secrets in environment variables
let secret = std::env::var("JWT_SECRET")?;

3. User Validation

// Always validate users after authentication
auth_service.validate(&user)?;

// Check user is active
if !user.is_active() {
    return Err(Error::InactiveUser);
}

4. Guard Composition

// Combine multiple guards
async fn admin_only_handler(request: HttpRequest) -> Result<Response> {
    // First: Authentication
    AuthGuard::new().can_activate(&request).await?;

    // Second: Authorization
    let user = extract_user(&request)?;
    let role_guard = RoleGuard::any(vec!["admin".to_string()]);

    if !role_guard.check_roles(&user) {
        return Err(Error::Forbidden);
    }

    // Handler logic
    Ok(Response::ok("Admin resource"))
}

5. Error Handling

use armature_auth::AuthError;

match auth_service.verify_password(password, hash) {
    Ok(true) => { /* Success */ },
    Ok(false) => return Err(AuthError::InvalidCredentials),
    Err(AuthError::PasswordVerifyError(e)) => {
        log::error!("Password verification error: {}", e);
        return Err(AuthError::AuthenticationFailed("Internal error".into()));
    },
    Err(e) => return Err(e),
}

6. Database Integration

// Store hashed passwords only
async fn create_user(email: String, password: String) -> Result<User> {
    let auth_service = AuthService::new();

    // Hash password
    let password_hash = auth_service.hash_password(&password)?;

    // NEVER store plain password
    let user = User {
        id: generate_id(),
        email,
        password_hash, // Store hash, not password
        roles: vec!["user".to_string()],
        active: true,
    };

    db.save(&user).await?;
    Ok(user)
}

7. Rate Limiting

// Implement rate limiting for auth endpoints
use std::collections::HashMap;
use std::time::{Duration, Instant};

struct RateLimiter {
    attempts: HashMap<String, (u32, Instant)>,
    max_attempts: u32,
    window: Duration,
}

impl RateLimiter {
    fn check(&mut self, email: &str) -> Result<(), AuthError> {
        let now = Instant::now();
        let entry = self.attempts.entry(email.to_string())
            .or_insert((0, now));

        if now.duration_since(entry.1) > self.window {
            *entry = (1, now);
            Ok(())
        } else if entry.0 >= self.max_attempts {
            Err(AuthError::AuthenticationFailed(
                "Too many attempts".into()
            ))
        } else {
            entry.0 += 1;
            Ok(())
        }
    }
}

Summary

The armature-auth module provides:

  • โœ… Secure password hashing with Bcrypt and Argon2
  • โœ… JWT integration for stateless authentication
  • โœ… Guards for route protection
  • โœ… RBAC with roles and permissions
  • โœ… Flexible strategies for different auth methods
  • โœ… Type-safe user context
  • โœ… DI integration with the Armature framework

For more examples, see:

  • examples/auth_complete.rs - Complete authentication demo
  • examples/jwt_simple.rs - JWT basics
  • docs/jwt-guide.md - JWT details (coming soon)

See Also