auth-headers 0.1.0

Simple library for authorization header parsing / creation
Documentation
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
#![deny(trivial_casts, trivial_numeric_casts, unused_extern_crates, unused_qualifications)]
#![warn(
	missing_debug_implementations,
	missing_docs,
	unused_import_braces,
	dead_code,
	clippy::unwrap_used,
	clippy::expect_used,
	clippy::missing_docs_in_private_items
)]

//! Authorization header parser

use std::{borrow::Borrow, fmt::Debug};

pub use error::ParseError;
use http::{header::InvalidHeaderValue, HeaderMap, HeaderValue};

use crate::error::CredentialsDecodeError;

mod error;

/// [Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
#[derive(Debug)]
pub enum AuthorizationHeader {
	/// [Basic auth header](https://datatracker.ietf.org/doc/html/rfc7617)
	Basic {
		/// Basic auth username
		username: String,
		/// Basic auth password
		password: String,
	},
	/// [Bearer token](https://datatracker.ietf.org/doc/html/rfc6750)
	Bearer {
		/// Bearer token
		token: String,
	},
}

impl TryFrom<HeaderMap> for AuthorizationHeader {
	type Error = ParseError;

	fn try_from(value: HeaderMap) -> Result<Self, Self::Error> {
		let auth_head_parts: Vec<&str> = value
			.get(http::header::AUTHORIZATION)
			.ok_or(ParseError::AuthHeaderMissing)?
			.to_str()
			.map_err(|_| ParseError::InvalidCharacters)?
			.split(' ')
			.collect();

		let (auth_type, auth_content) =
			auth_head_parts.split_first().ok_or(ParseError::BadFormat)?;

		match (auth_type.to_lowercase().as_str(), auth_content) {
			("basic", [auth_content, ..]) => Ok(Self::parse_basic_auth(auth_content)?),
			("bearer", [auth_content, ..]) => {
				Ok(Self::Bearer { token: (*auth_content).to_string() })
			}
			(auth_type, [..]) => Err(ParseError::unknown_authentication_type(auth_type)),
		}
	}
}

impl TryFrom<&HeaderMap> for AuthorizationHeader {
	type Error = ParseError;

	fn try_from(value: &HeaderMap) -> Result<Self, Self::Error> {
		value.to_owned().try_into()
	}
}

impl TryFrom<&AuthorizationHeader> for HeaderMap {
	type Error = ParseError;
	fn try_from(auth: &AuthorizationHeader) -> Result<Self, Self::Error> {
		let mut headers = HeaderMap::new();
		headers.insert(http::header::AUTHORIZATION, auth.header_value()?);
		Ok(headers)
	}
}

impl TryFrom<AuthorizationHeader> for HeaderMap {
	type Error = ParseError;
	fn try_from(auth: AuthorizationHeader) -> Result<Self, Self::Error> {
		auth.borrow().try_into()
	}
}

impl AuthorizationHeader {
	/// Constructor for basic authorization header
	pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
		Self::Basic { username: username.into(), password: password.into() }
	}

	/// Constructor for bearer authorization header
	pub fn bearer(token: impl Into<String>) -> Self {
		Self::Bearer { token: token.into() }
	}

	/// parse the basic auth token
	fn parse_basic_auth(auth: &str) -> Result<Self, CredentialsDecodeError> {
		let auth_string = String::from_utf8(base64::decode(auth)?)?;
		let (username, password) =
			auth_string.split_once(':').ok_or(CredentialsDecodeError::DelimiterNotFound)?;
		Ok(Self::Basic { username: username.to_owned(), password: password.to_owned() })
	}

	/// generate a HeaderValue
	/// ```
	/// # use auth_headers::AuthorizationHeader;
	///
	/// let header = AuthorizationHeader::basic("aladdin", "opensesame");
	/// assert_eq!(
	///     http::HeaderValue::from_str("Basic YWxhZGRpbjpvcGVuc2VzYW1l").unwrap(),
	///     header.header_value().unwrap()
	/// );
	/// ```
	pub fn header_value(&self) -> Result<HeaderValue, InvalidHeaderValue> {
		let value = match self {
			Self::Basic { username, password } => {
				format!("Basic {}", base64::encode(format!("{}:{}", username, password)))
			}
			Self::Bearer { token } => format!("Bearer {}", token),
		};
		value.parse()
	}
}

#[cfg(test)]
mod tests {
	use eyre::{bail, Result};

	use super::*;

	#[test]
	fn header_parse_basic() -> Result<()> {
		let mut header_map = http::header::HeaderMap::new();
		header_map.insert(http::header::AUTHORIZATION, "Basic YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);

		let auth = AuthorizationHeader::try_from(&header_map)?;
		if let AuthorizationHeader::Basic { username, password } = auth {
			assert_eq!("aladdin", username.as_str());
			assert_eq!("opensesame", password.as_str());
		} else {
			bail!("Parsed header wasn't identified as Basic")
		}
		Ok(())
	}

	#[test]
	fn header_parse_bearer() -> Result<()> {
		let mut header_map = http::header::HeaderMap::new();
		header_map.insert(http::header::AUTHORIZATION, "Bearer YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);

		let auth = AuthorizationHeader::try_from(&header_map)?;
		if let AuthorizationHeader::Bearer { token } = auth {
			assert_eq!("YWxhZGRpbjpvcGVuc2VzYW1l", token.as_str());
		} else {
			bail!("Parsed header wasn't identified as Basic")
		}
		Ok(())
	}

	#[test]
	fn header_parse_unimplemented() -> Result<()> {
		let mut header_map = http::header::HeaderMap::new();
		header_map
			.insert(http::header::AUTHORIZATION, "FoxieAuth YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);

		let res = AuthorizationHeader::try_from(&header_map);
		match res {
			Err(ParseError::UnknownAuthenticationType(auth_type)) => {
				assert_eq!("foxieauth", auth_type.as_str())
			}
			Err(_) => bail!("Wrong error type"),
			Ok(_) => bail!("This authorization type shouldn't work at all"),
		}
		Ok(())
	}

	#[test]
	fn header_create_basic_auth() -> Result<()> {
		let header = AuthorizationHeader::basic("aladdin", "opensesame");
		assert_eq!(
			HeaderValue::from_str("Basic YWxhZGRpbjpvcGVuc2VzYW1l")?,
			header.header_value()?
		);
		Ok(())
	}

	#[test]
	fn header_create_bearer_auth() -> Result<()> {
		let header = &AuthorizationHeader::bearer("fox");
		let mut header_map = HeaderMap::new();
		header_map.insert(http::header::AUTHORIZATION, HeaderValue::from_str("Bearer fox")?);
		assert_eq!(header_map, header.try_into()?);
		Ok(())
	}
}