rocket_basicauth/
lib.rs

1//! [![Tests](https://github.com/Owez/rocket-basicauth/workflows/Tests/badge.svg)](https://github.com/Owez/rocket-basicauth/actions?query=workflow%3ATests) [![Docs](https://docs.rs/rocket-basicauth/badge.svg)](https://docs.rs/rocket-basicauth/)
2//!
3//! A high-level [basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) request guard for [Rocket.rs](https://rocket.rs)
4//!
5//! ## Example
6//!
7//! ```rust
8//! #[macro_use] extern crate rocket;
9//!
10//! use rocket_basicauth::BasicAuth;
11//!
12//! /// Hello route with `auth` request guard, containing a `name` and `password`
13//! #[get("/hello/<age>")]
14//! fn hello(auth: BasicAuth, age: u8) -> String {
15//!     format!("Hello, {} year old named {}!", age, auth.username)
16//! }
17//!
18//! #[launch]
19//! fn rocket() -> _ {
20//!     rocket::build().mount("/", routes![hello])
21//! }
22//! ```
23//!
24//! ## Installation
25//!
26//! Simply add the following to your `Cargo.toml` file:
27//!
28//! ```toml
29//! [dependencies]
30//! rocket-basicauth = "2"
31//! ```
32//!
33//! #### Disabling logging
34//!
35//! By default, this crate uses the [`log`](https://crates.io/crates/log) library to automatically add minimal trace-level logging, to disable this, instead write:
36//!
37//! ```toml
38//! [dependencies]
39//! rocket-basicauth = { version = "2", default-features = false }
40//! ```
41//!
42//! #### Rocket 0.4
43//!
44//! Support for Rocket 0.4 is **decrepit** in the eyes of this crate but may still be used by changing the version, to do this, instead write:
45//!
46//! ```toml
47//! [dependencies]
48//! rocket-basicauth = "1"
49//! ```
50//!
51//! ## Security
52//!
53//! Some essential security considerations to take into account are the following:
54//!
55//! - This crate has not been audited by any security professionals. If you are willing to do or have already done an audit on this crate, please create an issue as it would help out enormously! 😊
56//! - This crate purposefully does not limit the maximum length of http basic auth headers arriving so please ensure your webserver configurations are set properly.
57
58use base64;
59#[cfg(feature = "log")]
60use log::trace;
61use rocket::http::Status;
62use rocket::outcome::Outcome;
63use rocket::request::{self, FromRequest, Request};
64
65/// Contains errors relating to the [BasicAuth] request guard
66#[derive(Debug)]
67pub enum BasicAuthError {
68    /// Length check fail or misc error
69    BadCount,
70
71    /// Header is missing and is required
72    //Missing, // NOTE: removed migrating to 0.5 in v2 of this crate
73
74    /// Header is invalid in formatting/encoding
75    Invalid,
76}
77
78/// Decodes a base64-encoded string into a tuple of `(username, password)` or a
79/// [Option::None] if badly formatted, e.g. if an error occurs
80fn decode_to_creds<T: Into<String>>(base64_encoded: T) -> Option<(String, String)> {
81    let decoded_creds = match base64::decode(base64_encoded.into()) {
82        Ok(cred_bytes) => String::from_utf8(cred_bytes).unwrap(),
83        Err(_) => return None,
84    };
85
86    if let Some((username, password)) = decoded_creds.split_once(":") {
87        #[cfg(feature = "log")]
88        {
89            const TRUNCATE_LEN: usize = 64;
90            let mut s = username.to_string();
91            let fmt_id = if username.len() > TRUNCATE_LEN {
92                s.truncate(TRUNCATE_LEN);
93                format!("{}.. (truncated to {})", s, TRUNCATE_LEN)
94            } else {
95                s
96            };
97
98            trace!(
99                "Decoded basic authentication credentials for user of id {}",
100                fmt_id
101            );
102        }
103      
104        Some((username.to_owned(), password.to_owned()))
105    } else {
106        None
107    }
108}
109
110/// A high-level [basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
111/// request guard implementation, containing the `username` and `password` used for
112/// authentication
113///
114/// # Example
115///
116/// ```no_run
117/// #[macro_use] extern crate rocket;
118///
119/// use rocket_basicauth::BasicAuth;
120///
121/// /// Hello route with `auth` request guard, containing a `username` and `password`
122/// #[get("/hello/<age>")]
123/// fn hello(auth: BasicAuth, age: u8) -> String {
124///     format!("Hello, {} year old named {}!", age, auth.username)
125/// }
126///
127/// #[launch]
128/// fn rocket() -> _ {
129///     rocket::build().mount("/", routes![hello])
130/// }
131/// ```
132#[derive(Debug)]
133pub struct BasicAuth {
134    /// Required username
135    pub username: String,
136
137    /// Required password
138    pub password: String,
139}
140
141impl BasicAuth {
142    /// Creates a new [BasicAuth] struct/request guard from a given plaintext
143    /// http auth header or returns a [Option::None] if invalid
144    pub fn new<T: Into<String>>(auth_header: T) -> Option<Self> {
145        let key = auth_header.into();
146
147        if key.len() < 7 || &key[..6] != "Basic " {
148            return None;
149        }
150
151        let (username, password) = decode_to_creds(&key[6..])?;
152        Some(Self { username, password })
153    }
154}
155
156#[rocket::async_trait]
157impl<'r> FromRequest<'r> for BasicAuth {
158    type Error = BasicAuthError;
159
160    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
161        #[cfg(feature = "log")]
162        trace!("Basic authorization requested, starting decode process");
163
164        let keys: Vec<_> = request.headers().get("Authorization").collect();
165        match keys.len() {
166            0 => Outcome::Forward(Status::Unauthorized),
167            1 => match BasicAuth::new(keys[0]) {
168                Some(auth_header) => Outcome::Success(auth_header),
169                None => Outcome::Error((Status::BadRequest, BasicAuthError::Invalid)),
170            },
171            _ => Outcome::Error((Status::BadRequest, BasicAuthError::BadCount)),
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn decode_to_creds_check() {
182        // Tests: name:password
183        assert_eq!(
184            decode_to_creds("bmFtZTpwYXNzd29yZA=="),
185            Some(("name".to_string(), "password".to_string()))
186        );
187        // Tests: name:pass:word
188        assert_eq!(
189            decode_to_creds("bmFtZTpwYXNzOndvcmQ="),
190            Some(("name".to_string(), "pass:word".to_string()))
191        );
192        // Tests: emptypass:
193        assert_eq!(
194            decode_to_creds("ZW1wdHlwYXNzOg=="),
195            Some(("emptypass".to_string(), "".to_string()))
196        );
197        // Tests: :
198        assert_eq!(
199            decode_to_creds("Og=="),
200            Some(("".to_string(), "".to_string()))
201        );
202        assert_eq!(decode_to_creds("bm9jb2xvbg=="), None);
203    }
204}