Skip to content

Commit 9127b5f

Browse files
authored
Merge pull request #252 from http-rs/basic-auth
Add `auth::{Authorization, AuthenticationScheme, BasicAuth, WwwAuthenticate, ProxyAuthorization, ProxyAuthenticate}`
2 parents 8aa4f52 + 61681e7 commit 9127b5f

File tree

8 files changed

+666
-12
lines changed

8 files changed

+666
-12
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ serde = { version = "1.0.106", features = ["derive"] }
4343
serde_urlencoded = "0.7.0"
4444
rand = "0.7.3"
4545
serde_qs = "0.7.0"
46+
base64 = "0.13.0"
4647

4748
[dev-dependencies]
4849
http = "0.2.0"

src/auth/authentication_scheme.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use std::fmt::{self, Display};
2+
use std::str::FromStr;
3+
4+
use crate::bail_status as bail;
5+
6+
/// HTTP Mutual Authentication Algorithms
7+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
8+
#[non_exhaustive]
9+
pub enum AuthenticationScheme {
10+
/// [RFC7617](https://tools.ietf.org/html/rfc7617) Basic auth
11+
Basic,
12+
/// [RFC6750](https://tools.ietf.org/html/rfc6750) Bearer auth
13+
Bearer,
14+
/// [RFC7616](https://tools.ietf.org/html/rfc7616) Digest auth
15+
Digest,
16+
/// [RFC7486](https://tools.ietf.org/html/rfc7486) HTTP Origin-Bound Authentication (HOBA)
17+
Hoba,
18+
/// [RFC8120](https://tools.ietf.org/html/rfc8120) Mutual auth
19+
Mutual,
20+
/// [RFC4559](https://tools.ietf.org/html/rfc4559) Negotiate auth
21+
Negotiate,
22+
/// [RFC5849](https://tools.ietf.org/html/rfc5849) OAuth
23+
OAuth,
24+
/// [RFC7804](https://tools.ietf.org/html/rfc7804) SCRAM SHA1 auth
25+
ScramSha1,
26+
/// [RFC7804](https://tools.ietf.org/html/rfc7804) SCRAM SHA256 auth
27+
ScramSha256,
28+
/// [RFC8292](https://tools.ietf.org/html/rfc8292) Vapid auth
29+
Vapid,
30+
}
31+
32+
impl Display for AuthenticationScheme {
33+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34+
match self {
35+
Self::Basic => write!(f, "Basic"),
36+
Self::Bearer => write!(f, "Bearer"),
37+
Self::Digest => write!(f, "Digest"),
38+
Self::Hoba => write!(f, "HOBA"),
39+
Self::Mutual => write!(f, "Mutual"),
40+
Self::Negotiate => write!(f, "Negotiate"),
41+
Self::OAuth => write!(f, "OAuth"),
42+
Self::ScramSha1 => write!(f, "SCRAM-SHA-1"),
43+
Self::ScramSha256 => write!(f, "SCRAM-SHA-256"),
44+
Self::Vapid => write!(f, "vapid"),
45+
}
46+
}
47+
}
48+
49+
impl FromStr for AuthenticationScheme {
50+
type Err = crate::Error;
51+
52+
fn from_str(s: &str) -> Result<Self, Self::Err> {
53+
// NOTE(yosh): matching here is lowercase as specified by RFC2617#section-1.2
54+
// > [...] case-insensitive token to identify the authentication scheme [...]
55+
// https://tools.ietf.org/html/rfc2617#section-1.2
56+
match s.to_lowercase().as_str() {
57+
"basic" => Ok(Self::Basic),
58+
"bearer" => Ok(Self::Bearer),
59+
"digest" => Ok(Self::Digest),
60+
"hoba" => Ok(Self::Hoba),
61+
"mutual" => Ok(Self::Mutual),
62+
"negotiate" => Ok(Self::Negotiate),
63+
"oauth" => Ok(Self::OAuth),
64+
"scram-sha-1" => Ok(Self::ScramSha1),
65+
"scram-sha-256" => Ok(Self::ScramSha256),
66+
"vapid" => Ok(Self::Vapid),
67+
s => bail!(400, "`{}` is not a recognized authentication scheme", s),
68+
}
69+
}
70+
}

src/auth/authorization.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use crate::auth::AuthenticationScheme;
2+
use crate::bail_status as bail;
3+
use crate::headers::{HeaderName, HeaderValue, Headers, AUTHORIZATION};
4+
5+
/// Credentials to authenticate a user agent with a server.
6+
///
7+
/// # Specifications
8+
///
9+
/// - [RFC 7235, section 4.2: Authorization](https://tools.ietf.org/html/rfc7235#section-4.2)
10+
///
11+
/// # Examples
12+
///
13+
/// ```
14+
/// # fn main() -> http_types::Result<()> {
15+
/// #
16+
/// use http_types::Response;
17+
/// use http_types::auth::{AuthenticationScheme, Authorization};
18+
///
19+
/// let scheme = AuthenticationScheme::Basic;
20+
/// let credentials = "0xdeadbeef202020";
21+
/// let authz = Authorization::new(scheme, credentials.into());
22+
///
23+
/// let mut res = Response::new(200);
24+
/// authz.apply(&mut res);
25+
///
26+
/// let authz = Authorization::from_headers(res)?.unwrap();
27+
///
28+
/// assert_eq!(authz.scheme(), AuthenticationScheme::Basic);
29+
/// assert_eq!(authz.credentials(), credentials);
30+
/// #
31+
/// # Ok(()) }
32+
/// ```
33+
#[derive(Debug)]
34+
pub struct Authorization {
35+
scheme: AuthenticationScheme,
36+
credentials: String,
37+
}
38+
39+
impl Authorization {
40+
/// Create a new instance of `Authorization`.
41+
pub fn new(scheme: AuthenticationScheme, credentials: String) -> Self {
42+
Self {
43+
scheme,
44+
credentials,
45+
}
46+
}
47+
48+
/// Create a new instance from headers.
49+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
50+
let headers = match headers.as_ref().get(AUTHORIZATION) {
51+
Some(headers) => headers,
52+
None => return Ok(None),
53+
};
54+
55+
// If we successfully parsed the header then there's always at least one
56+
// entry. We want the last entry.
57+
let value = headers.iter().last().unwrap();
58+
59+
let mut iter = value.as_str().splitn(2, ' ');
60+
let scheme = iter.next();
61+
let credential = iter.next();
62+
let (scheme, credentials) = match (scheme, credential) {
63+
(None, _) => bail!(400, "Could not find scheme"),
64+
(Some(_), None) => bail!(400, "Could not find credentials"),
65+
(Some(scheme), Some(credentials)) => (scheme.parse()?, credentials.to_owned()),
66+
};
67+
68+
Ok(Some(Self {
69+
scheme,
70+
credentials,
71+
}))
72+
}
73+
74+
/// Sets the header.
75+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
76+
headers.as_mut().insert(self.name(), self.value());
77+
}
78+
79+
/// Get the `HeaderName`.
80+
pub fn name(&self) -> HeaderName {
81+
AUTHORIZATION
82+
}
83+
84+
/// Get the `HeaderValue`.
85+
pub fn value(&self) -> HeaderValue {
86+
let output = format!("{} {}", self.scheme, self.credentials);
87+
88+
// SAFETY: the internal string is validated to be ASCII.
89+
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
90+
}
91+
92+
/// Get the authorization scheme.
93+
pub fn scheme(&self) -> AuthenticationScheme {
94+
self.scheme
95+
}
96+
97+
/// Set the authorization scheme.
98+
pub fn set_scheme(&mut self, scheme: AuthenticationScheme) {
99+
self.scheme = scheme;
100+
}
101+
102+
/// Get the authorization credentials.
103+
pub fn credentials(&self) -> &str {
104+
self.credentials.as_str()
105+
}
106+
107+
/// Set the authorization credentials.
108+
pub fn set_credentials(&mut self, credentials: String) {
109+
self.credentials = credentials;
110+
}
111+
}
112+
113+
#[cfg(test)]
114+
mod test {
115+
use super::*;
116+
use crate::headers::Headers;
117+
118+
#[test]
119+
fn smoke() -> crate::Result<()> {
120+
let scheme = AuthenticationScheme::Basic;
121+
let credentials = "0xdeadbeef202020";
122+
let authz = Authorization::new(scheme, credentials.into());
123+
124+
let mut headers = Headers::new();
125+
authz.apply(&mut headers);
126+
127+
let authz = Authorization::from_headers(headers)?.unwrap();
128+
129+
assert_eq!(authz.scheme(), AuthenticationScheme::Basic);
130+
assert_eq!(authz.credentials(), credentials);
131+
Ok(())
132+
}
133+
134+
#[test]
135+
fn bad_request_on_parse_error() -> crate::Result<()> {
136+
let mut headers = Headers::new();
137+
headers.insert(AUTHORIZATION, "<nori ate the tag. yum.>");
138+
let err = Authorization::from_headers(headers).unwrap_err();
139+
assert_eq!(err.status(), 400);
140+
Ok(())
141+
}
142+
}

src/auth/basic_auth.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use crate::auth::{AuthenticationScheme, Authorization};
2+
use crate::headers::{HeaderName, HeaderValue, Headers, AUTHORIZATION};
3+
use crate::Status;
4+
use crate::{bail_status as bail, ensure_status as ensure};
5+
6+
/// HTTP Basic authorization.
7+
///
8+
/// # Specifications
9+
///
10+
/// - [RFC7617](https://tools.ietf.org/html/rfc7617)
11+
///
12+
/// # Examples
13+
///
14+
/// ```
15+
/// # fn main() -> http_types::Result<()> {
16+
/// #
17+
/// use http_types::Response;
18+
/// use http_types::auth::{AuthenticationScheme, BasicAuth};
19+
///
20+
/// let username = "nori";
21+
/// let password = "secret_fish!!";
22+
/// let authz = BasicAuth::new(username, password);
23+
///
24+
/// let mut res = Response::new(200);
25+
/// authz.apply(&mut res);
26+
///
27+
/// let authz = BasicAuth::from_headers(res)?.unwrap();
28+
///
29+
/// assert_eq!(authz.username(), username);
30+
/// assert_eq!(authz.password(), password);
31+
/// #
32+
/// # Ok(()) }
33+
/// ```
34+
#[derive(Debug)]
35+
pub struct BasicAuth {
36+
username: String,
37+
password: String,
38+
}
39+
40+
impl BasicAuth {
41+
/// Create a new instance of `BasicAuth`.
42+
pub fn new<U, P>(username: U, password: P) -> Self
43+
where
44+
U: AsRef<str>,
45+
P: AsRef<str>,
46+
{
47+
let username = username.as_ref().to_owned();
48+
let password = password.as_ref().to_owned();
49+
Self { username, password }
50+
}
51+
52+
/// Create a new instance from headers.
53+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
54+
let auth = match Authorization::from_headers(headers)? {
55+
Some(auth) => auth,
56+
None => return Ok(None),
57+
};
58+
59+
let scheme = auth.scheme();
60+
ensure!(
61+
matches!(scheme, AuthenticationScheme::Basic),
62+
400,
63+
"Expected basic auth scheme found `{}`",
64+
scheme
65+
);
66+
67+
let bytes = base64::decode(auth.credentials()).status(400)?;
68+
let credentials = String::from_utf8(bytes).status(400)?;
69+
70+
let mut iter = credentials.splitn(2, ':');
71+
let username = iter.next();
72+
let password = iter.next();
73+
74+
let (username, password) = match (username, password) {
75+
(Some(username), Some(password)) => (username.to_string(), password.to_string()),
76+
(Some(_), None) => bail!(400, "Expected basic auth to contain a password"),
77+
(None, _) => bail!(400, "Expected basic auth to contain a username"),
78+
};
79+
80+
Ok(Some(Self { username, password }))
81+
}
82+
83+
/// Sets the header.
84+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
85+
headers.as_mut().insert(self.name(), self.value());
86+
}
87+
88+
/// Get the `HeaderName`.
89+
pub fn name(&self) -> HeaderName {
90+
AUTHORIZATION
91+
}
92+
93+
/// Get the `HeaderValue`.
94+
pub fn value(&self) -> HeaderValue {
95+
let scheme = AuthenticationScheme::Basic;
96+
let credentials = base64::encode(format!("{}:{}", self.username, self.password));
97+
let auth = Authorization::new(scheme, credentials);
98+
auth.value()
99+
}
100+
101+
/// Get the username.
102+
pub fn username(&self) -> &str {
103+
self.username.as_str()
104+
}
105+
106+
/// Get the password.
107+
pub fn password(&self) -> &str {
108+
self.password.as_str()
109+
}
110+
}
111+
112+
#[cfg(test)]
113+
mod test {
114+
use super::*;
115+
use crate::headers::Headers;
116+
117+
#[test]
118+
fn smoke() -> crate::Result<()> {
119+
let username = "nori";
120+
let password = "secret_fish!!";
121+
let authz = BasicAuth::new(username, password);
122+
123+
let mut headers = Headers::new();
124+
authz.apply(&mut headers);
125+
126+
let authz = BasicAuth::from_headers(headers)?.unwrap();
127+
128+
assert_eq!(authz.username(), username);
129+
assert_eq!(authz.password(), password);
130+
Ok(())
131+
}
132+
133+
#[test]
134+
fn bad_request_on_parse_error() -> crate::Result<()> {
135+
let mut headers = Headers::new();
136+
headers.insert(AUTHORIZATION, "<nori ate the tag. yum.>");
137+
let err = BasicAuth::from_headers(headers).unwrap_err();
138+
assert_eq!(err.status(), 400);
139+
Ok(())
140+
}
141+
}

src/auth/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//! HTTP authentication and authorization.
2+
//!
3+
//! # Examples
4+
//!
5+
//! ```
6+
//! # fn main() -> http_types::Result<()> {
7+
//! #
8+
//! use http_types::Response;
9+
//! use http_types::auth::{AuthenticationScheme, BasicAuth};
10+
//!
11+
//! let username = "nori";
12+
//! let password = "secret_fish!!";
13+
//! let authz = BasicAuth::new(username, password);
14+
//!
15+
//! let mut res = Response::new(200);
16+
//! authz.apply(&mut res);
17+
//!
18+
//! let authz = BasicAuth::from_headers(res)?.unwrap();
19+
//!
20+
//! assert_eq!(authz.username(), username);
21+
//! assert_eq!(authz.password(), password);
22+
//! #
23+
//! # Ok(()) }
24+
//! ```
25+
26+
mod authentication_scheme;
27+
mod authorization;
28+
mod basic_auth;
29+
mod www_authenticate;
30+
31+
pub use authentication_scheme::AuthenticationScheme;
32+
pub use authorization::Authorization;
33+
pub use basic_auth::BasicAuth;
34+
pub use www_authenticate::WwwAuthenticate;

0 commit comments

Comments
 (0)