rocket_basicauth/lib.rs
1//! [](https://github.com/Owez/rocket-basicauth/actions?query=workflow%3ATests) [](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}