What You Will Build
By the end of this tutorial, you will have a production-ready Rust authentication service with: secure password hashing (Argon2id), JWT issuance and validation, session management, authentication middleware for both Actix-web and Axum, and H33 post-quantum biometric verification integrated via a single API call. Every code example compiles and runs.
Why Rust for Authentication
Authentication sits on the critical path of every request. It is the single highest-frequency security operation in any web application. A slow auth layer compounds latency across every endpoint behind it. A memory-unsafe auth layer is an attack surface measured in CVEs per year. Rust eliminates both problems simultaneously.
Rust's ownership model provides memory safety without garbage collection pauses. There is no stop-the-world event in the middle of a token validation. The borrow checker enforces at compile time that secret key material cannot be aliased, leaked through dangling references, or used after being freed. These are not theoretical benefits — they are the exact vulnerability classes that have produced critical CVEs in authentication libraries written in C, C++, and even managed languages with unsafe FFI bindings.
Beyond safety, Rust's performance profile matters for authentication at scale. When you are processing millions of auth requests per second, the difference between a 500-microsecond and a 50-microsecond authentication translates directly into server cost. H33's production stack authenticates at ~50 microseconds per user and sustains over 1.2 million authentications per second on a single machine. That throughput is only possible because the entire pipeline is native Rust with zero allocation in the hot path.
A typical Node.js/Express auth middleware adds 2-8 milliseconds per request. A Go auth middleware adds 200-800 microseconds. Rust with H33 completes full post-quantum authentication, including FHE biometric verification, ZK proof validation, and Dilithium signature attestation in ~50 microseconds. That is 40-160x faster than the Node.js baseline.
This tutorial covers the full journey: starting with classical authentication patterns in Rust (Argon2 hashing, JWTs, sessions, middleware), then layering in H33's quantum-resistant auth API. Whether you are migrating from another language or building greenfield, the patterns here are production-tested.
Prerequisites and Project Setup
You need Rust 1.75+ (for async trait stabilization), an H33 API key (free tier gives you 10,000 calls/month), and a basic understanding of async Rust. We will use Actix-web for the primary examples and show Axum equivalents where the patterns diverge.
Create the Project
cargo new h33-auth-service
cd h33-auth-service
Dependencies
# Cargo.toml [dependencies] # Web framework (choose one) actix-web = "4" actix-rt = "2" # Password hashing argon2 = "0.5" # JWT tokens jsonwebtoken = "9" # Serialization serde = { version = "1", features = ["derive"] } serde_json = "1" # Async HTTP client (for H33 API calls) reqwest = { version = "0.12", features = ["json", "rustls-tls"] } # Time handling chrono = { version = "0.4", features = ["serde"] } # Environment config dotenvy = "0.15" # Async runtime tokio = { version = "1", features = ["full"] } # UUID generation for session IDs uuid = { version = "1", features = ["v4"] } # Secure random bytes rand = "0.8" # Secret zeroization zeroize = { version = "1", features = ["derive"] }
A note on the zeroize crate: any struct that holds secret material (password hashes, JWT signing keys, API tokens) should derive ZeroizeOnDrop. This guarantees that when the struct goes out of scope, its memory is overwritten with zeros before being returned to the allocator. This is not optional hygiene — it is a defense against memory-scraping attacks and core dumps leaking credentials.
Secure Password Hashing with Argon2
Password hashing is the foundation of credential-based authentication. The correct choice in 2026 is Argon2id — the winner of the Password Hashing Competition, combining resistance to both GPU-based brute force (memory hardness) and side-channel attacks (data-independent memory access in the first pass). Do not use bcrypt for new projects. Do not use PBKDF2. Both are inferior to Argon2id on every axis that matters.
use argon2::{ password_hash::{ rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, }, Algorithm, Argon2, Params, Version, }; /// Hash a password with Argon2id using OWASP-recommended parameters. /// Returns a PHC-formatted string containing algorithm, params, salt, and hash. pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> { // OWASP 2024 recommended: m=19456 (19 MiB), t=2, p=1 let params = Params::new( 19_456, // memory cost in KiB (19 MiB) 2, // time cost (iterations) 1, // parallelism None, // output length (default 32 bytes) )?; let argon2 = Argon2::new( Algorithm::Argon2id, // hybrid: side-channel resistant + GPU resistant Version::V0x13, // version 1.3 params, ); let salt = SaltString::generate(&mut OsRng); let hash = argon2.hash_password(password.as_bytes(), &salt)?; Ok(hash.to_string()) } /// Verify a password against a stored PHC-formatted hash. /// Returns true if the password matches. Constant-time comparison internally. pub fn verify_password(password: &str, hash: &str) -> bool { let parsed = match PasswordHash::new(hash) { Ok(h) => h, Err(_) => return false, }; Argon2::default() .verify_password(password.as_bytes(), &parsed) .is_ok() }
The argon2 crate handles salt generation, constant-time comparison, and PHC string encoding. Do not attempt to implement any of these yourself. A single timing side-channel in password comparison can leak password lengths and prefix matches. The crate's verify_password method uses constant-time byte comparison internally.
Parameter Selection
The Argon2id parameters above follow OWASP's 2024 recommendations. The tradeoff is simple: higher memory cost means slower brute-force attacks but more server RAM per concurrent hash operation. For a server handling 100 concurrent logins, 19 MiB per hash means ~1.9 GiB of peak memory for hashing alone. If that is too high for your infrastructure, the minimum acceptable configuration is m=12288, t=3, p=1.
| Profile | Memory (KiB) | Iterations | Parallelism | Hash Time (approx) | Use Case |
|---|---|---|---|---|---|
| OWASP Recommended | 19,456 | 2 | 1 | ~250ms | Standard web apps |
| High Security | 65,536 | 3 | 4 | ~800ms | Financial, healthcare |
| Minimum Acceptable | 12,288 | 3 | 1 | ~150ms | Resource-constrained |
Password hashing is intentionally slow — that is the entire point. It should take 200-1000ms per hash. If your hash completes in under 100ms, your parameters are too weak. Conversely, authentication via H33's biometric API does not involve password hashing at all — it operates on encrypted biometric templates using fully homomorphic encryption, which runs at ~50 microseconds per auth. The two approaches serve different layers of the security stack.
JWT Authentication: Issuance and Validation
JSON Web Tokens (JWTs) are the standard mechanism for stateless authentication in REST APIs. The jsonwebtoken crate provides a well-audited, zero-copy implementation. We will build a complete JWT module with access tokens, refresh tokens, and proper claims validation.
Token Claims Structure
use chrono::{Duration, Utc}; use jsonwebtoken::{ decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation, }; use serde::{Deserialize, Serialize}; use zeroize::ZeroizeOnDrop; /// JWT claims for access tokens. #[derive(Debug, Serialize, Deserialize)] pub struct Claims { pub sub: String, // user ID pub email: String, // user email pub role: String, // user role (admin, user, etc.) pub exp: usize, // expiration (UNIX timestamp) pub iat: usize, // issued at pub jti: String, // unique token ID (for revocation) pub h33_attested: bool, // was this auth H33-verified? } /// Holds the JWT signing secret. Zeroized from memory on drop. #[derive(Clone, ZeroizeOnDrop)] pub struct JwtSecret { secret: Vec<u8>, } impl JwtSecret { pub fn from_env() -> Self { let secret = std::env::var("JWT_SECRET") .expect("JWT_SECRET must be set"); Self { secret: secret.into_bytes() } } pub fn encoding_key(&self) -> EncodingKey { EncodingKey::from_secret(&self.secret) } pub fn decoding_key(&self) -> DecodingKey { DecodingKey::from_secret(&self.secret) } }
Token Issuance
/// Create an access token. Short-lived (15 minutes). pub fn create_access_token( user_id: &str, email: &str, role: &str, h33_attested: bool, secret: &JwtSecret, ) -> Result<String, jsonwebtoken::errors::Error> { let now = Utc::now(); let claims = Claims { sub: user_id.to_owned(), email: email.to_owned(), role: role.to_owned(), exp: (now + Duration::minutes(15)).timestamp() as usize, iat: now.timestamp() as usize, jti: uuid::Uuid::new_v4().to_string(), h33_attested, }; encode(&Header::default(), &claims, &secret.encoding_key()) } /// Validate and decode an access token. pub fn validate_token( token: &str, secret: &JwtSecret, ) -> Result<TokenData<Claims>, jsonwebtoken::errors::Error> { let mut validation = Validation::default(); validation.validate_exp = true; validation.leeway = 30; // 30 second clock skew tolerance decode::<Claims>(token, &secret.decoding_key(), &validation) }
Notice the h33_attested field in the claims. This boolean indicates whether the authentication was verified through H33's post-quantum pipeline. Downstream services can use this claim to enforce different authorization policies: for example, requiring H33 attestation for financial transactions while accepting password-only auth for profile updates.
Token Lifetime Best Practices
Access tokens: 15 minutes maximum. Short-lived tokens limit the blast radius of token theft. A stolen access token is useless after 15 minutes without the refresh token.
Refresh tokens: 7 days, stored in httpOnly cookies only. Never in localStorage, never in sessionStorage. Rotate on every use (one-time-use refresh tokens).
H33 attestation tokens: These are Dilithium-signed and verified server-side. They are not JWTs — they are cryptographic attestation blobs with their own verification path. See the Nested Hybrid Signatures post for details on the signature chain.
Building Auth Routes with Actix-web
Now we wire the password hashing and JWT modules into actual HTTP endpoints. Actix-web's extractor pattern makes this ergonomic: request bodies are deserialized and validated before the handler runs, and errors are automatically converted to HTTP responses.
Request and Response Types
use actix_web::{web, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub struct RegisterRequest { pub email: String, pub password: String, pub name: String, } #[derive(Deserialize)] pub struct LoginRequest { pub email: String, pub password: String, } #[derive(Serialize)] pub struct AuthResponse { pub access_token: String, pub token_type: String, pub expires_in: u64, pub h33_attested: bool, }
Registration Handler
use crate::auth::password::{hash_password, verify_password}; use crate::auth::jwt::{create_access_token, JwtSecret}; /// POST /api/auth/register /// Creates a new user account with Argon2id-hashed password. pub async fn register( body: web::Json<RegisterRequest>, db: web::Data<DbPool>, jwt: web::Data<JwtSecret>, ) -> impl Responder { // Validate input if body.password.len() < 12 { return HttpResponse::BadRequest() .json(serde_json::json!({ "error": "Password must be at least 12 characters" })); } // Hash password (this is intentionally slow: ~250ms) let password_hash = match hash_password(&body.password) { Ok(h) => h, Err(_) => return HttpResponse::InternalServerError() .json(serde_json::json!({ "error": "Failed to hash password" })), }; // Insert user into database (pseudocode — use your ORM) let user_id = db.insert_user(&body.email, &body.name, &password_hash) .await .map_err(|_| HttpResponse::Conflict() .json(serde_json::json!({ "error": "Email already registered" })))?; // Issue JWT let token = create_access_token( &user_id.to_string(), &body.email, "user", false, // not H33-attested (password-only registration) &jwt, ).unwrap(); HttpResponse::Created().json(AuthResponse { access_token: token, token_type: "Bearer".to_string(), expires_in: 900, // 15 minutes h33_attested: false, }) }
Login Handler
/// POST /api/auth/login /// Verifies credentials and issues a JWT access token. pub async fn login( body: web::Json<LoginRequest>, db: web::Data<DbPool>, jwt: web::Data<JwtSecret>, ) -> impl Responder { // Fetch user by email let user = match db.get_user_by_email(&body.email).await { Some(u) => u, None => { // IMPORTANT: hash a dummy password to prevent timing oracle. // An attacker should not be able to distinguish "user not found" // from "wrong password" based on response time. let _ = hash_password("dummy_password_for_timing"); return HttpResponse::Unauthorized() .json(serde_json::json!({ "error": "Invalid email or password" })); } }; // Verify password (constant-time comparison inside) if !verify_password(&body.password, &user.password_hash) { return HttpResponse::Unauthorized() .json(serde_json::json!({ "error": "Invalid email or password" })); } // Issue JWT let token = create_access_token( &user.id.to_string(), &user.email, &user.role, false, &jwt, ).unwrap(); HttpResponse::Ok().json(AuthResponse { access_token: token, token_type: "Bearer".to_string(), expires_in: 900, h33_attested: false, }) }
When a user is not found, we still run hash_password with a dummy value. This ensures the response time is identical whether the user exists or not. Without this, an attacker can enumerate valid email addresses by measuring response latency. This is a real-world attack that has been used against production systems.
Authentication Middleware
Middleware is where authentication enforcement lives. Every protected route passes through middleware that extracts the JWT from the Authorization header, validates it, and injects the decoded claims into the request context. Requests with missing, expired, or invalid tokens are rejected before reaching the handler.
Actix-web Middleware
use actix_web::{ dev::{ServiceRequest, ServiceResponse, Transform, Service}, Error, HttpMessage, HttpResponse, web, }; use futures::future::{ok, Ready, LocalBoxFuture}; use crate::auth::jwt::{validate_token, Claims, JwtSecret}; pub struct AuthMiddleware; impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware where S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse<B>; type Error = Error; type Transform = AuthMiddlewareService<S>; type InitError = (); type Future = Ready<Result<Self::Transform, Self::InitError>>; fn new_transform(&self, service: S) -> Self::Future { ok(AuthMiddlewareService { service }) } } pub struct AuthMiddlewareService<S> { service: S } impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S> where S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse<B>; type Error = Error; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; fn call(&self, req: ServiceRequest) -> Self::Future { // Extract Bearer token from Authorization header let auth_header = req.headers() .get("Authorization") .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")); let token = match auth_header { Some(t) => t.to_string(), None => { return Box::pin(async { Err(actix_web::error::ErrorUnauthorized( "Missing Authorization header" )) }); } }; // Validate the JWT let jwt_secret = req.app_data::<web::Data<JwtSecret>>() .expect("JwtSecret not configured") .clone(); match validate_token(&token, &jwt_secret) { Ok(token_data) => { // Inject claims into request extensions req.extensions_mut().insert(token_data.claims); let fut = self.service.call(req); Box::pin(async move { fut.await }) } Err(_) => { Box::pin(async { Err(actix_web::error::ErrorUnauthorized( "Invalid or expired token" )) }) } } } }
Axum Equivalent
If you are using Axum instead of Actix-web, the middleware pattern is different. Axum uses tower layers and extractors. Here is the equivalent:
use axum::{ extract::State, http::{Request, StatusCode}, middleware::Next, response::Response, }; use crate::auth::jwt::{validate_token, Claims, JwtSecret}; /// Axum middleware: extracts and validates JWT from Authorization header. pub async fn auth_middleware( State(jwt_secret): State<JwtSecret>, mut req: Request<axum::body::Body>, next: Next, ) -> Result<Response, StatusCode> { let token = req .headers() .get("Authorization") .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")) .ok_or(StatusCode::UNAUTHORIZED)?; let token_data = validate_token(token, &jwt_secret) .map_err(|_| StatusCode::UNAUTHORIZED)?; // Insert claims into request extensions for downstream handlers req.extensions_mut().insert(token_data.claims); Ok(next.run(req).await) } // Usage in router: // let app = Router::new() // .route("/api/protected", get(protected_handler)) // .layer(middleware::from_fn_with_state(jwt_secret, auth_middleware));
Both patterns achieve the same result: the handler function receives a request with validated Claims already injected into the extension map. The handler never sees an unauthenticated request. This is the correct pattern for defense in depth — the middleware is the single enforcement point, and protected routes are structurally unreachable without a valid token.
Session Management
While JWTs handle stateless authentication for APIs, many applications also need server-side session state for web UIs — tracking active sessions, enabling forced logout, and providing session metadata. Here is a minimal session store backed by an in-memory HashMap (swap for Redis in production).
use std::collections::HashMap; use std::sync::RwLock; use chrono::{DateTime, Utc, Duration}; use uuid::Uuid; #[derive(Clone, Debug)] pub struct Session { pub user_id: String, pub created_at: DateTime<Utc>, pub expires_at: DateTime<Utc>, pub ip_address: String, pub user_agent: String, pub h33_session: bool, // H33-attested session } pub struct SessionStore { sessions: RwLock<HashMap<String, Session>>, } impl SessionStore { pub fn new() -> Self { Self { sessions: RwLock::new(HashMap::new()) } } /// Create a new session. Returns the session ID (a UUID v4). pub fn create( &self, user_id: &str, ip: &str, ua: &str, h33_attested: bool, ) -> String { let id = Uuid::new_v4().to_string(); let session = Session { user_id: user_id.to_owned(), created_at: Utc::now(), expires_at: Utc::now() + Duration::hours(24), ip_address: ip.to_owned(), user_agent: ua.to_owned(), h33_session: h33_attested, }; self.sessions.write().unwrap().insert(id.clone(), session); id } /// Validate a session by ID. Returns None if expired or not found. pub fn validate(&self, session_id: &str) -> Option<Session> { let sessions = self.sessions.read().unwrap(); sessions.get(session_id) .filter(|s| s.expires_at > Utc::now()) .cloned() } /// Revoke a session (logout). pub fn revoke(&self, session_id: &str) -> bool { self.sessions.write().unwrap().remove(session_id).is_some() } /// Revoke all sessions for a user (forced logout everywhere). pub fn revoke_all_for_user(&self, user_id: &str) { self.sessions.write().unwrap() .retain(|_, s| s.user_id != user_id); } }
In production, replace the RwLock<HashMap> with Redis or DynamoDB. The interface stays identical. The h33_session flag lets you distinguish between sessions created with password-only auth and those created with H33 biometric attestation, enabling different trust levels per session.
Integrating H33's Post-Quantum API
This is where Rust authentication goes from good to quantum-secure. H33's API handles the entire post-quantum authentication pipeline in a single call: FHE biometric matching, zero-knowledge proof validation, and Dilithium signature attestation. Your Rust service calls one endpoint and receives a signed attestation token. No cryptographic libraries to manage, no key rotation to orchestrate, no parameter tuning.
H33 Client Module
use reqwest::Client; use serde::{Deserialize, Serialize}; use zeroize::ZeroizeOnDrop; /// H33 API client. API key is zeroized from memory on drop. #[derive(Clone)] pub struct H33Client { client: Client, base_url: String, api_key: String, } #[derive(Serialize)] struct VerifyRequest { biometric_template: String, // base64-encoded encrypted template user_id: String, } #[derive(Deserialize, Debug)] pub struct H33VerifyResponse { pub verified: bool, pub confidence: f64, pub attestation_token: String, // Dilithium-signed pub latency_us: u64, pub pq_secure: bool, } impl H33Client { pub fn new(api_key: &str) -> Self { Self { client: Client::builder() .timeout(std::time::Duration::from_secs(5)) .pool_max_idle_per_host(20) .build() .expect("Failed to create HTTP client"), base_url: "https://api.h33.ai/v1".to_string(), api_key: api_key.to_string(), } } /// Verify a biometric template against H33's post-quantum pipeline. /// Returns an attestation token signed with Dilithium. pub async fn verify_biometric( &self, user_id: &str, biometric_template: &str, ) -> Result<H33VerifyResponse, reqwest::Error> { let resp = self.client .post(format!("{}/auth/verify", self.base_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .header("Content-Type", "application/json") .json(&VerifyRequest { biometric_template: biometric_template.to_string(), user_id: user_id.to_string(), }) .send() .await? .json::<H33VerifyResponse>() .await?; Ok(resp) } }
Combining Password Auth with H33 Biometrics
The most robust pattern is layered authentication: password (something you know) plus H33 biometric verification (something you are). The H33 call happens after password verification succeeds, adding a second factor without requiring a separate TOTP app or SMS code.
/// POST /api/auth/login-biometric /// Password + H33 biometric verification. Returns H33-attested JWT. pub async fn login_with_biometric( body: web::Json<BiometricLoginRequest>, db: web::Data<DbPool>, jwt: web::Data<JwtSecret>, h33: web::Data<H33Client>, ) -> impl Responder { // Step 1: Verify password let user = match db.get_user_by_email(&body.email).await { Some(u) => u, None => { let _ = hash_password("dummy"); return HttpResponse::Unauthorized() .json(serde_json::json!({ "error": "Invalid credentials" })); } }; if !verify_password(&body.password, &user.password_hash) { return HttpResponse::Unauthorized() .json(serde_json::json!({ "error": "Invalid credentials" })); } // Step 2: Verify biometric via H33 API let h33_result = match h33.verify_biometric( &user.id.to_string(), &body.biometric_template, ).await { Ok(r) if r.verified => r, Ok(_) => { return HttpResponse::Unauthorized() .json(serde_json::json!({ "error": "Biometric verification failed" })); } Err(e) => { eprintln!("H33 API error: {}", e); return HttpResponse::ServiceUnavailable() .json(serde_json::json!({ "error": "Biometric service unavailable" })); } }; // Step 3: Issue H33-attested JWT let token = create_access_token( &user.id.to_string(), &user.email, &user.role, true, // H33-attested! &jwt, ).unwrap(); HttpResponse::Ok().json(serde_json::json!({ "access_token": token, "token_type": "Bearer", "expires_in": 900, "h33_attested": true, "h33_latency_us": h33_result.latency_us, "attestation_token": h33_result.attestation_token, "pq_secure": h33_result.pq_secure, })) }
A single call to /v1/auth/verify executes the full H33 pipeline: (1) FHE inner product on the encrypted biometric template (the server never sees the raw biometric), (2) ZK-STARK proof validation that the matching was honest, and (3) a Dilithium signature over the result, producing a post-quantum-secure attestation token. Total latency: ~50 microseconds per user. Your Rust service gets back a verified/not-verified result plus a signed attestation token, and you never need to touch a cryptographic primitive.
Calling H33 Endpoints from Rust: Complete API Reference
Beyond biometric verification, the H33 API provides endpoints for enrollment, template management, attestation token verification, and hybrid signatures. Here are the most commonly used endpoints with Rust examples.
Enroll a User
/// Enroll a user's biometric template with H33. /// The template is encrypted client-side — H33 never sees the raw biometric. pub async fn enroll( &self, user_id: &str, encrypted_template: &str, ) -> Result<EnrollResponse, reqwest::Error> { self.client .post(format!("{}/auth/enroll", self.base_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .json(&serde_json::json!({ "user_id": user_id, "template": encrypted_template, "modality": "face", // or "fingerprint", "voice", etc. })) .send().await? .json().await }
Verify an Attestation Token
/// Verify a Dilithium-signed attestation token from H33. /// Use this to validate tokens passed between microservices. pub async fn verify_attestation( &self, attestation_token: &str, ) -> Result<AttestationVerifyResponse, reqwest::Error> { self.client .post(format!("{}/auth/verify-attestation", self.base_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .json(&serde_json::json!({ "attestation_token": attestation_token, })) .send().await? .json().await } #[derive(Deserialize, Debug)] pub struct AttestationVerifyResponse { pub valid: bool, pub user_id: String, pub issued_at: String, pub signature_algorithm: String, // "dilithium3" pub pq_secure: bool, }
Generate a Hybrid Signature
For applications requiring nested hybrid signatures (Ed25519 + Dilithium-5 + FALCON-512), the pattern is identical:
/// Sign a payload with H33's nested hybrid signature. /// Returns Ed25519 + Dilithium-5 + FALCON-512 triple signature. pub async fn hybrid_sign( &self, key_id: &str, payload: &[u8], ) -> Result<HybridSignResponse, reqwest::Error> { use base64::engine::{general_purpose::STANDARD, Engine}; self.client .post(format!("{}/hybrid/sign", self.base_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .json(&serde_json::json!({ "key_id": key_id, "payload": STANDARD.encode(payload), "mode": "triple", })) .send().await? .json().await }
Error Handling and Resilience
Production authentication must handle failure gracefully. The H33 API may be unreachable due to network issues; the database may be under load; the JWT secret may not be configured. Rust's Result type makes it impossible to forget about error cases, but the policy for handling errors in authentication is just as important as the mechanism.
Error Type Hierarchy
use actix_web::{HttpResponse, ResponseError}; use std::fmt; #[derive(Debug)] pub enum AuthError { // Client errors (4xx) InvalidCredentials, TokenExpired, TokenMalformed, MissingAuthHeader, BiometricMismatch, AccountLocked { until: String }, // Server errors (5xx) HashingFailed, DatabaseError(String), H33Unavailable(String), InternalError(String), } impl fmt::Display for AuthError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InvalidCredentials => write!(f, "Invalid credentials"), Self::TokenExpired => write!(f, "Token expired"), Self::TokenMalformed => write!(f, "Malformed token"), Self::MissingAuthHeader => write!(f, "Missing auth header"), Self::BiometricMismatch => write!(f, "Biometric mismatch"), Self::AccountLocked { until } => write!(f, "Account locked until {}", until), Self::HashingFailed => write!(f, "Internal error"), Self::DatabaseError(_) => write!(f, "Internal error"), Self::H33Unavailable(_) => write!(f, "Service unavailable"), Self::InternalError(_) => write!(f, "Internal error"), } } } impl ResponseError for AuthError { fn error_response(&self) -> HttpResponse { match self { Self::InvalidCredentials | Self::BiometricMismatch => HttpResponse::Unauthorized() .json(serde_json::json!({ "error": self.to_string() })), Self::TokenExpired | Self::TokenMalformed | Self::MissingAuthHeader => HttpResponse::Unauthorized() .json(serde_json::json!({ "error": self.to_string() })), Self::AccountLocked { .. } => HttpResponse::Forbidden() .json(serde_json::json!({ "error": self.to_string() })), Self::H33Unavailable(_) => HttpResponse::ServiceUnavailable() .json(serde_json::json!({ "error": "Service temporarily unavailable" })), _ => HttpResponse::InternalServerError() .json(serde_json::json!({ "error": "Internal server error" })), } } }
The Display implementation for server errors always returns a generic message. The actual database error string or H33 error detail is logged server-side but never exposed to the client. Leaking internal error details is an information disclosure vulnerability that aids attackers in mapping your infrastructure.
Rate Limiting and Brute Force Protection
Authentication endpoints are the primary target for credential stuffing and brute force attacks. Without rate limiting, an attacker with a leaked password list can attempt thousands of logins per second. Here is a basic token-bucket rate limiter built on Actix-web's middleware system.
use std::collections::HashMap; use std::sync::Mutex; use std::time::{Duration, Instant}; pub struct RateLimiter { buckets: Mutex<HashMap<String, (u32, Instant)>>, max_attempts: u32, window: Duration, } impl RateLimiter { /// Create a rate limiter: max_attempts per window duration. pub fn new(max_attempts: u32, window: Duration) -> Self { Self { buckets: Mutex::new(HashMap::new()), max_attempts, window, } } /// Check if the key (IP or email) is rate-limited. /// Returns Ok(remaining) or Err(retry_after_secs). pub fn check(&self, key: &str) -> Result<u32, u64> { let mut buckets = self.buckets.lock().unwrap(); let now = Instant::now(); let (count, window_start) = buckets .entry(key.to_string()) .or_insert((0, now)); // Reset window if expired if now.duration_since(*window_start) > self.window { *count = 0; *window_start = now; } if *count >= self.max_attempts { let retry_after = self.window .checked_sub(now.duration_since(*window_start)) .unwrap_or(Duration::from_secs(0)) .as_secs(); return Err(retry_after); } *count += 1; Ok(self.max_attempts - *count) } } // Usage in handler: // let limiter = RateLimiter::new(5, Duration::from_secs(300)); // match limiter.check(&client_ip) { // Err(retry) => return HttpResponse::TooManyRequests() // .insert_header(("Retry-After", retry.to_string())) // .json(json!({"error": "Too many attempts"})), // Ok(_remaining) => { /* proceed with auth */ } // }
For production systems, use a dual-key strategy: rate limit by both IP address and email address. This prevents distributed attacks from many IPs targeting one account, while also preventing a single IP from probing many accounts. The recommended thresholds are 5 attempts per 5 minutes per key, with exponential backoff after lockout.
Wiring It All Together: Application Entry Point
Here is the complete main.rs that assembles all the components: JWT configuration, H33 client initialization, route registration, and middleware application.
use actix_web::{web, App, HttpServer, middleware}; mod auth; mod h33; mod routes; mod session; mod errors; #[actix_web::main] async fn main() -> std::io::Result<()> { dotenvy::dotenv().ok(); // Initialize shared state let jwt_secret = auth::jwt::JwtSecret::from_env(); let h33_client = h33::client::H33Client::new( &std::env::var("H33_API_KEY").expect("H33_API_KEY required"), ); let session_store = web::Data::new(session::store::SessionStore::new()); HttpServer::new(move || { App::new() .app_data(web::Data::new(jwt_secret.clone())) .app_data(web::Data::new(h33_client.clone())) .app_data(session_store.clone()) // Public routes (no auth required) .route("/api/auth/register", web::post().to(routes::auth::register)) .route("/api/auth/login", web::post().to(routes::auth::login)) .route("/api/auth/login-biometric", web::post().to(routes::auth::login_with_biometric)) .route("/health", web::get().to(|| async { "OK" })) // Protected routes (JWT required) .service( web::scope("/api") .wrap(crate::middleware::auth::AuthMiddleware) .route("/me", web::get().to(routes::user::get_profile)) .route("/sessions", web::get().to(routes::session::list)) .route("/sessions/{id}", web::delete().to(routes::session::revoke)) ) }) .bind("0.0.0.0:8080")? .workers(num_cpus::get()) .run() .await }
Production Deployment Checklist
Before deploying your Rust authentication service, verify every item on this checklist. Missing any one of these is a potential security vulnerability.
| Category | Item | Status | Notes |
|---|---|---|---|
| TLS | All endpoints served over HTTPS | Required | Use rustls or terminate at load balancer |
| Secrets | JWT_SECRET from env, not hardcoded | Required | Minimum 256-bit entropy |
| Secrets | H33_API_KEY from env or secrets manager | Required | Never commit to source control |
| Rate Limit | Auth endpoints rate limited | Required | 5 attempts / 5 min per key |
| CORS | CORS configured with explicit origins | Required | Never use wildcard (*) on auth endpoints |
| Headers | Security headers set (HSTS, CSP, X-Frame) | Required | HSTS max-age >= 31536000 |
| Cookies | Refresh tokens in httpOnly, Secure, SameSite cookies | Required | Never expose refresh tokens to JS |
| Logging | Auth events logged (success, failure, lockout) | Recommended | Never log passwords or tokens |
| Binary | Static binary compiled with musl | Recommended | cross build --target x86_64-unknown-linux-musl |
| Monitoring | Failed auth rate monitored and alerted | Recommended | Alert on >10% failure rate |
Static Binary Deployment
One of Rust's operational advantages is the ability to compile to a fully static binary with zero runtime dependencies. This means no shared library version mismatches, no glibc compatibility issues, and a minimal container image (just the binary, no base OS layer).
# Build stage FROM rust:1.76-alpine AS builder RUN apk add --no-cache musl-dev WORKDIR /app COPY . . RUN cargo build --release --target x86_64-unknown-linux-musl # Runtime stage — scratch image, nothing but the binary FROM scratch COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/h33-auth-service / EXPOSE 8080 ENTRYPOINT ["/h33-auth-service"]
The final container image is typically 8-15 MB — compared to 200+ MB for a typical Node.js image or 100+ MB for a Go image with a distroless base. Smaller images mean faster deployments, faster scaling, and a smaller attack surface (there is nothing in the container to exploit besides the binary itself).
Performance Benchmarks: Rust vs. The Field
How does a Rust auth service compare to common alternatives? The following benchmarks measure end-to-end authentication latency (password verify + JWT issue) and throughput on a single machine. H33 biometric auth latency is measured separately because it replaces the password-verify step entirely.
Password + JWT Auth Latency (Single Request)
The password hashing step dominates latency in every framework. With H33's FHE biometric authentication, the password hash is eliminated entirely — replaced by an encrypted template match at ~50 microseconds. This is not an apples-to-oranges comparison; it is a fundamentally different architecture that removes the slowest step from the pipeline while adding quantum resistance.
Throughput at Scale
On a single c8g.metal-48xl (96 cores, AWS Graviton4), H33's Rust-native stack sustains over 1.2 million authentications per second. Each authentication includes FHE biometric matching, ZK-STARK proof validation, and Dilithium attestation signing. At ~50 microseconds per auth, this is 84x faster than a single Go bcrypt-based auth and 15x faster than the theoretical throughput ceiling of the fastest Node.js auth implementation. See the Sub-Millisecond Security deep dive for methodology.
Security Best Practices for Rust Auth Services
Beyond the code patterns above, here are the security practices that separate a production auth service from a tutorial project.
1. Zeroize All Secrets
Every struct that holds secret material must derive ZeroizeOnDrop or manually implement the Drop trait to zero memory. This includes JWT signing keys, database connection strings (which contain passwords), H33 API keys, and any intermediate buffers used during cryptographic operations.
2. Constant-Time Operations
All authentication decisions that involve secrets must use constant-time comparison. The argon2 crate handles this for password verification. For any custom comparison logic (API key validation, HMAC comparison), use the subtle crate's ConstantTimeEq trait. Never use == on secret byte slices.
3. Connection Pooling
Database connections are expensive to establish. Use deadpool-postgres or bb8 for connection pooling. The recommended pool size is 2x the number of CPU cores. For the H33 API client, reqwest's built-in connection pool is sufficient — set pool_max_idle_per_host(20) as shown in the client module above.
4. Audit Logging
Log every authentication event with structured metadata: user ID (if known), IP address, user agent, timestamp, outcome (success/failure/lockout), and whether H33 attestation was used. Never log passwords, tokens, or biometric data. Use the tracing crate for structured, async-aware logging.
5. Graceful Degradation
If the H33 API is unreachable, your service should degrade to password-only authentication rather than failing completely. The h33_attested claim in the JWT makes this transparent to downstream services: they can see that a particular session was not biometrically verified and adjust authorization accordingly.
The pattern presented in this tutorial implements three independent authentication layers: (1) password verification with Argon2id, (2) H33 biometric verification with FHE, and (3) JWT token issuance with short-lived access tokens. Each layer provides security independently. A failure in any single layer does not compromise the overall system. The h33_attested flag in the JWT lets downstream services enforce policies based on the strength of the authentication that was performed, not just its presence.
Testing Authentication Flows
Authentication code requires thorough testing because it is the security boundary of your application. Here is a testing strategy with concrete examples.
Unit Tests
#[cfg(test)] mod tests { use crate::auth::password::{hash_password, verify_password}; use crate::auth::jwt::{create_access_token, validate_token, JwtSecret}; #[test] fn test_password_hash_and_verify() { let password = "correct-horse-battery-staple"; let hash = hash_password(password).unwrap(); // Correct password matches assert!(verify_password(password, &hash)); // Wrong password does not match assert!(!verify_password("wrong-password", &hash)); // Different hashes for same password (random salt) let hash2 = hash_password(password).unwrap(); assert_ne!(hash, hash2); } #[test] fn test_jwt_roundtrip() { std::env::set_var("JWT_SECRET", "test-secret-key-min-256-bits-long!!"); let secret = JwtSecret::from_env(); let token = create_access_token( "user-123", "test@example.com", "user", true, &secret, ).unwrap(); let decoded = validate_token(&token, &secret).unwrap(); assert_eq!(decoded.claims.sub, "user-123"); assert_eq!(decoded.claims.email, "test@example.com"); assert!(decoded.claims.h33_attested); } #[test] fn test_expired_token_rejected() { std::env::set_var("JWT_SECRET", "test-secret-key-min-256-bits-long!!"); let secret = JwtSecret::from_env(); // Create a token that expired 1 hour ago let claims = Claims { sub: "user-123".to_string(), email: "test@example.com".to_string(), role: "user".to_string(), exp: (chrono::Utc::now() - chrono::Duration::hours(1)) .timestamp() as usize, iat: chrono::Utc::now().timestamp() as usize, jti: uuid::Uuid::new_v4().to_string(), h33_attested: false, }; let token = jsonwebtoken::encode( &jsonwebtoken::Header::default(), &claims, &secret.encoding_key(), ).unwrap(); // Should fail validation assert!(validate_token(&token, &secret).is_err()); } }
For integration tests, use Actix-web's built-in test server to send actual HTTP requests against your auth endpoints. For H33 API integration tests, use the H33 sandbox environment (set H33_API_URL=https://sandbox.h33.ai/v1) which accepts test biometric templates without counting against your quota. See the Testing Authentication Flows post for a comprehensive integration testing guide.
From Here: Next Steps
You now have a complete Rust authentication service with password hashing, JWT management, session tracking, rate limiting, and H33 post-quantum biometric integration. Here is where to go next:
- H33 API Documentation — Full reference for all H33 endpoints, including enrollment, verification, attestation, and hybrid signatures.
- H33 Quickstart Guide — Get your API key and run your first authentication in under 5 minutes.
- Full-Stack Auth Implementation — Extends this backend with a frontend integration (React, Vue, or Svelte).
- Nested Hybrid Signatures — Deep dive into H33's Ed25519 + Dilithium-5 + FALCON-512 signature chain for maximum cryptographic resilience.
- Enterprise Auth Architecture — Multi-tenant, multi-region deployment patterns with H33.
- Webhooks Implementation Guide — Receive real-time notifications for auth events.
- Error Handling Best Practices — Production error handling patterns for auth services.
Key Takeaways
Rust eliminates entire classes of auth vulnerabilities at compile time. Memory safety, type safety, and the ownership model mean no buffer overflows, no use-after-free, and no data races in your auth layer. Combined with H33's post-quantum API, you get authentication that is both the fastest and the most future-proof option available today.
H33 reduces the cryptographic surface area to a single API call. Instead of managing Argon2 parameters, key rotation schedules, and certificate infrastructure, you call one endpoint and get back a Dilithium-signed attestation token. The hard cryptography is handled by a system that sustains 1.2 million auths per second with post-quantum security.
Ready to Go Quantum-Secure?
Start protecting your users with post-quantum authentication today. Free tier includes 10,000 API calls/month, no credit card required.
Get Free API Key