Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auth::{Authorization, AuthenticationScheme, BasicAuth, WwwAuthenticate, ProxyAuthorization, ProxyAuthenticate} #252

Merged
merged 11 commits into from
Oct 25, 2020
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ serde = { version = "1.0.106", features = ["derive"] }
serde_urlencoded = "0.7.0"
rand = "0.7.3"
serde_qs = "0.7.0"
base64 = "0.13.0"

[dev-dependencies]
http = "0.2.0"
Expand Down
70 changes: 70 additions & 0 deletions src/auth/authentication_scheme.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::fmt::{self, Display};
use std::str::FromStr;

use crate::bail_status as bail;

/// HTTP Mutual Authentication Algorithms
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub enum AuthenticationScheme {
/// [RFC7617](https://tools.ietf.org/html/rfc7617) Basic auth
Basic,
/// [RFC6750](https://tools.ietf.org/html/rfc6750) Bearer auth
Bearer,
/// [RFC7616](https://tools.ietf.org/html/rfc7616) Digest auth
Digest,
/// [RFC7486](https://tools.ietf.org/html/rfc7486) HTTP Origin-Bound Authentication (HOBA)
Hoba,
/// [RFC8120](https://tools.ietf.org/html/rfc8120) Mutual auth
Mutual,
/// [RFC4559](https://tools.ietf.org/html/rfc4559) Negotiate auth
Negotiate,
/// [RFC5849](https://tools.ietf.org/html/rfc5849) OAuth
OAuth,
/// [RFC7804](https://tools.ietf.org/html/rfc7804) SCRAM SHA1 auth
ScramSha1,
/// [RFC7804](https://tools.ietf.org/html/rfc7804) SCRAM SHA256 auth
ScramSha256,
/// [RFC8292](https://tools.ietf.org/html/rfc8292) Vapid auth
Vapid,
}

impl Display for AuthenticationScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Basic => write!(f, "Basic"),
Self::Bearer => write!(f, "Bearer"),
Self::Digest => write!(f, "Digest"),
Self::Hoba => write!(f, "HOBA"),
Self::Mutual => write!(f, "Mutual"),
Self::Negotiate => write!(f, "Negotiate"),
Self::OAuth => write!(f, "OAuth"),
Self::ScramSha1 => write!(f, "SCRAM-SHA-1"),
Self::ScramSha256 => write!(f, "SCRAM-SHA-256"),
Self::Vapid => write!(f, "vapid"),
}
}
}

impl FromStr for AuthenticationScheme {
type Err = crate::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// NOTE(yosh): matching here is lowercase as specified by RFC2617#section-1.2
// > [...] case-insensitive token to identify the authentication scheme [...]
// https://tools.ietf.org/html/rfc2617#section-1.2
match s.to_lowercase().as_str() {
"basic" => Ok(Self::Basic),
"bearer" => Ok(Self::Bearer),
"digest" => Ok(Self::Digest),
"hoba" => Ok(Self::Hoba),
"mutual" => Ok(Self::Mutual),
"negotiate" => Ok(Self::Negotiate),
"oauth" => Ok(Self::OAuth),
"scram-sha-1" => Ok(Self::ScramSha1),
"scram-sha-256" => Ok(Self::ScramSha256),
"vapid" => Ok(Self::Vapid),
s => bail!(400, "`{}` is not a recognized authentication scheme", s),
}
}
}
142 changes: 142 additions & 0 deletions src/auth/authorization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use crate::auth::AuthenticationScheme;
use crate::bail_status as bail;
use crate::headers::{HeaderName, HeaderValue, Headers, AUTHORIZATION};

/// Credentials to authenticate a user agent with a server.
///
/// # Specifications
///
/// - [RFC 7235, section 4.2: Authorization](https://tools.ietf.org/html/rfc7235#section-4.2)
///
/// # Examples
///
/// ```
/// # fn main() -> http_types::Result<()> {
/// #
/// use http_types::Response;
/// use http_types::auth::{AuthenticationScheme, Authorization};
///
/// let scheme = AuthenticationScheme::Basic;
/// let credentials = "0xdeadbeef202020";
/// let authz = Authorization::new(scheme, credentials.into());
///
/// let mut res = Response::new(200);
/// authz.apply(&mut res);
///
/// let authz = Authorization::from_headers(res)?.unwrap();
///
/// assert_eq!(authz.scheme(), AuthenticationScheme::Basic);
/// assert_eq!(authz.credentials(), credentials);
/// #
/// # Ok(()) }
/// ```
#[derive(Debug)]
pub struct Authorization {
scheme: AuthenticationScheme,
credentials: String,
}

impl Authorization {
/// Create a new instance of `Authorization`.
pub fn new(scheme: AuthenticationScheme, credentials: String) -> Self {
Self {
scheme,
credentials,
}
}

/// Create a new instance from headers.
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
let headers = match headers.as_ref().get(AUTHORIZATION) {
Some(headers) => headers,
None => return Ok(None),
};

// If we successfully parsed the header then there's always at least one
// entry. We want the last entry.
let value = headers.iter().last().unwrap();

let mut iter = value.as_str().splitn(2, ' ');
let scheme = iter.next();
let credential = iter.next();
let (scheme, credentials) = match (scheme, credential) {
(None, _) => bail!(400, "Could not find scheme"),
(Some(_), None) => bail!(400, "Could not find credentials"),
(Some(scheme), Some(credentials)) => (scheme.parse()?, credentials.to_owned()),
};

Ok(Some(Self {
scheme,
credentials,
}))
}

/// Sets the header.
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
headers.as_mut().insert(self.name(), self.value());
}

/// Get the `HeaderName`.
pub fn name(&self) -> HeaderName {
AUTHORIZATION
}

/// Get the `HeaderValue`.
pub fn value(&self) -> HeaderValue {
let output = format!("{} {}", self.scheme, self.credentials);

// SAFETY: the internal string is validated to be ASCII.
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
}

/// Get the authorization scheme.
pub fn scheme(&self) -> AuthenticationScheme {
self.scheme
}

/// Set the authorization scheme.
pub fn set_scheme(&mut self, scheme: AuthenticationScheme) {
self.scheme = scheme;
}

/// Get the authorization credentials.
pub fn credentials(&self) -> &str {
self.credentials.as_str()
}

/// Set the authorization credentials.
pub fn set_credentials(&mut self, credentials: String) {
self.credentials = credentials;
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::headers::Headers;

#[test]
fn smoke() -> crate::Result<()> {
let scheme = AuthenticationScheme::Basic;
let credentials = "0xdeadbeef202020";
let authz = Authorization::new(scheme, credentials.into());

let mut headers = Headers::new();
authz.apply(&mut headers);

let authz = Authorization::from_headers(headers)?.unwrap();

assert_eq!(authz.scheme(), AuthenticationScheme::Basic);
assert_eq!(authz.credentials(), credentials);
Ok(())
}

#[test]
fn bad_request_on_parse_error() -> crate::Result<()> {
let mut headers = Headers::new();
headers.insert(AUTHORIZATION, "<nori ate the tag. yum.>");
let err = Authorization::from_headers(headers).unwrap_err();
assert_eq!(err.status(), 400);
Ok(())
}
}
141 changes: 141 additions & 0 deletions src/auth/basic_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use crate::auth::{AuthenticationScheme, Authorization};
use crate::headers::{HeaderName, HeaderValue, Headers, AUTHORIZATION};
use crate::Status;
use crate::{bail_status as bail, ensure_status as ensure};

/// HTTP Basic authorization.
///
/// # Specifications
///
/// - [RFC7617](https://tools.ietf.org/html/rfc7617)
///
/// # Examples
///
/// ```
/// # fn main() -> http_types::Result<()> {
/// #
/// use http_types::Response;
/// use http_types::auth::{AuthenticationScheme, BasicAuth};
///
/// let username = "nori";
/// let password = "secret_fish!!";
/// let authz = BasicAuth::new(username, password);
///
/// let mut res = Response::new(200);
/// authz.apply(&mut res);
///
/// let authz = BasicAuth::from_headers(res)?.unwrap();
///
/// assert_eq!(authz.username(), username);
/// assert_eq!(authz.password(), password);
/// #
/// # Ok(()) }
/// ```
#[derive(Debug)]
pub struct BasicAuth {
username: String,
password: String,
}

impl BasicAuth {
/// Create a new instance of `BasicAuth`.
pub fn new<U, P>(username: U, password: P) -> Self
where
U: AsRef<str>,
P: AsRef<str>,
{
let username = username.as_ref().to_owned();
let password = password.as_ref().to_owned();
Self { username, password }
}

/// Create a new instance from headers.
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
let auth = match Authorization::from_headers(headers)? {
Some(auth) => auth,
None => return Ok(None),
};

let scheme = auth.scheme();
ensure!(
matches!(scheme, AuthenticationScheme::Basic),
400,
"Expected basic auth scheme found `{}`",
scheme
);

let bytes = base64::decode(auth.credentials()).status(400)?;
let credentials = String::from_utf8(bytes).status(400)?;

let mut iter = credentials.splitn(2, ':');
let username = iter.next();
let password = iter.next();

let (username, password) = match (username, password) {
(Some(username), Some(password)) => (username.to_string(), password.to_string()),
(Some(_), None) => bail!(400, "Expected basic auth to contain a password"),
(None, _) => bail!(400, "Expected basic auth to contain a username"),
};

Ok(Some(Self { username, password }))
}

/// Sets the header.
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
headers.as_mut().insert(self.name(), self.value());
}

/// Get the `HeaderName`.
pub fn name(&self) -> HeaderName {
AUTHORIZATION
}

/// Get the `HeaderValue`.
pub fn value(&self) -> HeaderValue {
let scheme = AuthenticationScheme::Basic;
let credentials = base64::encode(format!("{}:{}", self.username, self.password));
let auth = Authorization::new(scheme, credentials);
auth.value()
}

/// Get the username.
pub fn username(&self) -> &str {
self.username.as_str()
}

/// Get the password.
pub fn password(&self) -> &str {
self.password.as_str()
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::headers::Headers;

#[test]
fn smoke() -> crate::Result<()> {
let username = "nori";
let password = "secret_fish!!";
let authz = BasicAuth::new(username, password);

let mut headers = Headers::new();
authz.apply(&mut headers);

let authz = BasicAuth::from_headers(headers)?.unwrap();

assert_eq!(authz.username(), username);
assert_eq!(authz.password(), password);
Ok(())
}

#[test]
fn bad_request_on_parse_error() -> crate::Result<()> {
let mut headers = Headers::new();
headers.insert(AUTHORIZATION, "<nori ate the tag. yum.>");
let err = BasicAuth::from_headers(headers).unwrap_err();
assert_eq!(err.status(), 400);
Ok(())
}
}
34 changes: 34 additions & 0 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! HTTP authentication and authorization.
//!
//! # Examples
//!
//! ```
//! # fn main() -> http_types::Result<()> {
//! #
//! use http_types::Response;
//! use http_types::auth::{AuthenticationScheme, BasicAuth};
//!
//! let username = "nori";
//! let password = "secret_fish!!";
//! let authz = BasicAuth::new(username, password);
//!
//! let mut res = Response::new(200);
//! authz.apply(&mut res);
//!
//! let authz = BasicAuth::from_headers(res)?.unwrap();
//!
//! assert_eq!(authz.username(), username);
//! assert_eq!(authz.password(), password);
//! #
//! # Ok(()) }
//! ```

mod authentication_scheme;
mod authorization;
mod basic_auth;
mod www_authenticate;

pub use authentication_scheme::AuthenticationScheme;
pub use authorization::Authorization;
pub use basic_auth::BasicAuth;
pub use www_authenticate::WwwAuthenticate;
Loading