Skip to main content

crabllm_proxy/
auth.rs

1use crate::AppState;
2use axum::{
3    Json,
4    extract::{Request, State},
5    http::StatusCode,
6    middleware::Next,
7    response::{IntoResponse, Response},
8};
9use crabllm_core::{ApiError, Provider, Storage};
10
11/// Opaque identity token attached by the authentication layer, inserted into
12/// request extensions. Standalone deployments populate this from the configured
13/// key name; embedders providing their own auth populate it with whatever
14/// caller identifier they need to attribute work against. Treat as opaque —
15/// do not parse, sanitize, or display without intentional formatting.
16#[derive(Clone, Debug)]
17pub struct Principal(pub Option<String>);
18
19/// Auth middleware: validates Bearer token against configured virtual keys.
20/// Skips auth only when no admin_token is configured AND key_map is empty.
21/// Inserts `Principal` into request extensions for downstream handlers.
22pub async fn auth<S: Storage + 'static, P: Provider + 'static>(
23    State(state): State<AppState<S, P>>,
24    mut request: Request,
25    next: Next,
26) -> Response {
27    // Skip auth when key management is disabled and no keys exist.
28    if state.config.admin_token.is_none()
29        && state
30            .key_map
31            .read()
32            .unwrap_or_else(|e| e.into_inner())
33            .is_empty()
34    {
35        request.extensions_mut().insert(Principal(None));
36        return next.run(request).await;
37    }
38
39    // Accept either OpenAI-style `Authorization: Bearer <key>` or Anthropic-style
40    // `x-api-key: <key>`. Both map to the same virtual-key lookup.
41    let headers = request.headers();
42    let bearer = headers
43        .get("authorization")
44        .and_then(|v| v.to_str().ok())
45        .and_then(|h| h.strip_prefix("Bearer "));
46    let x_api_key = headers.get("x-api-key").and_then(|v| v.to_str().ok());
47
48    let token = match bearer.or(x_api_key) {
49        Some(t) => t,
50        None => {
51            return (
52                StatusCode::UNAUTHORIZED,
53                Json(ApiError::new(
54                    "missing Authorization or x-api-key header",
55                    "authentication_error",
56                )),
57            )
58                .into_response();
59        }
60    };
61
62    let principal = state
63        .key_map
64        .read()
65        .unwrap_or_else(|e| e.into_inner())
66        .get(token)
67        .cloned();
68
69    let Some(principal) = principal else {
70        return (
71            StatusCode::UNAUTHORIZED,
72            Json(ApiError::new("invalid API key", "authentication_error")),
73        )
74            .into_response();
75    };
76
77    request.extensions_mut().insert(Principal(Some(principal)));
78
79    next.run(request).await
80}