use axum::{ extract::FromRequestParts, http::{request::Parts, StatusCode}, }; use crate::AppState; /// Extractor that gates a route behind the `X-Api-Token` header, checked /// against the shared secret in config. A header (not a query param) so the /// token never ends up in nginx access logs or gets pasted around in URLs. pub struct ApiToken; #[axum::async_trait] impl FromRequestParts for ApiToken { type Rejection = (StatusCode, &'static str); async fn from_request_parts( parts: &mut Parts, state: &AppState, ) -> Result { let provided = parts .headers .get("X-Api-Token") .and_then(|v| v.to_str().ok()); match provided { Some(token) if constant_time_eq(token.as_bytes(), state.config.auth.token.as_bytes()) => { Ok(ApiToken) } _ => Err((StatusCode::UNAUTHORIZED, "missing or invalid X-Api-Token header")), } } } /// Avoids leaking the token's length/prefix via response-timing side channels. fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; } let mut diff = 0u8; for (x, y) in a.iter().zip(b.iter()) { diff |= x ^ y; } diff == 0 }