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::Basic, Authorization, HeaderMap, HeaderMapExt};
11use hyper::{header::WWW_AUTHENTICATE, Body, Request, Response, StatusCode};
12
13use crate::{error_page, handler::RequestHandlerOpts, http_ext::MethodExt, Error};
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::{handler::RequestHandlerOpts, Error};
95    use headers::HeaderMap;
96    use hyper::{header::WWW_AUTHENTICATE, Body, Request, Response, StatusCode};
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!(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    #[test]
136    fn test_invalid_auth_configuration() {
137        assert!(is_500(pre_process(
138            &RequestHandlerOpts {
139                basic_auth: "xyz".into(),
140                ..Default::default()
141            },
142            &make_request("GET", "Basic anE6anE=")
143        )));
144    }
145
146    #[test]
147    fn test_options_request() {
148        assert!(pre_process(
149            &RequestHandlerOpts {
150                basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
151                    .into(),
152                ..Default::default()
153            },
154            &make_request("OPTIONS", "")
155        )
156        .is_none());
157    }
158
159    #[test]
160    fn test_valid_auth() {
161        let mut headers = HeaderMap::new();
162        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
163        assert!(check_request(
164            &headers,
165            "jq",
166            "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
167        )
168        .is_ok());
169
170        assert!(pre_process(
171            &RequestHandlerOpts {
172                basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
173                    .into(),
174                ..Default::default()
175            },
176            &make_request("GET", "Basic anE6anE=")
177        )
178        .is_none());
179    }
180
181    #[test]
182    fn test_invalid_auth_header() {
183        let headers = HeaderMap::new();
184        assert!(check_request(&headers, "jq", "").is_err());
185
186        assert!(is_401(pre_process(
187            &RequestHandlerOpts {
188                basic_auth: "jq:".into(),
189                ..Default::default()
190            },
191            &make_request("GET", "")
192        )));
193    }
194
195    #[test]
196    fn test_invalid_auth_pairs() {
197        let mut headers = HeaderMap::new();
198        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
199        assert!(check_request(&headers, "xyz", "").is_err());
200
201        assert!(is_401(pre_process(
202            &RequestHandlerOpts {
203                basic_auth: "xyz:".into(),
204                ..Default::default()
205            },
206            &make_request("GET", "Basic anE6anE=")
207        )));
208    }
209
210    #[test]
211    fn test_invalid_auth() {
212        let mut headers = HeaderMap::new();
213        headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
214        assert!(check_request(
215            &headers,
216            "abc",
217            "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
218        )
219        .is_err());
220        assert!(check_request(&headers, "jq", "password").is_err());
221        assert!(check_request(&headers, "", "password").is_err());
222        assert!(check_request(&headers, "jq", "").is_err());
223
224        assert!(is_401(pre_process(
225            &RequestHandlerOpts {
226                basic_auth: "abc:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
227                    .into(),
228                ..Default::default()
229            },
230            &make_request("GET", "Basic anE6anE=")
231        )));
232        assert!(is_401(pre_process(
233            &RequestHandlerOpts {
234                basic_auth: "jq:password".into(),
235                ..Default::default()
236            },
237            &make_request("GET", "Basic anE6anE=")
238        )));
239        assert!(is_401(pre_process(
240            &RequestHandlerOpts {
241                basic_auth: ":password".into(),
242                ..Default::default()
243            },
244            &make_request("GET", "Basic anE6anE=")
245        )));
246        assert!(is_401(pre_process(
247            &RequestHandlerOpts {
248                basic_auth: "jq:".into(),
249                ..Default::default()
250            },
251            &make_request("GET", "Basic anE6anE=")
252        )));
253    }
254
255    #[test]
256    fn test_invalid_auth_encoding() {
257        let mut headers = HeaderMap::new();
258        headers.insert("Authorization", "Basic xyz".parse().unwrap());
259        assert!(check_request(
260            &headers,
261            "jq",
262            "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
263        )
264        .is_err());
265
266        assert!(is_401(pre_process(
267            &RequestHandlerOpts {
268                basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
269                    .into(),
270                ..Default::default()
271            },
272            &make_request("GET", "Basic xyz")
273        )));
274    }
275
276    #[test]
277    fn test_invalid_auth_encoding2() {
278        let mut headers = HeaderMap::new();
279        headers.insert("Authorization", "abcd".parse().unwrap());
280        assert!(check_request(
281            &headers,
282            "jq",
283            "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
284        )
285        .is_err());
286
287        assert!(is_401(pre_process(
288            &RequestHandlerOpts {
289                basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
290                    .into(),
291                ..Default::default()
292            },
293            &make_request("GET", "abcd")
294        )));
295    }
296}