embacle_server/uhp/auth.rs
1// ABOUTME: Bearer auth for UHP routes, answering refusals in the UHP error envelope
2// ABOUTME: Separate from the OpenAI-surface middleware because the error shape differs
3//
4// SPDX-License-Identifier: Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7//! Authentication for the UHP surface.
8//!
9//! The same `EMBACLE_API_KEY` as the rest of the server, but a refusal must be
10//! the UHP envelope with `error.type` of `authentication_error` — the suite
11//! reads that field, so reusing the OpenAI-surface middleware would fail A-02
12//! while looking correct.
13//!
14//! Discovery is deliberately **not** behind this: a client has to be able to
15//! learn whether it is talking to a UHP server before deciding what credential
16//! to present.
17
18use std::env;
19
20use axum::extract::Request;
21use axum::http::header::AUTHORIZATION;
22use axum::middleware::Next;
23use axum::response::{IntoResponse, Response};
24use subtle::ConstantTimeEq;
25
26use super::error::UhpFailure;
27
28/// The variable holding the key, shared with the rest of the server.
29const API_KEY_ENV: &str = "EMBACLE_API_KEY";
30
31/// Require a bearer token matching `EMBACLE_API_KEY`, when one is configured.
32///
33/// With no key set the server is in local development mode and every request
34/// passes, matching the behaviour of the `OpenAI` surface beside it.
35pub async fn require_auth(request: Request, next: Next) -> Response {
36 let expected = match env::var(API_KEY_ENV) {
37 Ok(key) if !key.is_empty() => key,
38 _ => return next.run(request).await,
39 };
40
41 let presented = request
42 .headers()
43 .get(AUTHORIZATION)
44 .and_then(|v| v.to_str().ok())
45 .and_then(|v| v.strip_prefix("Bearer "))
46 .unwrap_or_default();
47
48 let matches = expected.len() == presented.len()
49 && bool::from(expected.as_bytes().ct_eq(presented.as_bytes()));
50
51 if matches {
52 next.run(request).await
53 } else if presented.is_empty() {
54 UhpFailure::unauthorized(
55 "missing_credential",
56 "this endpoint requires a bearer token",
57 )
58 .into_response()
59 } else {
60 UhpFailure::unauthorized("invalid_credential", "the bearer token was not accepted")
61 .into_response()
62 }
63}