auth_headers/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2#![deny(trivial_casts, trivial_numeric_casts, unused_extern_crates, unused_qualifications)]
3#![warn(
4	missing_debug_implementations,
5	missing_docs,
6	unused_import_braces,
7	dead_code,
8	clippy::unwrap_used,
9	clippy::expect_used,
10	clippy::missing_docs_in_private_items
11)]
12
13//! Authorization header parser
14
15use std::{borrow::Borrow, fmt::Debug};
16
17pub use error::ParseError;
18use http::{header::InvalidHeaderValue, HeaderMap, HeaderValue};
19
20use crate::error::CredentialsDecodeError;
21
22mod error;
23
24/// [Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
25#[derive(Debug)]
26pub enum AuthorizationHeader {
27	/// [Basic auth header](https://datatracker.ietf.org/doc/html/rfc7617)
28	Basic {
29		/// Basic auth username
30		username: String,
31		/// Basic auth password
32		password: String,
33	},
34	/// [Bearer token](https://datatracker.ietf.org/doc/html/rfc6750)
35	Bearer {
36		/// Bearer token
37		token: String,
38	},
39}
40
41impl TryFrom<HeaderMap> for AuthorizationHeader {
42	type Error = ParseError;
43
44	fn try_from(value: HeaderMap) -> Result<Self, Self::Error> {
45		let auth_head_parts: Vec<&str> = value
46			.get(http::header::AUTHORIZATION)
47			.ok_or(ParseError::AuthHeaderMissing)?
48			.to_str()
49			.map_err(|_| ParseError::InvalidCharacters)?
50			.split(' ')
51			.collect();
52
53		let (auth_type, auth_content) =
54			auth_head_parts.split_first().ok_or(ParseError::BadFormat)?;
55
56		match (auth_type.to_lowercase().as_str(), auth_content) {
57			("basic", [auth_content, ..]) => Ok(Self::parse_basic_auth(auth_content)?),
58			("bearer", [auth_content, ..]) => {
59				Ok(Self::Bearer { token: (*auth_content).to_string() })
60			}
61			(auth_type, [..]) => Err(ParseError::unknown_authentication_type(auth_type)),
62		}
63	}
64}
65
66impl TryFrom<&HeaderMap> for AuthorizationHeader {
67	type Error = ParseError;
68
69	fn try_from(value: &HeaderMap) -> Result<Self, Self::Error> {
70		value.to_owned().try_into()
71	}
72}
73
74impl TryFrom<&AuthorizationHeader> for HeaderMap {
75	type Error = ParseError;
76	fn try_from(auth: &AuthorizationHeader) -> Result<Self, Self::Error> {
77		let mut headers = HeaderMap::new();
78		headers.insert(http::header::AUTHORIZATION, auth.header_value()?);
79		Ok(headers)
80	}
81}
82
83impl TryFrom<AuthorizationHeader> for HeaderMap {
84	type Error = ParseError;
85	fn try_from(auth: AuthorizationHeader) -> Result<Self, Self::Error> {
86		auth.borrow().try_into()
87	}
88}
89
90impl AuthorizationHeader {
91	/// Constructor for basic authorization header
92	pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
93		Self::Basic { username: username.into(), password: password.into() }
94	}
95
96	/// Constructor for bearer authorization header
97	pub fn bearer(token: impl Into<String>) -> Self {
98		Self::Bearer { token: token.into() }
99	}
100
101	/// parse the basic auth token
102	fn parse_basic_auth(auth: &str) -> Result<Self, CredentialsDecodeError> {
103		let auth_string = String::from_utf8(base64::decode(auth)?)?;
104		let (username, password) =
105			auth_string.split_once(':').ok_or(CredentialsDecodeError::DelimiterNotFound)?;
106		Ok(Self::Basic { username: username.to_owned(), password: password.to_owned() })
107	}
108
109	/// generate a HeaderValue
110	/// ```
111	/// # use auth_headers::AuthorizationHeader;
112	///
113	/// let header = AuthorizationHeader::basic("aladdin", "opensesame");
114	/// assert_eq!(
115	///     http::HeaderValue::from_str("Basic YWxhZGRpbjpvcGVuc2VzYW1l").unwrap(),
116	///     header.header_value().unwrap()
117	/// );
118	/// ```
119	pub fn header_value(&self) -> Result<HeaderValue, InvalidHeaderValue> {
120		let value = match self {
121			Self::Basic { username, password } => {
122				format!("Basic {}", base64::encode(format!("{}:{}", username, password)))
123			}
124			Self::Bearer { token } => format!("Bearer {}", token),
125		};
126		value.parse()
127	}
128}
129
130#[cfg(test)]
131mod tests {
132	use eyre::{bail, Result};
133
134	use super::*;
135
136	#[test]
137	fn header_parse_basic() -> Result<()> {
138		let mut header_map = http::header::HeaderMap::new();
139		header_map.insert(http::header::AUTHORIZATION, "Basic YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
140
141		let auth = AuthorizationHeader::try_from(&header_map)?;
142		if let AuthorizationHeader::Basic { username, password } = auth {
143			assert_eq!("aladdin", username.as_str());
144			assert_eq!("opensesame", password.as_str());
145		} else {
146			bail!("Parsed header wasn't identified as Basic")
147		}
148		Ok(())
149	}
150
151	#[test]
152	fn header_parse_bearer() -> Result<()> {
153		let mut header_map = http::header::HeaderMap::new();
154		header_map.insert(http::header::AUTHORIZATION, "Bearer YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
155
156		let auth = AuthorizationHeader::try_from(&header_map)?;
157		if let AuthorizationHeader::Bearer { token } = auth {
158			assert_eq!("YWxhZGRpbjpvcGVuc2VzYW1l", token.as_str());
159		} else {
160			bail!("Parsed header wasn't identified as Basic")
161		}
162		Ok(())
163	}
164
165	#[test]
166	fn header_parse_unimplemented() -> Result<()> {
167		let mut header_map = http::header::HeaderMap::new();
168		header_map
169			.insert(http::header::AUTHORIZATION, "FoxieAuth YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
170
171		let res = AuthorizationHeader::try_from(&header_map);
172		match res {
173			Err(ParseError::UnknownAuthenticationType(auth_type)) => {
174				assert_eq!("foxieauth", auth_type.as_str())
175			}
176			Err(_) => bail!("Wrong error type"),
177			Ok(_) => bail!("This authorization type shouldn't work at all"),
178		}
179		Ok(())
180	}
181
182	#[test]
183	fn header_create_basic_auth() -> Result<()> {
184		let header = AuthorizationHeader::basic("aladdin", "opensesame");
185		assert_eq!(
186			HeaderValue::from_str("Basic YWxhZGRpbjpvcGVuc2VzYW1l")?,
187			header.header_value()?
188		);
189		Ok(())
190	}
191
192	#[test]
193	fn header_create_bearer_auth() -> Result<()> {
194		let header = &AuthorizationHeader::bearer("fox");
195		let mut header_map = HeaderMap::new();
196		header_map.insert(http::header::AUTHORIZATION, HeaderValue::from_str("Bearer fox")?);
197		assert_eq!(header_map, header.try_into()?);
198		Ok(())
199	}
200}