Skip to main content

api_tools/server/axum/layers/
basic_auth.rs

1//! Basic Auth layer
2
3use 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    /// Create a new `BasicAuthLayer`
23    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    // `BoxFuture` is a type alias for `Pin<Box<dyn Future + Send + 'a>>`
58    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        // Layer attaches WWW-Authenticate per RFC 7235.
144        assert_eq!(
145            resp.headers().get(header::WWW_AUTHENTICATE).unwrap(),
146            "basic realm=RESTRICTED",
147        );
148        // body_from_parts forces a JSON ApiErrorResponse body.
149        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        // Request without Authorization header
218        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        // Request with invalid credentials
223        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        // Request with bad credentials
233        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        // Request with good credentials
243        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}