Skip to main content

embacle_server/
auth.rs

1// ABOUTME: Optional bearer token authentication middleware for the REST API
2// ABOUTME: Enforces EMBACLE_API_KEY when set, allows unauthenticated access otherwise
3//
4// SPDX-License-Identifier: Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7use axum::extract::Request;
8use axum::http::StatusCode;
9use axum::middleware::Next;
10use axum::response::{IntoResponse, Response};
11use axum::Json;
12use subtle::ConstantTimeEq;
13
14use crate::openai_types::ErrorResponse;
15
16/// Environment variable name for the API key
17const API_KEY_ENV: &str = "EMBACLE_API_KEY";
18
19/// Middleware that validates the bearer token against `EMBACLE_API_KEY`
20///
21/// The env var is read on every request to allow runtime key rotation
22/// without restarting the server. If the variable is not set, all requests
23/// are allowed through (localhost development mode). If set, requests must
24/// include a matching `Authorization: Bearer <key>` header.
25pub async fn require_auth(request: Request, next: Next) -> Response {
26    let expected_key = match std::env::var(API_KEY_ENV) {
27        Ok(key) if !key.is_empty() => key,
28        _ => return next.run(request).await,
29    };
30
31    let auth_header = request
32        .headers()
33        .get("authorization")
34        .and_then(|v| v.to_str().ok());
35
36    match auth_header {
37        Some(header) if header.starts_with("Bearer ") => {
38            let token = &header.as_bytes()["Bearer ".len()..];
39            let expected = expected_key.as_bytes();
40            if token.ct_eq(expected).into() {
41                next.run(request).await
42            } else {
43                auth_error("Invalid API key")
44            }
45        }
46        Some(_) => auth_error("Authorization header must use Bearer scheme"),
47        None => auth_error("Missing Authorization header"),
48    }
49}
50
51/// Build a 401 error response
52fn auth_error(message: &str) -> Response {
53    let body = ErrorResponse::new("authentication_error", message);
54    (StatusCode::UNAUTHORIZED, Json(body)).into_response()
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn api_key_env_is_correct() {
63        assert_eq!(API_KEY_ENV, "EMBACLE_API_KEY");
64    }
65}