| name | auth-architecture |
| description | LiteLLM-RS Authentication Architecture. Covers JWT + API Key + RBAC multi-method auth, rate limiting with DashMap, middleware pipeline, and secure credential management. |
Authentication Architecture Guide
Overview
LiteLLM-RS implements a multi-layered authentication system supporting JWT tokens, API keys, and Role-Based Access Control (RBAC) with lock-free rate limiting.
Authentication Flow
┌─────────────────────────────────────────────────────────────────┐
│ Request │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Auth Middleware │
│ 1. Extract credentials (JWT/API Key) │
│ 2. Validate credentials │
│ 3. Load user context │
│ 4. Check rate limits │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RBAC Middleware │
│ 1. Check required permissions │
│ 2. Validate resource access │
│ 3. Log access attempt │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Handler │
└─────────────────────────────────────────────────────────────────┘
API Key Authentication
Key Generation
use rand::Rng;
pub fn generate_api_key() -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const KEY_LENGTH: usize = 64;
let mut rng = rand::thread_rng();
let key: String = (0..KEY_LENGTH)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect();
format!("sk-{}", key)
}
Key Storage
use argon2::{Argon2, PasswordHasher, password_hash::SaltString};
use rand_core::OsRng;
pub struct ApiKeyManager {
storage: Arc<dyn KeyStorage>,
hasher: Argon2<'static>,
}
impl ApiKeyManager {
pub fn new(storage: Arc<dyn KeyStorage>) -> Self {
Self {
storage,
hasher: Argon2::default(),
}
}
pub async fn create_key(&self, user_id: &str, name: &str) -> Result<ApiKey, AuthError> {
let raw_key = generate_api_key();
let salt = SaltString::generate(&mut OsRng);
let hash = self.hasher
.hash_password(raw_key.as_bytes(), &salt)
.map_err(|e| AuthError::Internal(e.to_string()))?
.to_string();
let key = ApiKey {
id: uuid::Uuid::new_v4().to_string(),
user_id: user_id.(),
name: name.(),
key_hash: hash,
prefix: raw_key[..].(),
created_at: chrono::Utc::(),
expires_at: ,
last_used_at: ,
permissions: [],
};
.storage.(&key).?;
(ApiKey {
key_hash: raw_key,
..key
})
}
(&, raw_key: &) <ApiKey, AuthError> {
= &raw_key[...(raw_key.())];
= .storage
.(prefix)
.?
.(AuthError::InvalidCredentials)?;
= argon2::PasswordHash::(&key.key_hash)
.(|_| AuthError::InvalidCredentials)?;
.hasher
.(raw_key.(), &parsed_hash)
.(|_| AuthError::InvalidCredentials)?;
(expires_at) = key.expires_at {
expires_at < chrono::Utc::() {
(AuthError::ExpiredCredentials);
}
}
.storage.(&key.id).?;
(key)
}
}
API Key Middleware
use actix_web::{HttpRequest, HttpMessage, dev::ServiceRequest};
pub async fn api_key_middleware(
req: ServiceRequest,
key_manager: Arc<ApiKeyManager>,
) -> Result<ServiceRequest, AuthError> {
let api_key = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.ok_or(AuthError::MissingCredentials)?;
let key = key_manager.validate_key(api_key).await?;
req.extensions_mut().insert(AuthContext {
user_id: key.user_id.clone(),
permissions: key.permissions.clone(),
auth_method: AuthMethod::ApiKey,
});
Ok(req)
}
JWT Authentication
Token Structure
use serde::{Deserialize, Serialize};
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
pub iat: usize,
pub iss: String,
pub aud: String,
pub roles: Vec<String>,
pub permissions: Vec<String>,
}
pub struct JwtManager {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
issuer: String,
audience: String,
token_expiry: Duration,
}
impl JwtManager {
pub fn new(secret: &[u8], issuer: String, audience: String) -> Self {
Self {
encoding_key: EncodingKey::from_secret(secret),
decoding_key: DecodingKey::from_secret(secret),
issuer,
audience,
token_expiry: Duration::(),
}
}
(&, user_id: &, roles: <>, permissions: <>) <, AuthError> {
= chrono::Utc::();
= Claims {
sub: user_id.(),
exp: (now + chrono::Duration::(.token_expiry).()).() ,
iat: now.() ,
iss: .issuer.(),
aud: .audience.(),
roles,
permissions,
};
(&Header::(Algorithm::HS256), &claims, &.encoding_key)
.(|e| AuthError::(e.()))
}
(&, token: &) <Claims, AuthError> {
= Validation::(Algorithm::HS256);
validation.(&[&.issuer]);
validation.(&[&.audience]);
decode::<Claims>(token, &.decoding_key, &validation)
.(|data| data.claims)
.(|e| e.() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => AuthError::ExpiredCredentials,
jsonwebtoken::errors::ErrorKind::InvalidToken => AuthError::InvalidCredentials,
_ => AuthError::(e.()),
})
}
}
JWT Middleware
pub async fn jwt_middleware(
req: ServiceRequest,
jwt_manager: Arc<JwtManager>,
) -> Result<ServiceRequest, AuthError> {
let token = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.ok_or(AuthError::MissingCredentials)?;
let claims = jwt_manager.validate_token(token)?;
req.extensions_mut().insert(AuthContext {
user_id: claims.sub,
permissions: claims.permissions,
auth_method: AuthMethod::Jwt,
});
Ok(req)
}
Role-Based Access Control (RBAC)
Permission Model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Role {
pub id: String,
pub name: String,
pub permissions: Vec<Permission>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Permission {
ChatCompletion,
ChatCompletionStream,
Embeddings,
ListModels,
GetModelInfo,
ManageUsers,
ManageApiKeys,
ViewMetrics,
ConfigureProviders,
UseProvider(String),
UseModel(String),
}
pub struct RbacManager {
roles: DashMap<String, Role>,
user_roles: DashMap<String, Vec<String>>,
}
impl RbacManager {
pub fn has_permission(&self, user_id: &str, permission: &Permission) -> bool {
let user_role_ids = match self.user_roles.get(user_id) {
Some(roles) => roles.clone(),
=> ,
};
user_role_ids {
(role) = .roles.(&role_id) {
role.permissions.(permission) {
;
}
}
}
}
(&, user_id: &, permission: &Permission) <(), AuthError> {
.(user_id, permission) {
(())
} {
(AuthError::InsufficientPermissions)
}
}
}
RBAC Middleware
pub fn require_permission(permission: Permission) -> impl Fn(ServiceRequest) -> Result<ServiceRequest, AuthError> {
move |req: ServiceRequest| {
let auth_context = req
.extensions()
.get::<AuthContext>()
.ok_or(AuthError::MissingContext)?
.clone();
if !auth_context.permissions.contains(&permission.to_string()) {
return Err(AuthError::InsufficientPermissions);
}
Ok(req)
}
}
app.route(
"/chat/completions",
web::post()
.wrap(require_permission(Permission::ChatCompletion))
.to(chat_completion_handler)
)
Rate Limiting
Lock-Free Rate Limiter
use dashmap::DashMap;
use std::sync::atomic::{AtomicU64, Ordering};
pub struct RateLimiter {
counters: DashMap<String, RateState>,
config: RateLimitConfig,
}
struct RateState {
request_count: AtomicU64,
token_count: AtomicU64,
window_start: AtomicU64,
}
#[derive(Clone)]
pub struct RateLimitConfig {
pub requests_per_minute: u64,
pub tokens_per_minute: u64,
pub window_size: Duration,
}
impl RateLimiter {
pub fn new(config: RateLimitConfig) -> Self {
Self {
counters: DashMap::new(),
config,
}
}
pub fn check_rate_limit(&self, key: &str) -> Result<(), RateLimitError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let state = self.counters.entry(key.to_string()).(|| {
RateState {
request_count: AtomicU64::(),
token_count: AtomicU64::(),
window_start: AtomicU64::(now),
}
});
= state.window_start.(Ordering::SeqCst);
= .config.window_size.();
now - window_start >= window_size_secs {
state.request_count.(, Ordering::SeqCst);
state.token_count.(, Ordering::SeqCst);
state.window_start.(now, Ordering::SeqCst);
}
= state.request_count.(, Ordering::SeqCst);
current_requests >= .config.requests_per_minute {
state.request_count.(, Ordering::SeqCst);
= window_size_secs - (now - window_start);
(RateLimitError::RequestsExceeded {
limit: .config.requests_per_minute,
retry_after,
});
}
(())
}
(&, key: &, tokens: ) <(), RateLimitError> {
(state) = .counters.(key) {
= state.token_count.(tokens, Ordering::SeqCst);
current_tokens + tokens > .config.tokens_per_minute {
state.token_count.(tokens, Ordering::SeqCst);
(RateLimitError::TokensExceeded {
limit: .config.tokens_per_minute,
});
}
}
(())
}
(&, key: &) RateLimitInfo {
.counters
.(key)
.(|state| {
= SystemTime::()
.(UNIX_EPOCH)
.()
.();
= state.window_start.(Ordering::SeqCst);
= window_start + .config.window_size.();
RateLimitInfo {
remaining_requests: .config.requests_per_minute
.(state.request_count.(Ordering::SeqCst)),
remaining_tokens: .config.tokens_per_minute
.(state.token_count.(Ordering::SeqCst)),
reset_at,
}
})
.(RateLimitInfo {
remaining_requests: .config.requests_per_minute,
remaining_tokens: .config.tokens_per_minute,
reset_at: ,
})
}
}
Rate Limit Middleware
pub async fn rate_limit_middleware(
req: ServiceRequest,
rate_limiter: Arc<RateLimiter>,
) -> Result<ServiceRequest, actix_web::Error> {
let auth_context = req
.extensions()
.get::<AuthContext>()
.ok_or(AuthError::MissingContext)?;
rate_limiter
.check_rate_limit(&auth_context.user_id)
.map_err(|e| {
let response = actix_web::HttpResponse::TooManyRequests()
.insert_header(("Retry-After", e.retry_after().to_string()))
.insert_header(("X-RateLimit-Limit", rate_limiter.config.requests_per_minute.to_string()))
.insert_header(("X-RateLimit-Remaining", "0"))
.json(json!({
"error": {
"message": "Rate limit exceeded",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}));
actix_web::error::InternalError::from_response(e, response).into()
})?;
Ok(req)
}
Middleware Pipeline
Combined Auth Middleware
pub struct AuthMiddleware {
api_key_manager: Arc<ApiKeyManager>,
jwt_manager: Arc<JwtManager>,
rbac_manager: Arc<RbacManager>,
rate_limiter: Arc<RateLimiter>,
}
impl AuthMiddleware {
pub async fn authenticate(&self, req: &ServiceRequest) -> Result<AuthContext, AuthError> {
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or(AuthError::MissingCredentials)?;
let context = if auth_header.starts_with("Bearer sk-") {
let key = auth_header.strip_prefix("Bearer ").unwrap();
let api_key = self.api_key_manager.validate_key(key).await?;
AuthContext {
user_id: api_key.user_id,
permissions: api_key.permissions,
auth_method: AuthMethod::ApiKey,
}
} else if auth_header.starts_with() {
= auth_header.().();
= .jwt_manager.(token)?;
AuthContext {
user_id: claims.sub,
permissions: claims.permissions,
auth_method: AuthMethod::Jwt,
}
} {
(AuthError::InvalidCredentials);
};
.rate_limiter.(&context.user_id)?;
(context)
}
(&, context: &AuthContext, required: &Permission) <(), AuthError> {
context.permissions.(&required.()) {
(())
} {
.rbac_manager.(&context.user_id, required)
}
}
}
Configuration
Auth Configuration
auth:
enabled: true
jwt:
secret: ${JWT_SECRET}
issuer: "litellm-gateway"
audience: "litellm-api"
token_expiry_seconds: 3600
api_key:
enabled: true
key_length: 64
prefix: "sk-"
rate_limiting:
enabled: true
requests_per_minute: 60
tokens_per_minute: 100000
window_size_seconds: 60
rbac:
enabled: true
default_role: "user"
roles:
- name: "admin"
permissions:
- "*"
- name: "user"
permissions:
- "chat_completion"
- "chat_completion_stream"
- "embeddings"
- "list_models"
Security Best Practices
1. Secure Secret Generation
use rand::RngCore;
pub fn generate_jwt_secret() -> String {
let mut secret = [0u8; 64];
rand::thread_rng().fill_bytes(&mut secret);
base64::encode(secret)
}
2. Constant-Time Comparison
use subtle::ConstantTimeEq;
fn secure_compare(a: &[u8], b: &[u8]) -> bool {
a.ct_eq(b).into()
}
3. Key Rotation
impl ApiKeyManager {
pub async fn rotate_key(&self, key_id: &str) -> Result<ApiKey, AuthError> {
let old_key = self.storage.get_key(key_id).await?
.ok_or(AuthError::KeyNotFound)?;
let new_key = self.create_key(&old_key.user_id, &format!("{} (rotated)", old_key.name)).await?;
self.storage.mark_for_deletion(key_id, Duration::from_secs(86400)).await?;
Ok(new_key)
}
}
4. Audit Logging
pub struct AuthAuditLog {
logger: Arc<dyn AuditLogger>,
}
impl AuthAuditLog {
pub fn log_auth_success(&self, context: &AuthContext, endpoint: &str) {
self.logger.log(AuditEvent {
event_type: "auth_success",
user_id: &context.user_id,
auth_method: &context.auth_method.to_string(),
endpoint,
timestamp: chrono::Utc::now(),
success: true,
error: None,
});
}
pub fn log_auth_failure(&self, error: &AuthError, endpoint: &str) {
self.logger.log(AuditEvent {
event_type: "auth_failure",
user_id: "unknown",
auth_method: "unknown",
endpoint,
timestamp: chrono::Utc::now(),
success: false,
error: Some(error.to_string()),
});
}
}
Error Types
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Missing credentials")]
MissingCredentials,
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Expired credentials")]
ExpiredCredentials,
#[error("Insufficient permissions")]
InsufficientPermissions,
#[error("Token creation failed: {0}")]
TokenCreation(String),
#[error("Token validation failed: {0}")]
TokenValidation(String),
#[error("Rate limit exceeded")]
RateLimitExceeded,
#[error("Key not found")]
KeyNotFound,
#[error("Missing auth context")]
MissingContext,
#[error("Internal error: {0}")]
Internal(String),
}