Skip to main content

http_extract/
authorization.rs

1//! Raw extraction of the sensitive `Authorization` field.
2//!
3//! In addition to raw field extraction, this module can route the common Bearer
4//! and Basic schemes to borrowed credential strings. It does not validate
5//! tokens, decode Basic credentials, fully validate the Authorization grammar,
6//! or authenticate requests. Returned values are sensitive and must not be
7//! logged or echoed; extraction errors never contain them.
8//!
9//! Standards context: the HTTP authentication framework is defined by
10//! [RFC 9110 Section 11](https://www.rfc-editor.org/rfc/rfc9110.html#section-11),
11//! Bearer usage by
12//! [RFC 6750 Section 2.1](https://www.rfc-editor.org/rfc/rfc6750.html#section-2.1),
13//! and Basic by
14//! [RFC 7617 Section 2](https://www.rfc-editor.org/rfc/rfc7617.html#section-2).
15//! The helpers here only recognize a scheme and return its raw credential
16//! string; they do not implement authentication, complete RFC syntax
17//! validation, or Basic decoding.
18
19use http::{HeaderMap, Request, header::AUTHORIZATION};
20
21use crate::{Error, header::extract_single_header_text};
22
23/// The Bearer scheme.
24pub const BEARER_SCHEME: &str = "Bearer";
25
26/// The Basic scheme.
27pub const BASIC_SCHEME: &str = "Basic";
28
29/// The space character that separates the scheme from the credentials.
30pub const SCHEME_SEPARATOR: char = ' ';
31
32/// Extract the singular `Authorization` field as text.
33///
34/// A missing field returns `None`. The value is returned unchanged, including
35/// an empty value. Duplicate field lines or a non-text value return an error
36/// that does not contain the sensitive value. This function does not parse,
37/// validate, decode, or authenticate the field, and callers must not log or
38/// echo it. See the HTTP authentication framework in
39/// [RFC 9110, Section 11].
40///
41/// [RFC 9110, Section 11]: https://www.rfc-editor.org/rfc/rfc9110.html#section-11
42pub fn extract_header_authorization(headers: &HeaderMap) -> Result<Option<&str>, Error> {
43    extract_single_header_text(headers, &AUTHORIZATION)
44}
45
46/// Extract the raw `Authorization` field from a complete request.
47///
48/// This reads `request.headers()` and delegates to
49/// [`extract_header_authorization`], preserving its missing, empty, duplicate,
50/// and non-text behavior. The returned value is sensitive and must not be
51/// logged or echoed; no validation, decoding, or authentication is performed.
52pub fn extract_request_authorization<B>(request: &Request<B>) -> Result<Option<&str>, Error> {
53    extract_header_authorization(request.headers())
54}
55
56/// Extract raw Bearer credentials from the `Authorization` field.
57///
58/// Scheme matching is ASCII case-insensitive and requires at least one ASCII
59/// space. Only the first required space is consumed; any additional spaces are
60/// preserved in the returned credentials. `Bearer ` therefore produces
61/// `Some("")`. A missing field, another scheme, or a scheme without the
62/// required space returns `None`. Duplicate or non-text `Authorization` fields
63/// return an error before scheme matching. This function does not validate the
64/// token syntax or authenticate it, and the returned credential is sensitive.
65/// See Bearer usage in [RFC 6750, Section 2.1].
66///
67/// [RFC 6750, Section 2.1]: https://www.rfc-editor.org/rfc/rfc6750.html#section-2.1
68pub fn extract_header_bearer_token(headers: &HeaderMap) -> Result<Option<&str>, Error> {
69    Ok(extract_header_authorization(headers)?
70        .and_then(|value| extract_scheme_credentials(value, BEARER_SCHEME)))
71}
72
73/// Extract raw Bearer credentials from a complete request.
74///
75/// This reads `request.headers()` and delegates to
76/// [`extract_header_bearer_token`], preserving its missing, scheme-mismatch,
77/// empty-credential, and field-error behavior. No token validation or
78/// authentication is performed.
79pub fn extract_request_bearer_token<B>(request: &Request<B>) -> Result<Option<&str>, Error> {
80    extract_header_bearer_token(request.headers())
81}
82
83/// Extract raw Basic credentials from the `Authorization` field.
84///
85/// Scheme matching is ASCII case-insensitive and requires at least one ASCII
86/// space. Only the first required space is consumed; any additional spaces are
87/// preserved in the returned credentials. `Basic ` therefore produces
88/// `Some("")`. A missing field, another scheme, or a scheme without the
89/// required space returns `None`. Duplicate or non-text `Authorization` fields
90/// return an error before scheme matching. This function does not validate or
91/// Base64-decode the credential, split a user name and password, or authenticate
92/// it. The returned credential is sensitive. See [RFC 7617, Section 2].
93///
94/// [RFC 7617, Section 2]: https://www.rfc-editor.org/rfc/rfc7617.html#section-2
95pub fn extract_header_basic_credentials(headers: &HeaderMap) -> Result<Option<&str>, Error> {
96    Ok(extract_header_authorization(headers)?
97        .and_then(|value| extract_scheme_credentials(value, BASIC_SCHEME)))
98}
99
100/// Extract raw Basic credentials from a complete request.
101///
102/// This reads `request.headers()` and delegates to
103/// [`extract_header_basic_credentials`], preserving its missing,
104/// scheme-mismatch, empty-credential, and field-error behavior. No validation,
105/// decoding, or authentication is performed.
106pub fn extract_request_basic_credentials<B>(request: &Request<B>) -> Result<Option<&str>, Error> {
107    extract_header_basic_credentials(request.headers())
108}
109
110fn extract_scheme_credentials<'a>(value: &'a str, scheme: &str) -> Option<&'a str> {
111    let candidate = value.get(..scheme.len())?;
112    if !candidate.eq_ignore_ascii_case(scheme) {
113        return None;
114    }
115
116    value.get(scheme.len()..)?.strip_prefix(SCHEME_SEPARATOR)
117}
118
119#[cfg(test)]
120mod tests {
121    use http::{HeaderMap, HeaderValue, Request, header::AUTHORIZATION};
122
123    use super::*;
124
125    #[test]
126    fn extracts_raw_and_empty_authorization_values() {
127        let mut headers = HeaderMap::new();
128        assert_eq!(extract_header_authorization(&headers).unwrap(), None);
129
130        headers.insert(AUTHORIZATION, "Bearer opaque-secret".parse().unwrap());
131        assert_eq!(
132            extract_header_authorization(&headers).unwrap(),
133            Some("Bearer opaque-secret")
134        );
135
136        headers.insert(AUTHORIZATION, "".parse().unwrap());
137        assert_eq!(extract_header_authorization(&headers).unwrap(), Some(""));
138    }
139
140    #[test]
141    fn rejects_duplicate_and_non_text_values_without_echoing_them() {
142        let mut duplicate = HeaderMap::new();
143        duplicate.append(AUTHORIZATION, "Bearer first-secret".parse().unwrap());
144        duplicate.append(AUTHORIZATION, "Basic second-secret".parse().unwrap());
145        let error = extract_header_authorization(&duplicate).unwrap_err();
146        assert!(matches!(error, Error::DuplicateHeader { .. }));
147        assert!(!format!("{error:?}").contains("secret"));
148
149        let mut non_text = HeaderMap::new();
150        non_text.insert(AUTHORIZATION, HeaderValue::from_bytes(&[0xff]).unwrap());
151        assert!(matches!(
152            extract_header_authorization(&non_text),
153            Err(Error::InvalidHeader { .. })
154        ));
155    }
156
157    #[test]
158    fn routes_bearer_without_validating_or_normalizing_credentials() {
159        assert_eq!(extract_scheme_credentials("Bearer🦀", BEARER_SCHEME), None);
160        let mut headers = HeaderMap::new();
161        assert_eq!(extract_header_bearer_token(&headers).unwrap(), None);
162
163        headers.insert(AUTHORIZATION, "Basic encoded".parse().unwrap());
164        assert_eq!(extract_header_bearer_token(&headers).unwrap(), None);
165
166        headers.insert(AUTHORIZATION, "bEaReR opaque==".parse().unwrap());
167        assert_eq!(
168            extract_header_bearer_token(&headers).unwrap(),
169            Some("opaque==")
170        );
171
172        headers.insert(AUTHORIZATION, "Bearer  keep-space".parse().unwrap());
173        assert_eq!(
174            extract_header_bearer_token(&headers).unwrap(),
175            Some(" keep-space")
176        );
177
178        headers.insert(AUTHORIZATION, "Bearer ".parse().unwrap());
179        assert_eq!(extract_header_bearer_token(&headers).unwrap(), Some(""));
180
181        headers.insert(AUTHORIZATION, "Bearer".parse().unwrap());
182        assert_eq!(extract_header_bearer_token(&headers).unwrap(), None);
183
184        assert_eq!(
185            extract_scheme_credentials("Béarer token", BEARER_SCHEME),
186            None
187        );
188        assert_eq!(extract_scheme_credentials("🦀", BEARER_SCHEME), None);
189    }
190
191    #[test]
192    fn routes_basic_without_decoding_credentials() {
193        let mut headers = HeaderMap::new();
194        headers.insert(AUTHORIZATION, "Bearer token".parse().unwrap());
195        assert_eq!(extract_header_basic_credentials(&headers).unwrap(), None);
196
197        headers.insert(AUTHORIZATION, "bAsIc plain-credentials".parse().unwrap());
198        assert_eq!(
199            extract_header_basic_credentials(&headers).unwrap(),
200            Some("plain-credentials")
201        );
202
203        headers.insert(AUTHORIZATION, "Basic ".parse().unwrap());
204        assert_eq!(
205            extract_header_basic_credentials(&headers).unwrap(),
206            Some("")
207        );
208
209        headers.insert(AUTHORIZATION, "Basic  keep-space".parse().unwrap());
210        assert_eq!(
211            extract_header_basic_credentials(&headers).unwrap(),
212            Some(" keep-space")
213        );
214
215        headers.insert(AUTHORIZATION, "Basic".parse().unwrap());
216        assert_eq!(extract_header_basic_credentials(&headers).unwrap(), None);
217    }
218
219    #[test]
220    fn requires_a_space_between_scheme_and_credentials() {
221        let mut headers = HeaderMap::new();
222
223        headers.insert(AUTHORIZATION, "BearerAA".parse().unwrap());
224        assert_eq!(extract_header_bearer_token(&headers).unwrap(), None);
225
226        headers.insert(AUTHORIZATION, "Bearer AA".parse().unwrap());
227        assert_eq!(extract_header_bearer_token(&headers).unwrap(), Some("AA"));
228
229        headers.insert(AUTHORIZATION, "BasicAA".parse().unwrap());
230        assert_eq!(extract_header_basic_credentials(&headers).unwrap(), None);
231
232        headers.insert(AUTHORIZATION, "Basic AA".parse().unwrap());
233        assert_eq!(
234            extract_header_basic_credentials(&headers).unwrap(),
235            Some("AA")
236        );
237    }
238
239    #[test]
240    fn request_entry_points_delegate_to_headers() {
241        let request = Request::builder()
242            .header(AUTHORIZATION, "Bearer unparsed credentials")
243            .body(())
244            .unwrap();
245
246        assert_eq!(
247            extract_request_authorization(&request).unwrap(),
248            Some("Bearer unparsed credentials")
249        );
250        assert_eq!(
251            extract_request_bearer_token(&request).unwrap(),
252            Some("unparsed credentials")
253        );
254        assert_eq!(extract_request_basic_credentials(&request).unwrap(), None);
255
256        let basic = Request::builder()
257            .header(AUTHORIZATION, "Basic encoded")
258            .body(())
259            .unwrap();
260        assert_eq!(
261            extract_request_basic_credentials(&basic).unwrap(),
262            Some("encoded")
263        );
264    }
265}