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::{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    let user_match = credentials.0.username() == userid;
78    let password_match = bcrypt_verify(credentials.0.password(), password)
79        .inspect_err(|err| tracing::error!("bcrypt password verification error: {:?}", err))
80        .unwrap_or(false);
81    let valid = user_match && password_match;
82    valid.then_some(()).ok_or(StatusCode::UNAUTHORIZED)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::{check_request, pre_process};
88    use crate::{Error, handler::RequestHandlerOpts};
89    use headers::HeaderMap;
90    use hyper::{Body, Request, Response, StatusCode, header::WWW_AUTHENTICATE};
91
92    fn make_request(method: &str, auth_header: &str) -> Request<Body> {
93        let mut builder = Request::builder();
94        if !auth_header.is_empty() {
95            builder = builder.header("Authorization", auth_header);
96        }
97        builder.method(method).uri("/").body(Body::empty()).unwrap()
98    }
99
100    fn is_401(result: Option<Result<Response<Body>, Error>>) -> bool {
101        if let Some(Ok(response)) = result {
102            response.status() == StatusCode::UNAUTHORIZED
103                && response.headers().get(WWW_AUTHENTICATE).is_some()
104        } else {
105            false
106        }
107    }
108
109    fn is_500(result: Option<Result<Response<Body>, Error>>) -> bool {
110        if let Some(Ok(response)) = result {
111            response.status() == StatusCode::INTERNAL_SERVER_ERROR
112        } else {
113            false
114        }
115    }
116
117    #[test]
118    fn test_auth_disabled() {
119        assert!(
120            pre_process(
121                &RequestHandlerOpts {
122                    basic_auth: "".into(),
123                    ..Default::default()
124                },
125                &make_request("GET", "Basic anE6anE=")
126            )
127            .is_none()
128        );
129    }
130
131    #[test]
132    fn test_invalid_auth_configuration() {
133        assert!(is_500(pre_process(
134            &RequestHandlerOpts {
135                basic_auth: "xyz".into(),
136                ..Default::default()
137            },
138            &make_request("GET", "Basic anE6anE=")
139        )));
140    }
141
142    #[test]
143    fn test_options_request() {
144        assert!(
145            pre_process(
146                &RequestHandlerOpts {
147                    basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
148                        .into(),
149                    ..Default::default()
150                },
151                &make_request("OPTIONS", "")
152            )
153            .is_none()
154        );
155    }
156
157    #[test]
158    fn test_valid_auth() {
159        let mut headers = HeaderMap::new();
160        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
161        assert!(
162            check_request(
163                &headers,
164                "jq",
165                "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
166            )
167            .is_ok()
168        );
169
170        assert!(
171            pre_process(
172                &RequestHandlerOpts {
173                    basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
174                        .into(),
175                    ..Default::default()
176                },
177                &make_request("GET", "Basic anE6anE=")
178            )
179            .is_none()
180        );
181    }
182
183    #[test]
184    fn test_invalid_auth_header() {
185        let headers = HeaderMap::new();
186        assert!(check_request(&headers, "jq", "").is_err());
187
188        assert!(is_401(pre_process(
189            &RequestHandlerOpts {
190                basic_auth: "jq:".into(),
191                ..Default::default()
192            },
193            &make_request("GET", "")
194        )));
195    }
196
197    #[test]
198    fn test_invalid_auth_pairs() {
199        let mut headers = HeaderMap::new();
200        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
201        assert!(check_request(&headers, "xyz", "").is_err());
202
203        assert!(is_401(pre_process(
204            &RequestHandlerOpts {
205                basic_auth: "xyz:".into(),
206                ..Default::default()
207            },
208            &make_request("GET", "Basic anE6anE=")
209        )));
210    }
211
212    #[test]
213    fn test_invalid_auth() {
214        let mut headers = HeaderMap::new();
215        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
216        assert!(
217            check_request(
218                &headers,
219                "abc",
220                "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
221            )
222            .is_err()
223        );
224        assert!(check_request(&headers, "jq", "password").is_err());
225        assert!(check_request(&headers, "", "password").is_err());
226        assert!(check_request(&headers, "jq", "").is_err());
227
228        assert!(is_401(pre_process(
229            &RequestHandlerOpts {
230                basic_auth: "abc:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
231                    .into(),
232                ..Default::default()
233            },
234            &make_request("GET", "Basic anE6anE=")
235        )));
236        assert!(is_401(pre_process(
237            &RequestHandlerOpts {
238                basic_auth: "jq:password".into(),
239                ..Default::default()
240            },
241            &make_request("GET", "Basic anE6anE=")
242        )));
243        assert!(is_401(pre_process(
244            &RequestHandlerOpts {
245                basic_auth: ":password".into(),
246                ..Default::default()
247            },
248            &make_request("GET", "Basic anE6anE=")
249        )));
250        assert!(is_401(pre_process(
251            &RequestHandlerOpts {
252                basic_auth: "jq:".into(),
253                ..Default::default()
254            },
255            &make_request("GET", "Basic anE6anE=")
256        )));
257    }
258
259    #[test]
260    fn test_invalid_auth_encoding() {
261        let mut headers = HeaderMap::new();
262        headers.insert("Authorization", "Basic xyz".parse().unwrap());
263        assert!(
264            check_request(
265                &headers,
266                "jq",
267                "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
268            )
269            .is_err()
270        );
271
272        assert!(is_401(pre_process(
273            &RequestHandlerOpts {
274                basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
275                    .into(),
276                ..Default::default()
277            },
278            &make_request("GET", "Basic xyz")
279        )));
280    }
281
282    #[test]
283    fn test_invalid_auth_encoding2() {
284        let mut headers = HeaderMap::new();
285        headers.insert("Authorization", "abcd".parse().unwrap());
286        assert!(
287            check_request(
288                &headers,
289                "jq",
290                "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
291            )
292            .is_err()
293        );
294
295        assert!(is_401(pre_process(
296            &RequestHandlerOpts {
297                basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
298                    .into(),
299                ..Default::default()
300            },
301            &make_request("GET", "abcd")
302        )));
303    }
304}