1use axum::{
4 body::Body,
5 http::{Request, StatusCode},
6 middleware::Next,
7 response::Response,
8};
9
10pub async fn auth_middleware(
16 request: Request<Body>,
17 next: Next,
18 expected_token: Option<String>,
19) -> Result<Response, StatusCode> {
20 let Some(expected) = expected_token else {
22 return Ok(next.run(request).await);
23 };
24
25 let auth_header = request
27 .headers()
28 .get("authorization")
29 .and_then(|v| v.to_str().ok());
30
31 match auth_header {
32 Some(header) if header.starts_with("Bearer ") => {
33 let token = &header[7..];
34 if constant_time_eq(token.as_bytes(), expected.as_bytes()) {
36 Ok(next.run(request).await)
37 } else {
38 Err(StatusCode::UNAUTHORIZED)
39 }
40 }
41 _ => Err(StatusCode::UNAUTHORIZED),
42 }
43}
44
45fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
47 if a.len() != b.len() {
48 return false;
49 }
50 let mut result = 0u8;
51 for (x, y) in a.iter().zip(b.iter()) {
52 result |= x ^ y;
53 }
54 result == 0
55}
56
57#[cfg(test)]
62mod tests {
63 use super::*;
64 use axum::{Router, body::Body, middleware, routing::get};
65 use tower::ServiceExt;
66
67 fn test_app(token: Option<String>) -> Router {
69 let handler = get(|| async { "ok" });
70 Router::new()
71 .route("/api/test", handler)
72 .layer(middleware::from_fn(
73 move |req: Request<Body>, next: Next| {
74 let t = token.clone();
75 async move { auth_middleware(req, next, t).await }
76 },
77 ))
78 }
79
80 #[tokio::test]
81 async fn test_no_token_configured_passes() {
82 let app = test_app(None);
83 let req = Request::builder()
84 .uri("/api/test")
85 .body(Body::empty())
86 .unwrap();
87 let resp = app.oneshot(req).await.unwrap();
88 assert_eq!(resp.status(), StatusCode::OK);
89 }
90
91 #[tokio::test]
92 async fn test_correct_token_passes() {
93 let app = test_app(Some("secret123".into()));
94 let req = Request::builder()
95 .uri("/api/test")
96 .header("authorization", "Bearer secret123")
97 .body(Body::empty())
98 .unwrap();
99 let resp = app.oneshot(req).await.unwrap();
100 assert_eq!(resp.status(), StatusCode::OK);
101 }
102
103 #[tokio::test]
104 async fn test_wrong_token_rejected() {
105 let app = test_app(Some("secret123".into()));
106 let req = Request::builder()
107 .uri("/api/test")
108 .header("authorization", "Bearer wrong-token")
109 .body(Body::empty())
110 .unwrap();
111 let resp = app.oneshot(req).await.unwrap();
112 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
113 }
114
115 #[tokio::test]
116 async fn test_missing_auth_header_rejected() {
117 let app = test_app(Some("secret123".into()));
118 let req = Request::builder()
119 .uri("/api/test")
120 .body(Body::empty())
121 .unwrap();
122 let resp = app.oneshot(req).await.unwrap();
123 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
124 }
125
126 #[tokio::test]
127 async fn test_malformed_auth_header_rejected() {
128 let app = test_app(Some("secret123".into()));
129
130 let req = Request::builder()
132 .uri("/api/test")
133 .header("authorization", "Basic secret123")
134 .body(Body::empty())
135 .unwrap();
136 let resp = app.oneshot(req).await.unwrap();
137 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
138 }
139
140 #[test]
141 fn test_constant_time_eq_equal() {
142 assert!(constant_time_eq(b"hello", b"hello"));
143 }
144
145 #[test]
146 fn test_constant_time_eq_unequal() {
147 assert!(!constant_time_eq(b"hello", b"world"));
148 }
149
150 #[test]
151 fn test_constant_time_eq_different_lengths() {
152 assert!(!constant_time_eq(b"short", b"longer"));
153 }
154}