axum-firebase-middleware

Crates.ioaxum-firebase-middleware
lib.rsaxum-firebase-middleware
version0.1.0
created_at2025-08-01 09:34:21.708607+00
updated_at2025-08-01 09:34:21.708607+00
descriptionAxum middleware for authenticating Firebase ID tokens
homepage
repositoryhttps://github.com/ekosutrisno/axum-firebase-middleware
max_upload_size
id1776329
size82,893
Eko Sutrisno (ekosutrisno)

documentation

https://docs.rs/axum-firebase-middleware

README

Axum Firebase Middleware

License

A production-ready Firebase JWT authentication middleware for Axum web applications. This crate provides secure token validation with automatic public key caching, comprehensive error handling, and built-in security features.

Features

  • Secure JWT validation with Firebase-specific claim verification
  • Automatic public key caching with configurable expiration and key rotation
  • Production-ready error handling with detailed, actionable error types
  • Security hardening including token length limits, timing validation, and DoS protection
  • Retry logic with exponential backoff for robust key fetching
  • Comprehensive logging for monitoring and debugging
  • Zero-copy where possible for optimal performance

Quick Start

Add this to your Cargo.toml:

[dependencies]
axum-firebase-middleware = "0.1.0"
axum = "0.8"
tokio = { version = "1.0", features = ["full"] }

Basic usage:

use axum::{
    routing::get,
    Router,
    Extension,
    Json,
    middleware::from_fn_with_state,
};
use axum_firebase_middleware::{firebase_auth_middleware, FirebaseConfig, FirebaseClaims};
use serde_json::json;

// Your protected handler
async fn protected_handler(
    Extension(claims): Extension<FirebaseClaims>
) -> Json<serde_json::Value> {
    Json(json!({
        "message": "Successfully authenticated!",
        "user_id": claims.user_id,
        "email": claims.email,
        "email_verified": claims.email_verified
    }))
}

// Public handler (no authentication required)
async fn public_handler() -> Json<serde_json::Value> {
    Json(json!({
        "message": "This endpoint is public"
    }))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize logging
    env_logger::init();

    // Create Firebase configuration
    let firebase_config = FirebaseConfig::new("your-firebase-project-id".to_string())?
        .with_cache_duration(3600)? // Cache keys for 1 hour
        .with_max_token_age(std::time::Duration::from_secs(24 * 3600)); // 24 hour max token age

    // Build the application with protected and public routes
    let app = Router::new()
        .route("/public", get(public_handler))
        .nest("/api/v1", 
            Router::new()
                .route("/protected", get(protected_handler))
                .route_layer(from_fn_with_state(
                    firebase_config.clone(), 
                    firebase_auth_middleware
                ))
        )
        .with_state(firebase_config);

    // Run the server
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    println!("Server running on http://0.0.0.0:3000");
    
    axum::serve(listener, app).await?;
    Ok(())
}

Usage

1. Get Firebase Project ID

Get your Firebase project ID from the Firebase Console:

  • Go to Firebase Console
  • Select your project
  • Click on Project Settings (gear icon)
  • Copy the Project ID

2. Client-side Token Generation

On your client (web, mobile app), authenticate users and get ID tokens:

// Web example using Firebase SDK
import { getAuth, signInWithEmailAndPassword, getIdToken } from 'firebase/auth';

const auth = getAuth();
const userCredential = await signInWithEmailAndPassword(auth, email, password);
const idToken = await getIdToken(userCredential.user);

// Use this token in Authorization header
fetch('/api/v1/protected', {
    headers: {
        'Authorization': `Bearer ${idToken}`
    }
});

3. Server-side Validation

The middleware automatically validates tokens and provides claims:

async fn user_profile(
    Extension(claims): Extension<FirebaseClaims>
) -> Json<serde_json::Value> {
    Json(json!({
        "user_id": claims.user_id,
        "email": claims.email,
        "email_verified": claims.email_verified,
        "auth_time": claims.auth_time,
        "sign_in_provider": claims.firebase.sign_in_provider
    }))
}

Configuration Options

Custom Cache Duration

let config = FirebaseConfig::new("project-id".to_string())?
    .with_cache_duration(1800)?; // Cache for 30 minutes

Custom Token Age Limits

let config = FirebaseConfig::new("project-id".to_string())?
    .with_max_token_age(Duration::from_secs(12 * 3600)); // 12 hours max

Error Handling

The middleware returns appropriate HTTP status codes:

  • 401 Unauthorized: Invalid, expired, or malformed tokens
  • 503 Service Unavailable: Firebase key service unavailable
  • 429 Too Many Requests: Rate limiting (if implemented)
  • 413 Payload Too Large: Request body too large
  • 500 Internal Server Error: Configuration errors

Custom error handling:

use axum_firebase_middleware::FirebaseAuthError;

match validate_firebase_token(&token, &config).await {
    Ok(claims) => {
        // Token is valid, use claims
        println!("User ID: {}", claims.user_id);
    }
    Err(FirebaseAuthError::InvalidTokenFormat(msg)) => {
        println!("Invalid token format: {}", msg);
    }
    Err(FirebaseAuthError::ValidationFailed(msg)) => {
        println!("Token validation failed: {}", msg);
    }
    Err(e) => {
        println!("Other error: {}", e);
    }
}

Security Features

  • Token length limits prevent DoS attacks
  • Timing validation prevents replay attacks with old tokens
  • Algorithm validation ensures only RS256 is accepted
  • Claim validation verifies Firebase-specific claims
  • Public key caching with automatic rotation
  • Request size limits prevent large payload attacks
  • Comprehensive input validation on all user inputs

Performance

  • Automatic public key caching reduces Firebase API calls
  • Efficient JWT validation using jsonwebtoken crate
  • Minimal memory allocations in hot paths
  • Configurable cache duration for optimal performance vs security trade-offs

Logging

The crate uses the log crate for structured logging:

// Initialize logging in your main function
env_logger::init();

// The middleware will log:
// - INFO: Successful key refreshes
// - WARN: Authentication failures, key fetch issues
// - DEBUG: Cache hits, successful validations
// - ERROR: Configuration issues, repeated failures

Log levels:

  • DEBUG: Cache operations, successful validations
  • INFO: Key refresh operations
  • WARN: Authentication failures, recoverable errors
  • ERROR: Configuration issues, persistent failures

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Development Setup

  1. Clone the repository
  2. Run tests: cargo test
  3. Check formatting: cargo fmt
  4. Run clippy: cargo clippy

License

This project is licensed under either of:

at your option.

Changelog

0.1.0

  • Initial release
  • Firebase JWT validation
  • Automatic key caching
  • Production-ready error handling
  • Comprehensive security features
Commit count: 0

cargo fmt