use std::sync::Arc;
use axum::extract::{Request, State};
use axum::http::{StatusCode, header::AUTHORIZATION};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
pub(super) fn resolve_token(arg: Option<&str>) -> anyhow::Result<String> {
if let Some(t) = arg.map(str::trim).filter(|t| !t.is_empty()) {
return Ok(t.to_string());
}
if let Ok(env) = std::env::var("LEVIATH_API_TOKEN") {
let t = env.trim().to_string();
if !t.is_empty() {
return Ok(t);
}
}
anyhow::bail!(
"refusing to start an unauthenticated API server: it can spawn \
tool-executing agents. Set a token with `--token <token>` or the \
LEVIATH_API_TOKEN environment variable. Clients send it as \
`Authorization: Bearer <token>` (or `?token=<token>` for WebSockets)."
)
}
use leviath_core::constant_time_eq;
fn presented_token(req: &Request) -> Option<String> {
if let Some(bearer) = req
.headers()
.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
{
return Some(bearer.trim().to_string());
}
if !is_websocket_route(req.uri().path()) {
return None;
}
req.uri()
.query()
.and_then(|q| q.split('&').find_map(|kv| kv.strip_prefix("token=")))
.map(|t| t.to_string())
}
fn is_websocket_route(path: &str) -> bool {
path == "/ws" || path.starts_with("/ws/")
}
pub(super) async fn require_auth(
State(expected): State<Arc<String>>,
req: Request,
next: Next,
) -> Response {
match presented_token(&req) {
Some(token) if constant_time_eq(&token, &expected) => next.run(req).await,
_ => (
StatusCode::UNAUTHORIZED,
"unauthorized: missing or invalid API token",
)
.into_response(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request as HttpRequest;
fn req(auth: Option<&str>, query: Option<&str>) -> Request {
req_to("/api/agents", auth, query)
}
fn req_to(path: &str, auth: Option<&str>, query: Option<&str>) -> Request {
let uri = match query {
Some(q) => format!("http://x{path}?{q}"),
None => format!("http://x{path}"),
};
let mut b = HttpRequest::builder().uri(uri);
if let Some(a) = auth {
b = b.header(AUTHORIZATION, a);
}
b.body(Body::empty()).unwrap()
}
#[test]
fn resolve_token_prefers_arg_then_env_then_errors() {
assert_eq!(resolve_token(Some("from-arg")).unwrap(), "from-arg");
assert_eq!(resolve_token(Some(" spaced ")).unwrap(), "spaced");
temp_env::with_var("LEVIATH_API_TOKEN", Some("from-env"), || {
assert_eq!(resolve_token(Some(" ")).unwrap(), "from-env");
assert_eq!(resolve_token(None).unwrap(), "from-env");
});
temp_env::with_var("LEVIATH_API_TOKEN", Some(" "), || {
assert!(resolve_token(None).is_err());
});
temp_env::with_var("LEVIATH_API_TOKEN", None::<&str>, || {
assert!(resolve_token(None).is_err());
});
}
#[test]
fn constant_time_eq_matches_std_equality() {
assert!(constant_time_eq("abc", "abc"));
assert!(!constant_time_eq("abc", "abd"));
assert!(!constant_time_eq("abc", "abcd")); assert!(constant_time_eq("", ""));
}
#[test]
fn presented_token_reads_the_bearer_header_on_any_route() {
assert_eq!(
presented_token(&req(Some("Bearer tok123"), None)).as_deref(),
Some("tok123")
);
assert!(presented_token(&req(None, None)).is_none());
}
#[test]
fn the_query_token_works_on_the_websocket_routes() {
for path in ["/ws", "/ws/agents/run-1"] {
assert_eq!(
presented_token(&req_to(path, None, Some("token=qtok"))).as_deref(),
Some("qtok"),
"{path}"
);
assert_eq!(
presented_token(&req_to(path, None, Some("foo=1&token=qtok"))).as_deref(),
Some("qtok"),
"{path}"
);
assert_eq!(
presented_token(&req_to(path, Some("Basic x"), Some("token=qtok"))).as_deref(),
Some("qtok"),
"{path}"
);
assert!(presented_token(&req_to(path, None, Some("mytoken=x"))).is_none());
}
}
#[test]
fn the_query_token_is_refused_on_ordinary_routes() {
for path in ["/api/agents", "/api/config", "/wsomething", "/"] {
assert!(
presented_token(&req_to(path, None, Some("token=qtok"))).is_none(),
"{path} must not accept a query token"
);
}
}
#[tokio::test]
async fn require_auth_allows_valid_and_rejects_invalid() {
use axum::routing::get;
use axum::{Router, middleware};
use tower::ServiceExt;
let expected = Arc::new("secret".to_string());
let app: Router = Router::new()
.route("/api/agents", get(|| async { "ok" }))
.layer(middleware::from_fn_with_state(expected, require_auth));
let ok = app
.clone()
.oneshot(req(Some("Bearer secret"), None))
.await
.unwrap();
assert_eq!(ok.status(), StatusCode::OK);
let query_on_rest = app
.clone()
.oneshot(req(None, Some("token=secret")))
.await
.unwrap();
assert_eq!(query_on_rest.status(), StatusCode::UNAUTHORIZED);
let missing = app.clone().oneshot(req(None, None)).await.unwrap();
assert_eq!(missing.status(), StatusCode::UNAUTHORIZED);
let wrong = app.oneshot(req(Some("Bearer nope"), None)).await.unwrap();
assert_eq!(wrong.status(), StatusCode::UNAUTHORIZED);
}
}