api_tools/server/axum/layers/
basic_auth.rs1use super::body_from_parts;
4use axum::{
5 body::Body,
6 http::{HeaderValue, Request, header},
7 response::Response,
8};
9use futures::future::BoxFuture;
10use http_auth_basic::Credentials;
11use hyper::StatusCode;
12use std::task::{Context, Poll};
13use tower::{Layer, Service};
14
15#[derive(Clone)]
16pub struct BasicAuthLayer {
17 pub username: String,
18 pub password: String,
19}
20
21impl BasicAuthLayer {
22 pub fn new(username: &str, password: &str) -> Self {
24 Self {
25 username: username.to_string(),
26 password: password.to_string(),
27 }
28 }
29}
30
31impl<S> Layer<S> for BasicAuthLayer {
32 type Service = BasicAuthMiddleware<S>;
33
34 fn layer(&self, inner: S) -> Self::Service {
35 BasicAuthMiddleware {
36 inner,
37 username: self.username.clone(),
38 password: self.password.clone(),
39 }
40 }
41}
42
43#[derive(Clone)]
44pub struct BasicAuthMiddleware<S> {
45 inner: S,
46 username: String,
47 password: String,
48}
49
50impl<S> Service<Request<Body>> for BasicAuthMiddleware<S>
51where
52 S: Service<Request<Body>, Response = Response> + Send + 'static,
53 S::Future: Send + 'static,
54{
55 type Response = S::Response;
56 type Error = S::Error;
57 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
59
60 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
61 self.inner.poll_ready(cx)
62 }
63
64 fn call(&mut self, request: Request<Body>) -> Self::Future {
65 let auth = request
66 .headers()
67 .get(header::AUTHORIZATION)
68 .and_then(|h| h.to_str().ok())
69 .map(str::to_string);
70 let username = self.username.clone();
71 let password = self.password.clone();
72
73 let future = self.inner.call(request);
74 Box::pin(async move {
75 let mut response = Response::default();
76
77 let ok = match auth {
78 None => false,
79 Some(auth) => match Credentials::from_header(auth) {
80 Err(_) => false,
81 Ok(cred) => cred.user_id == username && cred.password == password,
82 },
83 };
84 response = match ok {
85 true => future.await?,
86 false => {
87 let (mut parts, _body) = response.into_parts();
88 let msg = body_from_parts(
89 &mut parts,
90 StatusCode::UNAUTHORIZED,
91 "Unauthorized",
92 Some(vec![(
93 header::WWW_AUTHENTICATE,
94 HeaderValue::from_static("basic realm=RESTRICTED"),
95 )]),
96 );
97 Response::from_parts(parts, Body::from(msg))
98 }
99 };
100
101 Ok(response)
102 })
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use axum::{
110 body::Body,
111 http::{Request, StatusCode, header},
112 response::Response,
113 };
114 use base64::{Engine as _, engine::general_purpose};
115 use std::convert::Infallible;
116 use tower::ServiceExt;
117
118 async fn dummy_service(_req: Request<Body>) -> Result<Response, Infallible> {
119 Ok(Response::builder()
120 .status(StatusCode::OK)
121 .body(Body::from("ok"))
122 .unwrap())
123 }
124
125 fn auth_header(value: &str) -> String {
126 format!("Basic {}", general_purpose::STANDARD.encode(value))
127 }
128
129 fn make_service(
130 username: &str,
131 password: &str,
132 ) -> impl tower::Service<Request<Body>, Response = Response, Error = Infallible> + Clone {
133 BasicAuthLayer::new(username, password).layer(tower::service_fn(dummy_service))
134 }
135
136 #[tokio::test]
137 async fn missing_authorization_header_returns_401_with_json_body() {
138 let svc = make_service("user", "pass");
139 let req = Request::builder().uri("/").body(Body::empty()).unwrap();
140 let resp = svc.oneshot(req).await.unwrap();
141
142 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
143 assert_eq!(
145 resp.headers().get(header::WWW_AUTHENTICATE).unwrap(),
146 "basic realm=RESTRICTED",
147 );
148 assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "application/json",);
150
151 let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
152 let body = std::str::from_utf8(&body).unwrap();
153 assert!(body.contains("\"code\":401"), "body was: {body}");
154 }
155
156 #[tokio::test]
157 async fn non_basic_authorization_scheme_returns_401() {
158 let svc = make_service("user", "pass");
159 let req = Request::builder()
160 .uri("/")
161 .header(header::AUTHORIZATION, "Bearer some.jwt.token")
162 .body(Body::empty())
163 .unwrap();
164 let resp = svc.oneshot(req).await.unwrap();
165
166 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
167 }
168
169 #[tokio::test]
170 async fn invalid_base64_in_basic_header_returns_401_without_panic() {
171 let svc = make_service("user", "pass");
172 let req = Request::builder()
173 .uri("/")
174 .header(header::AUTHORIZATION, "Basic !!!not-base64!!!")
175 .body(Body::empty())
176 .unwrap();
177 let resp = svc.oneshot(req).await.unwrap();
178
179 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
180 }
181
182 #[tokio::test]
183 async fn correct_credentials_invoke_inner_handler() {
184 let svc = make_service("user", "pass");
185 let req = Request::builder()
186 .uri("/")
187 .header(header::AUTHORIZATION, auth_header("user:pass"))
188 .body(Body::empty())
189 .unwrap();
190 let resp = svc.oneshot(req).await.unwrap();
191
192 assert_eq!(resp.status(), StatusCode::OK);
193 let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
194 assert_eq!(&body[..], b"ok");
195 }
196
197 #[tokio::test]
198 async fn matching_user_with_wrong_password_returns_401() {
199 let svc = make_service("user", "pass");
200 let req = Request::builder()
201 .uri("/")
202 .header(header::AUTHORIZATION, auth_header("user:wrong"))
203 .body(Body::empty())
204 .unwrap();
205 let resp = svc.oneshot(req).await.unwrap();
206
207 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
208 }
209
210 #[tokio::test]
211 async fn test_basic_auth_layer() {
212 let username = "user";
213 let password = "pass";
214 let layer = BasicAuthLayer::new(username, password);
215 let service = layer.layer(tower::service_fn(dummy_service));
216
217 let req = Request::builder().uri("/").body(Body::empty()).unwrap();
219 let resp = service.clone().oneshot(req).await.unwrap();
220 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
221
222 let bad_auth = format!("Basic {}", general_purpose::STANDARD.encode(""));
224 let req = Request::builder()
225 .uri("/")
226 .header(header::AUTHORIZATION, bad_auth)
227 .body(Body::empty())
228 .unwrap();
229 let resp = service.clone().oneshot(req).await.unwrap();
230 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
231
232 let bad_auth = format!("Basic {}", general_purpose::STANDARD.encode("user:wrong"));
234 let req = Request::builder()
235 .uri("/")
236 .header(header::AUTHORIZATION, bad_auth)
237 .body(Body::empty())
238 .unwrap();
239 let resp = service.clone().oneshot(req).await.unwrap();
240 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
241
242 let good_auth = format!(
244 "Basic {}",
245 general_purpose::STANDARD.encode(format!("{}:{}", username, password))
246 );
247 let req = Request::builder()
248 .uri("/")
249 .header(header::AUTHORIZATION, good_auth)
250 .body(Body::empty())
251 .unwrap();
252 let resp = service.oneshot(req).await.unwrap();
253 assert_eq!(resp.status(), StatusCode::OK);
254 }
255}