static_web_server/
basic_auth.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! Basic HTTP Authorization Schema module.
7//!
8
9use bcrypt::verify as bcrypt_verify;
10use headers::{Authorization, HeaderMap, HeaderMapExt, authorization::Basic};
11use hyper::{Body, Request, Response, StatusCode, header::WWW_AUTHENTICATE};
12
13use crate::{Error, error_page, handler::RequestHandlerOpts, http_ext::MethodExt};
14
15/// Initializes `Basic` HTTP Authorization handling
16pub(crate) fn init(credentials: &str, handler_opts: &mut RequestHandlerOpts) {
17    credentials.trim().clone_into(&mut handler_opts.basic_auth);
18    tracing::info!(
19        "basic authentication: enabled={}",
20        !handler_opts.basic_auth.is_empty()
21    );
22}
23
24/// Handles `Basic` HTTP Authorization Schema
25pub(crate) fn pre_process<T>(
26    opts: &RequestHandlerOpts,
27    req: &Request<T>,
28) -> Option<Result<Response<Body>, Error>> {
29    if opts.basic_auth.is_empty() {
30        return None;
31    }
32
33    let method = req.method();
34    if method.is_options() {
35        return None;
36    }
37
38    let uri = req.uri();
39    if let Some((user_id, password)) = opts.basic_auth.split_once(':') {
40        let err = check_request(req.headers(), user_id, password).err()?;
41        tracing::warn!("basic authentication failed {:?}", err);
42        let mut result = error_page::error_response(
43            uri,
44            method,
45            &StatusCode::UNAUTHORIZED,
46            &opts.page404,
47            &opts.page50x,
48        );
49        if let Ok(ref mut resp) = result {
50            resp.headers_mut().insert(
51                WWW_AUTHENTICATE,
52                "Basic realm=\"Static Web Server\", charset=\"UTF-8\""
53                    .parse()
54                    .unwrap(),
55            );
56        }
57        Some(result)
58    } else {
59        tracing::error!("invalid basic authentication `user_id:password` pairs");
60        Some(error_page::error_response(
61            uri,
62            method,
63            &StatusCode::INTERNAL_SERVER_ERROR,
64            &opts.page404,
65            &opts.page50x,
66        ))
67    }
68}
69
70/// Check for a `Basic` HTTP Authorization Schema of an incoming request
71/// and uses `bcrypt` for password hashing verification.
72pub fn check_request(headers: &HeaderMap, userid: &str, password: &str) -> Result<(), StatusCode> {
73    let credentials = headers
74        .typed_get::<Authorization<Basic>>()
75        .ok_or(StatusCode::UNAUTHORIZED)?;
76
77    if credentials.0.username() != userid {
78        return Err(StatusCode::UNAUTHORIZED);
79    }
80
81    match bcrypt_verify(credentials.0.password(), password) {
82        Ok(valid) if valid => Ok(()),
83        Ok(_) => Err(StatusCode::UNAUTHORIZED),
84        Err(err) => {
85            tracing::error!("bcrypt password verification error: {:?}", err);
86            Err(StatusCode::UNAUTHORIZED)
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::{check_request, pre_process};
94    use crate::{Error, handler::RequestHandlerOpts};
95    use headers::HeaderMap;
96    use hyper::{Body, Request, Response, StatusCode, header::WWW_AUTHENTICATE};
97
98    fn make_request(method: &str, auth_header: &str) -> Request<Body> {
99        let mut builder = Request::builder();
100        if !auth_header.is_empty() {
101            builder = builder.header("Authorization", auth_header);
102        }
103        builder.method(method).uri("/").body(Body::empty()).unwrap()
104    }
105
106    fn is_401(result: Option<Result<Response<Body>, Error>>) -> bool {
107        if let Some(Ok(response)) = result {
108            response.status() == StatusCode::UNAUTHORIZED
109                && response.headers().get(WWW_AUTHENTICATE).is_some()
110        } else {
111            false
112        }
113    }
114
115    fn is_500(result: Option<Result<Response<Body>, Error>>) -> bool {
116        if let Some(Ok(response)) = result {
117            response.status() == StatusCode::INTERNAL_SERVER_ERROR
118        } else {
119            false
120        }
121    }
122
123    #[test]
124    fn test_auth_disabled() {
125        assert!(
126            pre_process(
127                &RequestHandlerOpts {
128                    basic_auth: "".into(),
129                    ..Default::default()
130                },
131                &make_request("GET", "Basic anE6anE=")
132            )
133            .is_none()
134        );
135    }
136
137    #[test]
138    fn test_invalid_auth_configuration() {
139        assert!(is_500(pre_process(
140            &RequestHandlerOpts {
141                basic_auth: "xyz".into(),
142                ..Default::default()
143            },
144            &make_request("GET", "Basic anE6anE=")
145        )));
146    }
147
148    #[test]
149    fn test_options_request() {
150        assert!(
151            pre_process(
152                &RequestHandlerOpts {
153                    basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
154                        .into(),
155                    ..Default::default()
156                },
157                &make_request("OPTIONS", "")
158            )
159            .is_none()
160        );
161    }
162
163    #[test]
164    fn test_valid_auth() {
165        let mut headers = HeaderMap::new();
166        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
167        assert!(
168            check_request(
169                &headers,
170                "jq",
171                "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
172            )
173            .is_ok()
174        );
175
176        assert!(
177            pre_process(
178                &RequestHandlerOpts {
179                    basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
180                        .into(),
181                    ..Default::default()
182                },
183                &make_request("GET", "Basic anE6anE=")
184            )
185            .is_none()
186        );
187    }
188
189    #[test]
190    fn test_invalid_auth_header() {
191        let headers = HeaderMap::new();
192        assert!(check_request(&headers, "jq", "").is_err());
193
194        assert!(is_401(pre_process(
195            &RequestHandlerOpts {
196                basic_auth: "jq:".into(),
197                ..Default::default()
198            },
199            &make_request("GET", "")
200        )));
201    }
202
203    #[test]
204    fn test_invalid_auth_pairs() {
205        let mut headers = HeaderMap::new();
206        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
207        assert!(check_request(&headers, "xyz", "").is_err());
208
209        assert!(is_401(pre_process(
210            &RequestHandlerOpts {
211                basic_auth: "xyz:".into(),
212                ..Default::default()
213            },
214            &make_request("GET", "Basic anE6anE=")
215        )));
216    }
217
218    #[test]
219    fn test_invalid_auth() {
220        let mut headers = HeaderMap::new();
221        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
222        assert!(
223            check_request(
224                &headers,
225                "abc",
226                "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
227            )
228            .is_err()
229        );
230        assert!(check_request(&headers, "jq", "password").is_err());
231        assert!(check_request(&headers, "", "password").is_err());
232        assert!(check_request(&headers, "jq", "").is_err());
233
234        assert!(is_401(pre_process(
235            &RequestHandlerOpts {
236                basic_auth: "abc:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
237                    .into(),
238                ..Default::default()
239            },
240            &make_request("GET", "Basic anE6anE=")
241        )));
242        assert!(is_401(pre_process(
243            &RequestHandlerOpts {
244                basic_auth: "jq:password".into(),
245                ..Default::default()
246            },
247            &make_request("GET", "Basic anE6anE=")
248        )));
249        assert!(is_401(pre_process(
250            &RequestHandlerOpts {
251                basic_auth: ":password".into(),
252                ..Default::default()
253            },
254            &make_request("GET", "Basic anE6anE=")
255        )));
256        assert!(is_401(pre_process(
257            &RequestHandlerOpts {
258                basic_auth: "jq:".into(),
259                ..Default::default()
260            },
261            &make_request("GET", "Basic anE6anE=")
262        )));
263    }
264
265    #[test]
266    fn test_invalid_auth_encoding() {
267        let mut headers = HeaderMap::new();
268        headers.insert("Authorization", "Basic xyz".parse().unwrap());
269        assert!(
270            check_request(
271                &headers,
272                "jq",
273                "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
274            )
275            .is_err()
276        );
277
278        assert!(is_401(pre_process(
279            &RequestHandlerOpts {
280                basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
281                    .into(),
282                ..Default::default()
283            },
284            &make_request("GET", "Basic xyz")
285        )));
286    }
287
288    #[test]
289    fn test_invalid_auth_encoding2() {
290        let mut headers = HeaderMap::new();
291        headers.insert("Authorization", "abcd".parse().unwrap());
292        assert!(
293            check_request(
294                &headers,
295                "jq",
296                "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
297            )
298            .is_err()
299        );
300
301        assert!(is_401(pre_process(
302            &RequestHandlerOpts {
303                basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
304                    .into(),
305                ..Default::default()
306            },
307            &make_request("GET", "abcd")
308        )));
309    }
310}