Skip to main content

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