http_types/auth/
www_authenticate.rs

1use crate::auth::AuthenticationScheme;
2use crate::bail_status as bail;
3use crate::headers::{HeaderName, HeaderValue, Headers, WWW_AUTHENTICATE};
4
5/// Define the authentication method that should be used to gain access to a
6/// resource.
7///
8/// # Specifications
9///
10/// - [RFC 7235, section 4.1: WWW-Authenticate](https://tools.ietf.org/html/rfc7235#section-4.1)
11///
12/// # Implementation Notes
13///
14/// This implementation only encodes and parses a single authentication method,
15/// further authorization methods are ignored. It also always passes the utf-8 encoding flag.
16///
17/// # Examples
18///
19/// ```
20/// # fn main() -> http_types::Result<()> {
21/// #
22/// use http_types::Response;
23/// use http_types::auth::{AuthenticationScheme, WwwAuthenticate};
24///
25/// let scheme = AuthenticationScheme::Basic;
26/// let realm = "Access to the staging site";
27/// let authz = WwwAuthenticate::new(scheme, realm.into());
28///
29/// let mut res = Response::new(200);
30/// authz.apply(&mut res);
31///
32/// let authz = WwwAuthenticate::from_headers(res)?.unwrap();
33///
34/// assert_eq!(authz.scheme(), AuthenticationScheme::Basic);
35/// assert_eq!(authz.realm(), realm);
36/// #
37/// # Ok(()) }
38/// ```
39#[derive(Debug)]
40pub struct WwwAuthenticate {
41    scheme: AuthenticationScheme,
42    realm: String,
43}
44
45impl WwwAuthenticate {
46    /// Create a new instance of `WwwAuthenticate`.
47    pub fn new(scheme: AuthenticationScheme, realm: String) -> Self {
48        Self { scheme, realm }
49    }
50
51    /// Create a new instance from headers.
52    pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
53        let headers = match headers.as_ref().get(WWW_AUTHENTICATE) {
54            Some(headers) => headers,
55            None => return Ok(None),
56        };
57
58        // If we successfully parsed the header then there's always at least one
59        // entry. We want the last entry.
60        let value = headers.iter().last().unwrap();
61
62        let mut iter = value.as_str().splitn(2, ' ');
63        let scheme = iter.next();
64        let credential = iter.next();
65        let (scheme, realm) = match (scheme, credential) {
66            (None, _) => bail!(400, "Could not find scheme"),
67            (Some(_), None) => bail!(400, "Could not find realm"),
68            (Some(scheme), Some(realm)) => (scheme.parse()?, realm.to_owned()),
69        };
70
71        let realm = realm.trim_start();
72        let realm = match realm.strip_prefix(r#"realm=""#) {
73            Some(realm) => realm,
74            None => bail!(400, "realm not found"),
75        };
76
77        let mut chars = realm.chars();
78        let mut closing_quote = false;
79        let realm = (&mut chars)
80            .take_while(|c| {
81                if c == &'"' {
82                    closing_quote = true;
83                    false
84                } else {
85                    true
86                }
87            })
88            .collect();
89        if !closing_quote {
90            bail!(400, r"Expected a closing quote");
91        }
92
93        Ok(Some(Self { scheme, realm }))
94    }
95
96    /// Sets the header.
97    pub fn apply(&self, mut headers: impl AsMut<Headers>) {
98        headers.as_mut().insert(self.name(), self.value());
99    }
100
101    /// Get the `HeaderName`.
102    pub fn name(&self) -> HeaderName {
103        WWW_AUTHENTICATE
104    }
105
106    /// Get the `HeaderValue`.
107    pub fn value(&self) -> HeaderValue {
108        let output = format!(r#"{} realm="{}", charset="UTF-8""#, self.scheme, self.realm);
109
110        // SAFETY: the internal string is validated to be ASCII.
111        unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
112    }
113
114    /// Get the authorization scheme.
115    pub fn scheme(&self) -> AuthenticationScheme {
116        self.scheme
117    }
118
119    /// Set the authorization scheme.
120    pub fn set_scheme(&mut self, scheme: AuthenticationScheme) {
121        self.scheme = scheme;
122    }
123
124    /// Get the authorization realm.
125    pub fn realm(&self) -> &str {
126        self.realm.as_str()
127    }
128
129    /// Set the authorization realm.
130    pub fn set_realm(&mut self, realm: String) {
131        self.realm = realm;
132    }
133}
134
135#[cfg(test)]
136mod test {
137    use super::*;
138    use crate::headers::Headers;
139
140    #[test]
141    fn smoke() -> crate::Result<()> {
142        let scheme = AuthenticationScheme::Basic;
143        let realm = "Access to the staging site";
144        let authz = WwwAuthenticate::new(scheme, realm.into());
145
146        let mut headers = Headers::new();
147        authz.apply(&mut headers);
148
149        assert_eq!(
150            headers["WWW-Authenticate"],
151            r#"Basic realm="Access to the staging site", charset="UTF-8""#
152        );
153
154        let authz = WwwAuthenticate::from_headers(headers)?.unwrap();
155
156        assert_eq!(authz.scheme(), AuthenticationScheme::Basic);
157        assert_eq!(authz.realm(), realm);
158        Ok(())
159    }
160
161    #[test]
162    fn bad_request_on_parse_error() {
163        let mut headers = Headers::new();
164        headers.insert(WWW_AUTHENTICATE, "<nori ate the tag. yum.>");
165        let err = WwwAuthenticate::from_headers(headers).unwrap_err();
166        assert_eq!(err.status(), 400);
167    }
168}