Skip to Content
API Service (Rust)Middleware Pipeline

Middleware Pipeline

Chain Order

Every HTTP request passes through the following middleware stack in order:

Layers 1–2 apply globally to every request.
Layer 4 applies per route group (separate tower_governor instances for auth, contact, user/chat/admin/resources).
Layer 5 applies to all routes except /health, /auth, and /contact.
Layer 6 applies to protected role-gated routes (/admin/* and /resources/*).

auth_middleware

Injected as an Axum middleware layer on protected router groups including /user, /chat, /admin, and /resources.

Implementation:

  1. Reads Authorization: Bearer <token> header
  2. Queries sessions table: SELECT s.user_id, s.role FROM sessions s WHERE s.session_token = $1 AND s.expires_at > NOW()
  3. If found: inserts user_id: Uuid and role: Role into Request extensions
  4. If not found or expired: returns 401 Unauthorized

Optimization: Session rows store the user’s role directly (duplicated from users.role). This means a single DB query resolves both identity and authorization level — no secondary lookup against the users table.

Cost: One DB query per authenticated request. With a PgPool of 10 connections and a 20-connection max overflow, this is the primary connection consumer under load.

require_admin

Applied as an additional extractor on /admin route handlers:

pub async fn require_admin( Extension(role): Extension<Role>, ) -> Result<(), StatusCode> { match role { Role::Admin => Ok(()), _ => Err(StatusCode::FORBIDDEN), } }

Reads role from the request extensions set by auth_middleware. Zero database queries — pure memory read.

require_contributor_or_admin

Applied to contributor submission endpoints under /resources/submissions*.

if !role.can_submit_resources() { return Err(StatusCode::FORBIDDEN); }

Allows Role::Contributor and Role::Admin, rejects Role::User.

Rate Limiting

Uses tower_governor with PeerIpKeyExtractor (limits by client IP).

LayerEndpointsRateBurst
Standard/auth/signin, /auth/signup, OAuth endpoints6/s10
Strict/contact~1/20s3
Sensitive/auth/forgot-password, /auth/reset-password, /auth/resend-verification, /auth/recover-account~1/20s3

Rate limit response: 429 Too Many Requests with Retry-After header.

Bypass risk: PeerIpKeyExtractor uses the direct TCP peer IP. Behind a reverse proxy, this would always be the proxy IP unless X-Forwarded-For extraction is configured. This is a known attack surface — all clients behind the same proxy share a single rate limit bucket (see Security Analysis).

CORS Configuration

Built in config/cors.rs via CorsLayer. Controlled by CORS_ALLOWED_ORIGINS env var:

  • * → wildcard CORS (development mode, allows all origins)
  • Comma-separated list → specific origins only

Wildcard CORS combined with localStorage token (rather than httpOnly cookies) means CORS does not provide meaningful protection against credential theft — the token is accessible to any same-origin JS.

Tower Stack Composition

// Simplified router assembly in gateway/mod.rs let app = Router::new() .nest("/health", health_router()) .nest("/auth", auth_router().layer(rate_limit_standard)) .nest("/contact", contact_router().layer(strict_rate_limiter)) .nest("/user", user_router().layer(auth_layer)) .nest("/chat", chat_router().layer(auth_layer)) .nest("/admin", admin_router() .layer(require_admin_layer) .layer(auth_layer)) .nest("/resources", resources_submit_router() .layer(require_contributor_or_admin_layer) .layer(auth_layer)) .nest("/resources", resources_queue_router() .layer(require_admin_layer) .layer(auth_layer)) .nest("/resources", resources_admin_management_router() .layer(require_admin_layer) .layer(auth_layer)) .layer(cors_layer) .layer(trace_layer) .with_state(app_state)
Last updated on