Skip to main content
โšก Calmops

JWT Authentication in Rust Web Services

Implementing Secure, Stateless Authentication with JSON Web Tokens

Introduction

Authentication is a critical yet often overlooked aspect of web service development. Getting it wrong can lead to:

  • Unauthorized access to sensitive data
  • Session hijacking and credential theft
  • Account takeover attacks
  • Compliance violations (GDPR, HIPAA, PCI-DSS)

JSON Web Tokens (JWT) have become the industry standard for stateless authentication in modern APIs. They’re compact, secure, and work seamlessly across distributed microservices without requiring shared session stores.

Rust’s strong type system and memory safety make it ideal for implementing authentication systems that are both secure and performant. This article explores how to build production-grade JWT authentication in Rust using frameworks like Axum.


Part 1: Core Concepts

What is JWT?

A JSON Web Token (JWT) is a compact, URL-safe string that contains encoded claims (user data). It has three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header           Payload          Signature
[Base64URL]      [Base64URL]      [HMAC]

Header - Declares the token type and hashing algorithm:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload - Contains claims (user data):

{
  "sub": "user-123",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516325422
}

Signature - Created by signing the header and payload with a secret:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

JWT Flow in Web Services

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Client     โ”‚                                    โ”‚   Server     โ”‚
โ”‚ (Browser)    โ”‚                                    โ”‚   (API)      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
      โ”‚                                                     โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ POST /login โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’  โ”‚
      โ”‚             (username, password)                   โ”‚
      โ”‚                                                     โ”‚
      โ”‚  โ† โ”€โ”€ โ”€โ”€ โ”€โ”€ JWT Token โ† โ”€โ”€ โ”€โ”€ โ”€โ”€ โ”€โ”€ โ”€โ”€ โ”€โ”€ โ”€โ”€ โ”€โ”€  โ”‚
      โ”‚                                                     โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ GET /api/users โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’  โ”‚
      โ”‚             Header: Authorization: Bearer TOKEN    โ”‚
      โ”‚                                                     โ”‚
      โ”‚  Verify Signature โœ“                                โ”‚
      โ”‚  Check Expiration โœ“                                โ”‚
      โ”‚                                                     โ”‚
      โ”‚  โ† โ”€โ”€ โ”€โ”€ โ”€โ”€ Protected Data โ† โ”€โ”€ โ”€โ”€ โ”€โ”€ โ”€โ”€ โ”€โ”€ โ”€โ”€   โ”‚
      โ”‚                                                     โ”‚

Authentication vs Authorization

  • Authentication: Verifying who you are (login with credentials)
  • Authorization: Verifying what you can do (role-based access)

JWT handles both by including claims (roles, permissions) in the token.

Why JWT Over Sessions?

Aspect JWT Session (Cookie)
State Stateless Requires server storage
Scalability No session store needed Needs shared cache
Mobile Works seamlessly Awkward with mobile apps
CORS Easy cross-origin Problematic
Microservices Share secret key Complex coordination
Security Signed/encrypted Secure if HTTPS

Part 2: Implementing JWT in Rust

Dependencies Setup

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
bcrypt = "0.15"
tracing = "0.1"
tracing-subscriber = "0.3"

JWT Claims Structure

use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

// Define claims that go into the JWT
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub sub: String,           // Subject (usually user ID)
    pub email: String,         // User email
    pub role: UserRole,        // User role
    pub iat: i64,              // Issued at
    pub exp: i64,              // Expiration time
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum UserRole {
    Admin,
    User,
    Guest,
}

impl Claims {
    pub fn new(user_id: String, email: String, role: UserRole) -> Self {
        let now = Utc::now();
        let iat = now.timestamp();
        let exp = (now + Duration::hours(24)).timestamp();

        Claims {
            sub: user_id,
            email,
            role,
            iat,
            exp,
        }
    }

    // Check if token is expired
    pub fn is_expired(&self) -> bool {
        Utc::now().timestamp() > self.exp
    }
}

// Secret configuration
pub struct JwtConfig {
    pub secret: String,
    pub algorithm: jsonwebtoken::Algorithm,
}

impl JwtConfig {
    pub fn new(secret: String) -> Self {
        JwtConfig {
            secret,
            algorithm: jsonwebtoken::Algorithm::HS256,
        }
    }

    pub fn encoding_key(&self) -> EncodingKey {
        EncodingKey::from_secret(self.secret.as_ref())
    }

    pub fn decoding_key(&self) -> DecodingKey {
        DecodingKey::from_secret(self.secret.as_ref())
    }
}

Issuing Tokens

use jsonwebtoken::Header;

pub fn issue_token(claims: &Claims, config: &JwtConfig) -> Result<String, Box<dyn std::error::Error>> {
    let token = encode(
        &Header::default(),
        claims,
        &config.encoding_key(),
    )?;

    Ok(token)
}

// Example usage
async fn login_handler(body: Json<LoginRequest>) -> Result<Json<LoginResponse>, AuthError> {
    // Validate credentials (compare password hashes with bcrypt)
    let user = authenticate_user(&body.email, &body.password).await?;

    // Create claims
    let claims = Claims::new(
        user.id.to_string(),
        user.email.clone(),
        user.role,
    );

    // Issue token
    let config = JwtConfig::new(std::env::var("JWT_SECRET")?);
    let token = issue_token(&claims, &config)
        .map_err(|_| AuthError::TokenGeneration)?;

    Ok(Json(LoginResponse { token }))
}

Verifying Tokens

use jsonwebtoken::Validation;

pub fn verify_token(token: &str, config: &JwtConfig) -> Result<Claims, Box<dyn std::error::Error>> {
    let data = decode::<Claims>(
        token,
        &config.decoding_key(),
        &Validation::new(config.algorithm),
    )?;

    let claims = data.claims;

    // Check expiration
    if claims.is_expired() {
        return Err("Token expired".into());
    }

    Ok(claims)
}

Part 3: Middleware for Token Extraction

Axum middleware automatically extracts and verifies tokens from requests:

use axum::{
    async_trait,
    extract::{FromRef, FromRequestParts},
    http::request::Parts,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;

#[derive(Debug)]
pub struct AuthError(pub String);

impl IntoResponse for AuthError {
    fn into_response(self) -> Response {
        let status = axum::http::StatusCode::UNAUTHORIZED;
        let body = Json(json!({
            "error": self.0,
            "status": 401,
        }));

        (status, body).into_response()
    }
}

// Application state containing JWT config
#[derive(Clone)]
pub struct AppState {
    pub jwt_config: JwtConfig,
}

// Extract claims from Authorization header
#[async_trait]
impl<S> FromRequestParts<S> for Claims
where
    JwtConfig: FromRef<S>,
    S: Send + Sync,
{
    type Rejection = AuthError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        // Extract Authorization header
        let auth_header = parts
            .headers
            .get("Authorization")
            .and_then(|h| h.to_str().ok())
            .ok_or(AuthError("Missing Authorization header".to_string()))?;

        // Parse "Bearer TOKEN"
        let token = auth_header
            .strip_prefix("Bearer ")
            .ok_or(AuthError("Invalid Authorization header format".to_string()))?;

        // Get JWT config from state
        let jwt_config = JwtConfig::from_ref(state);

        // Verify and extract claims
        verify_token(token, &jwt_config)
            .map_err(|e| AuthError(e.to_string()))
    }
}

// Extract claims only if role matches
pub struct RequireRole(pub UserRole);

#[async_trait]
impl<S> FromRequestParts<S> for RequireRole
where
    Claims: FromRequestParts<S, Rejection = AuthError>,
    S: Send + Sync,
{
    type Rejection = AuthError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let claims = Claims::from_request_parts(parts, state).await?;

        match claims.role {
            UserRole::Admin => Ok(RequireRole(UserRole::Admin)),
            _ => Err(AuthError("Insufficient permissions".to_string())),
        }
    }
}

Part 4: Complete Authentication Example

use axum::{
    extract::Json,
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};

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

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

#[derive(Debug, Serialize)]
pub struct UserProfile {
    pub id: String,
    pub email: String,
    pub role: String,
}

// Login endpoint - public
async fn login(
    Json(payload): Json<LoginRequest>,
    state: axum::extract::State<AppState>,
) -> Result<Json<LoginResponse>, AuthError> {
    // In production, verify password against bcrypt hash
    if payload.password.len() < 8 {
        return Err(AuthError("Invalid credentials".to_string()));
    }

    let user_id = uuid::Uuid::new_v4().to_string();
    let claims = Claims::new(user_id, payload.email, UserRole::User);

    let token = issue_token(&claims, &state.jwt_config)
        .map_err(|_| AuthError("Token generation failed".to_string()))?;

    Ok(Json(LoginResponse { token }))
}

// Protected endpoint - requires valid JWT
async fn get_profile(claims: Claims) -> Json<UserProfile> {
    Json(UserProfile {
        id: claims.sub.clone(),
        email: claims.email.clone(),
        role: format!("{:?}", claims.role),
    })
}

// Admin-only endpoint
async fn admin_dashboard(
    RequireRole(_): RequireRole,
    claims: Claims,
) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "message": format!("Welcome Admin {}", claims.email),
        "timestamp": chrono::Utc::now().to_rfc3339(),
    }))
}

// Create router with authentication
pub fn create_app(jwt_config: JwtConfig) -> Router {
    let state = AppState { jwt_config };

    Router::new()
        .route("/login", post(login))
        .route("/profile", get(get_profile))
        .route("/admin/dashboard", get(admin_dashboard))
        .with_state(state)
}

Part 5: Password Hashing with bcrypt

Never store passwords in plain text. Always hash them:

use bcrypt::{hash, verify, DEFAULT_COST};

pub struct UserRepository;

impl UserRepository {
    // Store hashed password
    pub async fn create_user(
        email: &str,
        password: &str,
    ) -> Result<User, Box<dyn std::error::Error>> {
        // Hash password with bcrypt
        let hashed = hash(password, DEFAULT_COST)?;

        // Store in database
        let user = User {
            id: uuid::Uuid::new_v4(),
            email: email.to_string(),
            password_hash: hashed,
            role: UserRole::User,
        };

        // Insert into database...
        Ok(user)
    }

    // Verify password
    pub async fn verify_password(
        stored_hash: &str,
        provided_password: &str,
    ) -> Result<bool, Box<dyn std::error::Error>> {
        Ok(verify(provided_password, stored_hash)?)
    }
}

// Usage in login
async fn authenticate_user(email: &str, password: &str) -> Result<User, AuthError> {
    let user = database::find_user_by_email(email)
        .await
        .map_err(|_| AuthError("User not found".to_string()))?;

    let valid = UserRepository::verify_password(&user.password_hash, password)
        .await
        .map_err(|_| AuthError("Authentication failed".to_string()))?;

    if valid {
        Ok(user)
    } else {
        Err(AuthError("Invalid credentials".to_string()))
    }
}

Part 6: Refresh Tokens

JWT tokens have expiration times. Handle token refresh:

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenPair {
    pub access_token: String,
    pub refresh_token: String,
}

// Shorter-lived access token
pub fn issue_access_token(claims: &Claims, config: &JwtConfig) -> Result<String, Box<dyn std::error::Error>> {
    let mut claims = claims.clone();
    claims.exp = (Utc::now() + Duration::minutes(15)).timestamp();
    encode(&Header::default(), &claims, &config.encoding_key()).map_err(|e| Box::new(e) as _)
}

// Longer-lived refresh token
pub fn issue_refresh_token(claims: &Claims, config: &JwtConfig) -> Result<String, Box<dyn std::error::Error>> {
    let mut claims = claims.clone();
    claims.exp = (Utc::now() + Duration::days(7)).timestamp();
    encode(&Header::default(), &claims, &config.encoding_key()).map_err(|e| Box::new(e) as _)
}

// Endpoint to refresh access token
async fn refresh_token(
    Json(payload): Json<serde_json::Value>,
    state: axum::extract::State<AppState>,
) -> Result<Json<TokenPair>, AuthError> {
    let refresh_token = payload["refresh_token"]
        .as_str()
        .ok_or(AuthError("Missing refresh token".to_string()))?;

    // Verify refresh token
    let claims = verify_token(refresh_token, &state.jwt_config)
        .map_err(|_| AuthError("Invalid refresh token".to_string()))?;

    // Issue new access token
    let access_token = issue_access_token(&claims, &state.jwt_config)
        .map_err(|_| AuthError("Token generation failed".to_string()))?;

    let new_refresh_token = issue_refresh_token(&claims, &state.jwt_config)
        .map_err(|_| AuthError("Token generation failed".to_string()))?;

    Ok(Json(TokenPair {
        access_token,
        refresh_token: new_refresh_token,
    }))
}

Part 7: Common Pitfalls & Best Practices

โŒ Pitfall: Storing Sensitive Data in JWT

// BAD: Putting sensitive data in JWT
let claims = Claims {
    sub: user.id.to_string(),
    email: user.email.clone(),
    role: user.role,
    // DON'T do this:
    password_hash: user.password_hash.clone(),  // Never!
    credit_card: user.credit_card.clone(),      // Never!
    ssn: user.ssn.clone(),                      // Never!
    iat: now.timestamp(),
    exp: exp.timestamp(),
};

// GOOD: Only include necessary claims
let claims = Claims {
    sub: user.id.to_string(),
    email: user.email.clone(),
    role: user.role,
    iat: now.timestamp(),
    exp: exp.timestamp(),
};

Why it matters: JWT is Base64-encoded, not encrypted. Secrets can be read by decoding the token.

โŒ Pitfall: Using Weak Secrets

// BAD: Weak secret
let secret = "my-super-secret";  // Easily guessed!

// GOOD: Use strong, random secret
let secret = std::env::var("JWT_SECRET")
    .expect("JWT_SECRET not set");
// Generate: openssl rand -base64 32

Why it matters: A weak secret allows attackers to forge valid tokens.

โŒ Pitfall: Not Validating Expiration

// BAD: Accepting expired tokens
let data = decode::<Claims>(token, &key, &Validation::new(Algorithm::HS256))?;
// Token could be expired!

// GOOD: Always check expiration
if claims.is_expired() {
    return Err(AuthError("Token expired".to_string()));
}

โœ… Best Practice: Use HTTPS

// Configure HTTPS in production
// This prevents token interception
let app = create_app(jwt_config);

let listener = tokio::net::TcpListener::bind("0.0.0.0:443")
    .await?;

// Use rustls or native-tls for TLS

โœ… Best Practice: Token Rotation

// Issue new token pair on each refresh
pub async fn refresh_token_handler(
    state: axum::extract::State<AppState>,
) -> Result<Json<TokenPair>, AuthError> {
    // Verify old refresh token
    let claims = verify_token(refresh_token, &state.jwt_config)?;

    // Issue completely new tokens
    let new_access = issue_access_token(&claims, &state.jwt_config)?;
    let new_refresh = issue_refresh_token(&claims, &state.jwt_config)?;

    // Invalidate old tokens in database if needed
    // database::revoke_token(refresh_token).await?;

    Ok(Json(TokenPair {
        access_token: new_access,
        refresh_token: new_refresh,
    }))
}

โœ… Best Practice: Token Revocation

// For logout or security events, revoke tokens
pub struct TokenBlacklist {
    revoked: std::collections::HashSet<String>,
}

impl TokenBlacklist {
    pub fn revoke(&mut self, token: &str) {
        self.revoked.insert(token.to_string());
    }

    pub fn is_revoked(&self, token: &str) -> bool {
        self.revoked.contains(token)
    }
}

// Check before verifying
async fn verify_with_blacklist(
    token: &str,
    config: &JwtConfig,
    blacklist: &TokenBlacklist,
) -> Result<Claims, AuthError> {
    if blacklist.is_revoked(token) {
        return Err(AuthError("Token revoked".to_string()));
    }

    verify_token(token, config)
        .map_err(|e| AuthError(e.to_string()))
}

Part 8: Architecture Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        Client (Frontend)                        โ”‚
โ”‚                    - Browser/Mobile App                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                โ”‚                           โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚  POST /login   โ”‚         โ”‚ GET /protected  โ”‚
        โ”‚ (credentials)  โ”‚         โ”‚ with JWT        โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                โ”‚                           โ”‚
                โ”‚                           โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚         Rust Web Service (Axum)                 โ”‚
    โ”‚                                                 โ”‚
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
    โ”‚  โ”‚  Login Handler                           โ”‚  โ”‚
    โ”‚  โ”‚  - Verify credentials (bcrypt hash)      โ”‚  โ”‚
    โ”‚  โ”‚  - Create Claims                         โ”‚  โ”‚
    โ”‚  โ”‚  - Issue JWT Token                       โ”‚  โ”‚
    โ”‚  โ”‚  - Return token to client                โ”‚  โ”‚
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
    โ”‚                                                 โ”‚
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
    โ”‚  โ”‚  Authentication Middleware               โ”‚  โ”‚
    โ”‚  โ”‚  - Extract token from header             โ”‚  โ”‚
    โ”‚  โ”‚  - Verify signature (HMAC)               โ”‚  โ”‚
    โ”‚  โ”‚  - Check expiration                      โ”‚  โ”‚
    โ”‚  โ”‚  - Extract Claims                        โ”‚  โ”‚
    โ”‚  โ”‚  - Return 401 if invalid                 โ”‚  โ”‚
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
    โ”‚                                                 โ”‚
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
    โ”‚  โ”‚  Protected Route Handler                 โ”‚  โ”‚
    โ”‚  โ”‚  - Access claims from request            โ”‚  โ”‚
    โ”‚  โ”‚  - Validate permissions                  โ”‚  โ”‚
    โ”‚  โ”‚  - Execute business logic                โ”‚  โ”‚
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
    โ”‚                                                 โ”‚
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
    โ”‚  โ”‚  JWT Config                              โ”‚  โ”‚
    โ”‚  โ”‚  - Secret (from env variable)            โ”‚  โ”‚
    โ”‚  โ”‚  - Algorithm (HS256)                     โ”‚  โ”‚
    โ”‚  โ”‚  - Key for signing/verifying             โ”‚  โ”‚
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
    โ”‚                                                 โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”˜
                                                   โ”‚
                        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                        โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚    Database (PostgreSQL)     โ”‚
        โ”‚  - User credentials          โ”‚
        โ”‚  - User profiles             โ”‚
        โ”‚  - Role mappings             โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Part 9: JWT Comparison with Alternatives

Approach JWT OAuth 2.0 Session Cookies API Keys
Stateless Yes Not always No No
Scalability Excellent Good Poor Good
Security Good if HTTPS Excellent Good if HTTPS Limited
Mobile-friendly Yes Yes Poor Yes
Complexity Simple Complex Simple Simple
Use Case Internal APIs Third-party Web apps Simple APIs

When to Use JWT

  • Microservices architecture
  • Mobile/SPA applications
  • Cross-domain requests
  • Internal APIs with controlled clients

When to Use OAuth 2.0

  • Third-party integrations
  • Delegated authorization
  • Social login
  • Complex permission models

When to Use Session Cookies

  • Traditional server-rendered web apps
  • Tight security requirements
  • Session-dependent features

Part 10: Testing Authentication

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_token_creation() {
        let claims = Claims::new(
            "user-123".to_string(),
            "[email protected]".to_string(),
            UserRole::User,
        );

        let config = JwtConfig::new("test-secret-key".to_string());
        let token = issue_token(&claims, &config).unwrap();

        assert!(!token.is_empty());
        assert_eq!(token.matches('.').count(), 2); // JWT has 3 parts
    }

    #[test]
    fn test_token_verification() {
        let claims = Claims::new(
            "user-123".to_string(),
            "[email protected]".to_string(),
            UserRole::User,
        );

        let config = JwtConfig::new("test-secret-key".to_string());
        let token = issue_token(&claims, &config).unwrap();

        let verified = verify_token(&token, &config).unwrap();
        assert_eq!(verified.sub, "user-123");
        assert_eq!(verified.email, "[email protected]");
    }

    #[test]
    fn test_token_expiration() {
        let mut claims = Claims::new(
            "user-123".to_string(),
            "[email protected]".to_string(),
            UserRole::User,
        );

        // Set expiration to past
        claims.exp = (Utc::now() - Duration::seconds(1)).timestamp();
        assert!(claims.is_expired());
    }

    #[test]
    fn test_invalid_signature() {
        let claims = Claims::new(
            "user-123".to_string(),
            "[email protected]".to_string(),
            UserRole::User,
        );

        let config1 = JwtConfig::new("secret-1".to_string());
        let token = issue_token(&claims, &config1).unwrap();

        // Try to verify with different secret
        let config2 = JwtConfig::new("secret-2".to_string());
        let result = verify_token(&token, &config2);

        assert!(result.is_err());
    }
}

Part 11: Resources & Further Reading

Official Documentation

Articles & Guides

Books

  • The Rust Programming Language - Chapter on lifetimes and ownership
  • Building Microservices with Rust - Security chapter
  • OAuth 2.0 in Action - Authentication patterns

Tools & Libraries

  • jsonwebtoken - JWT encoding/decoding
  • bcrypt - Password hashing
  • argon2 - Modern password hashing (alternative to bcrypt)
  • ring - Cryptographic primitives
  • openssl - SSL/TLS support

Part 12: Security Hardening

Environment Variables

# .env (never commit this!)
JWT_SECRET=your-random-secret-here-min-32-chars
JWT_EXPIRATION_HOURS=24
REFRESH_TOKEN_EXPIRATION_DAYS=7
BCRYPT_COST=12

HTTPS Configuration

use rustls_pemfile::{certs, rsa_private_keys};
use std::fs::File;
use std::io::BufReader;

pub async fn run_secure(app: Router) -> Result<()> {
    let config = rustls::ServerConfig::builder()
        .with_safe_defaults()
        .with_no_client_auth()
        .with_single_cert(certs(cert_reader)?, key)?;

    let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));

    // Listen on HTTPS
    let listener = tokio::net::TcpListener::bind("0.0.0.0:443").await?;
    
    axum::serve(
        listener,
        app.layer(axum::middleware::from_fn(
            move |req, next| {
                // Ensure HTTPS
                Ok(next.run(req).await)
            },
        )),
    )
    .await?;

    Ok(())
}

Rate Limiting

use tower_governor::governor::RateLimiter;

pub fn rate_limiting_layer() -> tower_governor::key_extractor::KeyExtractor {
    tower_governor::governor::RateLimiter::direct(
        std::num::NonZeroU32::new(100).unwrap(), // 100 requests
    )
}

// Apply to sensitive endpoints
let app = Router::new()
    .route("/login", post(login).layer(rate_limiting_layer()))
    .route("/refresh", post(refresh_token).layer(rate_limiting_layer()));

Part 13: Alternative Approaches

Token Alternatives to JWT

Token Type Pros Cons
JWT Stateless, compact, self-contained Difficult to revoke immediately
Opaque Tokens Easy to revoke, smaller Requires backend lookup
PASETO More secure defaults than JWT Less ecosystem
Structured Tokens Best of both Complex implementation

When to Use Opaque Tokens

// Generate random token, store in Redis
pub fn issue_opaque_token(user_id: &str) -> String {
    let token = uuid::Uuid::new_v4().to_string();
    
    // Store mapping: token -> user_id (expires in 24 hours)
    redis_client.setex(&token, 86400, user_id).unwrap();
    
    token
}

// Verify requires backend lookup
pub fn verify_opaque_token(token: &str) -> Result<String, AuthError> {
    let user_id = redis_client.get(token)
        .map_err(|_| AuthError("Invalid token".to_string()))?;
    
    Ok(user_id)
}

Conclusion

JWT authentication in Rust combines type safety, performance, and security to create robust API authentication systems. Key takeaways:

  1. JWTs are stateless - No server storage required
  2. Use bcrypt for passwords - Never store plaintext
  3. Always verify expiration - Tokens have lifetime limits
  4. Implement refresh tokens - Shorten access token lifetime
  5. Use HTTPS always - Prevent token interception
  6. Follow best practices - Rotate tokens, handle revocation, rate limit

By leveraging Rust’s type system through frameworks like Axum, you create authentication systems that are not just secure but also impossible to misuse at compile time.


Comments