use axum::{
body::Body,
http::{Request, StatusCode},
middleware::Next,
response::Response,
};
pub async fn auth_middleware(
request: Request<Body>,
next: Next,
expected_token: Option<String>,
) -> Result<Response, StatusCode> {
let Some(expected) = expected_token else {
return Ok(next.run(request).await);
};
let auth_header = request
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok());
match auth_header {
Some(header) if header.starts_with("Bearer ") => {
let token = &header[7..];
if constant_time_eq(token.as_bytes(), expected.as_bytes()) {
Ok(next.run(request).await)
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{Router, body::Body, middleware, routing::get};
use tower::ServiceExt;
fn test_app(token: Option<String>) -> Router {
let handler = get(|| async { "ok" });
Router::new()
.route("/api/test", handler)
.layer(middleware::from_fn(
move |req: Request<Body>, next: Next| {
let t = token.clone();
async move { auth_middleware(req, next, t).await }
},
))
}
#[tokio::test]
async fn test_no_token_configured_passes() {
let app = test_app(None);
let req = Request::builder()
.uri("/api/test")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_correct_token_passes() {
let app = test_app(Some("secret123".into()));
let req = Request::builder()
.uri("/api/test")
.header("authorization", "Bearer secret123")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_wrong_token_rejected() {
let app = test_app(Some("secret123".into()));
let req = Request::builder()
.uri("/api/test")
.header("authorization", "Bearer wrong-token")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_missing_auth_header_rejected() {
let app = test_app(Some("secret123".into()));
let req = Request::builder()
.uri("/api/test")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_malformed_auth_header_rejected() {
let app = test_app(Some("secret123".into()));
let req = Request::builder()
.uri("/api/test")
.header("authorization", "Basic secret123")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_constant_time_eq_equal() {
assert!(constant_time_eq(b"hello", b"hello"));
}
#[test]
fn test_constant_time_eq_unequal() {
assert!(!constant_time_eq(b"hello", b"world"));
}
#[test]
fn test_constant_time_eq_different_lengths() {
assert!(!constant_time_eq(b"short", b"longer"));
}
}