# Auth Architecture

> LiteLLM-RS Authentication Architecture. Covers JWT + API Key + RBAC multi-method auth, DashMap-backed rate limiting, the actix-web middleware pipeline, and credential management (gw- prefixed keys, HMAC hashing). Use when adding auth methods, debugging JWT/API-key validation, implementing RBAC permission checks, or tuning rate limiting and auth configuration.

- Skill: `majiayu000/auth-architecture` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add majiayu000/auth-architecture`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/auth-architecture/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/majiayu000/auth-architecture

---


# Authentication Architecture Guide

## Overview

`AuthSystem` (`src/auth/system.rs`) composes three subsystems behind one `authenticate()` entry point:

```rust
pub struct AuthSystem {
    config: Arc<AuthConfig>,
    storage: Arc<StorageLayer>,
    jwt: Arc<JwtHandler>,        // src/auth/jwt/types.rs
    api_key: Arc<ApiKeyHandler>, // src/auth/api_key/creation.rs
    rbac: Arc<RbacSystem>,       // src/auth/rbac/system.rs
}
```

`AuthSystem::authenticate(auth_method, context) -> Result<AuthResult>` dispatches on
`AuthMethod::{Jwt, ApiKey, Session, None}` (`src/auth/types.rs`). Session auth is a
stub that always rejects ("Session authentication is not yet implemented").
Semantic rejections return `AuthResult { success: false, error: Some(..) }`;
infrastructure failures return `Err(GatewayError)`.

### Middleware pipeline

Wired in `src/server/http.rs` (actix runs wraps in reverse registration order, so
the request path is outermost-first):

```
RequestIdMiddleware -> AuditMiddleware -> IpAccessMiddleware -> CORS/Metrics
  -> AuthMiddleware            (src/server/middleware/auth.rs)
  -> RateLimitMiddleware       (src/server/middleware/rate_limit.rs)
  -> SecurityHeadersMiddleware -> handler
```

`AuthMiddleware` per request: public-route bypass → fail-closed check when both
auth methods are disabled (`allow_anonymous` gate) → brute-force lockout via
`AuthRateLimiter` → credential extraction → authentication → endpoint/operation
authorization → insert `User` / `ApiKey` into request extensions.

---

## Credential Extraction

`extract_auth_method_with_api_key_header` (`src/server/middleware/helpers.rs`)
resolves credentials in this priority order:

1. `Authorization: Bearer <jwt>` → `AuthMethod::Jwt`
2. `Authorization: ApiKey <key>` → `AuthMethod::ApiKey`
3. `Authorization: gw-...` (raw key, no scheme) → `AuthMethod::ApiKey`
4. Configured API key header (`auth.api_key_header`, default `Authorization`)
5. `X-API-Key` fallback (when the configured header differs)
6. `session=<id>` cookie → `AuthMethod::Session`

There is no `Bearer sk-` form: gateway keys are `gw-` prefixed, and a raw
`sk-...` value matches nothing.

## API Key Authentication

### Key generation and hashing

Keys are generated by `generate_api_key()` in `src/utils/auth/crypto/keys.rs`:
a fixed `gw` prefix plus 32 alphanumeric characters (`gw-<32 chars>`, 35 total).
They are hashed — never stored in plaintext:

```rust
// src/utils/auth/crypto/keys.rs
pub fn generate_api_key() -> String;                       // "gw-" + 32 alphanumerics
pub fn hash_api_key(api_key: &str, hmac_secret: Option<&str>) -> String;
pub fn extract_api_key_prefix(api_key: &str) -> String;    // "gw-a...mnop" display only
```

`hash_api_key` computes HMAC-SHA256 when `api_key_hmac_secret` is configured,
otherwise plain SHA-256. Argon2 is used only for user passwords
(`src/utils/auth/crypto/password.rs`), never for API keys.

### Handler

`ApiKeyHandler::new(storage: Arc<StorageLayer>, hmac_secret: Option<String>)`
stores keys through the database layer and looks them up **by full hash**
(`find_api_key_by_hash`) — there is no prefix lookup.

```rust
impl ApiKeyHandler {
    pub async fn create_key(&self, user_id: Option<Uuid>, team_id: Option<Uuid>,
        name: String, permissions: Vec<String>) -> Result<(ApiKey, String)>; // (stored, raw)
    pub async fn verify_key(&self, raw_key: &str) -> Result<Option<(ApiKey, Option<User>)>>;
    pub async fn verify_key_detailed(&self, raw_key: &str) -> Result<ApiKeyVerification>;
}
```

Verification rejects inactive or expired keys and keys whose owner user is
missing/inactive; it refreshes `last_used_at` throttled to once per 5 minutes
(`LAST_USED_THROTTLE`) via an in-memory `DashMap<Uuid, Instant>` cache.
Names must be 1–255 chars without control characters; permissions must be from
`VALID_PERMISSIONS` (creation.rs): `*`, `system.admin`, `analytics.read`,
`api.chat`, `api.embeddings`, `api.images`, and dotted read/write/delete grants
on `users`, `teams`, `api_keys` (e.g. `users.read`, `api_keys.delete`).
Management lives on the same handler: `revoke_key`, `list_user_keys`,
`update_permissions`, `update_expiration`, `regenerate_key` (returns a new raw
key), `cleanup_expired_keys` (`src/auth/api_key/management.rs`).

## JWT Authentication

### Claims

Single `role: String` (not a role list), plus token identity fields
(`src/auth/jwt/types.rs`):

```rust
pub struct Claims {
    pub sub: Uuid,                  // user ID
    pub iat: u64, pub exp: u64,
    pub iss: String,                // fixed "litellm-rs"
    pub aud: String,                // "api" for access, "refresh" for refresh tokens
    pub jti: String,                // UUID token ID
    pub role: String,
    pub permissions: Vec<String>,
    pub team_id: Option<Uuid>,
    pub session_id: Option<String>,
    pub token_type: TokenType,      // Access | Refresh | PasswordReset |
}                                   // EmailVerification | Invitation
```

### Handler

`JwtHandler::new(config: &AuthConfig)` builds HS256 signing keys from
`jwt_secret`; lifetime comes from `jwt_expiration`. There is no configurable
audience field — audiences are hard-coded per token kind.

```rust
impl JwtHandler {
    pub async fn create_access_token(&self, user_id: Uuid, role: String,
        permissions: Vec<String>, team_id: Option<Uuid>, session_id: Option<Uuid>)
        -> Result<String>;
    pub async fn create_refresh_token(&self, user_id: Uuid,
        session_id: Option<String>) -> Result<String>;      // exp = expiration * 24
    pub async fn create_token_pair(...) -> Result<TokenPair>;
    pub async fn verify_access_token(&self, token: &str) -> Result<Claims>;
    pub async fn verify_refresh_token(&self, token: &str) -> Result<Uuid>;
}
```

For both public creation methods, `team_id` must currently be `None`; passing `Some(...)`
returns `BadRequest("Active team selection requires verified membership")`.
Team-scoped access tokens and token pairs are created only through the crate-private
`create_access_token_for_verified_team` / `create_token_pair_for_verified_team` paths
after active membership has been verified.

`verify_access_token` enforces `aud == "api"` and rejects unknown team-scope
versions; `verify_refresh_token` enforces `aud == "refresh"` plus
`token_type == Refresh`. JWT decode failures map to
`GatewayError::Auth("JWT error: ...")`.

## References

- [reference/middleware-pipeline.md](reference/middleware-pipeline.md) — AuthMiddleware wiring, brute-force lockout, route-level permission checks
- [reference/rbac.md](reference/rbac.md) — Permission/Role structs, RbacSystem checks, role inheritance, default roles
- [reference/rate-limiting.md](reference/rate-limiting.md) — DashMap RateLimiter strategies, Redis backend, rate-limit key policy
- [reference/configuration.md](reference/configuration.md) — flat `auth:` YAML surface, validation rules, top-level `rate_limit:` section
- [reference/security-best-practices.md](reference/security-best-practices.md) — secret policy, HMAC key hashing, redaction, audit events, key lifecycle
- [reference/error-types.md](reference/error-types.md) — GatewayError variants used across the auth stack and their HTTP mappings

