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