Skip to main content

crabtalk_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 crabtalk_core::{ApiError, Storage};
10
11/// Wrapper for the authenticated key name, inserted into request extensions.
12#[derive(Clone, Debug)]
13pub struct KeyName(pub Option<String>);
14
15/// Auth middleware: validates Bearer token against configured virtual keys.
16/// If no keys are configured, all requests pass through.
17/// Inserts `KeyName` into request extensions for downstream handlers.
18pub async fn auth<S: Storage + 'static>(
19    State(state): State<AppState<S>>,
20    mut request: Request,
21    next: Next,
22) -> Response {
23    // If no keys configured, skip auth entirely.
24    if state.config.keys.is_empty() {
25        request.extensions_mut().insert(KeyName(None));
26        return next.run(request).await;
27    }
28
29    let auth_header = request
30        .headers()
31        .get("authorization")
32        .and_then(|v| v.to_str().ok());
33
34    let token = match auth_header.and_then(|h| h.strip_prefix("Bearer ")) {
35        Some(t) => t,
36        None => {
37            return (
38                StatusCode::UNAUTHORIZED,
39                Json(ApiError::new(
40                    "missing or invalid Authorization header",
41                    "authentication_error",
42                )),
43            )
44                .into_response();
45        }
46    };
47
48    let Some(key_name) = state.key_map.get(token) else {
49        return (
50            StatusCode::UNAUTHORIZED,
51            Json(ApiError::new("invalid API key", "authentication_error")),
52        )
53            .into_response();
54    };
55
56    request
57        .extensions_mut()
58        .insert(KeyName(Some(key_name.clone())));
59
60    next.run(request).await
61}