Skip to main content

io_http/rfc6750/
bearer.rs

1//! OAuth 2.0 Bearer token usage: tokens are transmitted as-is in the
2//! `Authorization` request header ([RFC 6750 §2.1]).
3//!
4//! # Example
5//!
6//! ```rust
7//! use io_http::rfc6750::bearer::HttpAuthBearer;
8//! use secrecy::ExposeSecret;
9//!
10//! let token = HttpAuthBearer::new("mF_9.B5f-4.1JqM");
11//! assert_eq!(token.to_authorization(), "Bearer mF_9.B5f-4.1JqM");
12//!
13//! let parsed = HttpAuthBearer::from_authorization("Bearer mF_9.B5f-4.1JqM").unwrap();
14//! assert_eq!(parsed.expose_secret(), "mF_9.B5f-4.1JqM");
15//! ```
16//!
17//! [RFC 6750 §2.1]: https://www.rfc-editor.org/rfc/rfc6750#section-2.1
18
19use core::fmt;
20
21use alloc::{format, string::String};
22
23use secrecy::{ExposeSecret, SecretString};
24use thiserror::Error;
25
26/// Failure causes when parsing a `Bearer` authorization value.
27#[derive(Debug, Error)]
28pub enum HttpAuthBearerError {
29    /// The value does not start with the bearer scheme prefix.
30    #[error("Missing `Bearer ` prefix in Authorization value")]
31    MissingPrefix,
32}
33
34/// OAuth 2.0 Bearer token; redacted in [`fmt::Debug`], zeroed on drop.
35#[derive(Clone)]
36pub struct HttpAuthBearer(SecretString);
37
38impl HttpAuthBearer {
39    /// Wraps a token string.
40    pub fn new(token: impl Into<String>) -> Self {
41        Self(SecretString::from(token.into()))
42    }
43
44    /// Returns the `Bearer <token>` header value.
45    pub fn to_authorization(&self) -> String {
46        format!("Bearer {}", self.0.expose_secret())
47    }
48
49    /// Parses a `Bearer <token>` header value.
50    pub fn from_authorization(value: &str) -> Result<Self, HttpAuthBearerError> {
51        value
52            .strip_prefix("Bearer ")
53            .ok_or(HttpAuthBearerError::MissingPrefix)
54            .map(|token| Self(SecretString::from(String::from(token))))
55    }
56}
57
58impl ExposeSecret<str> for HttpAuthBearer {
59    fn expose_secret(&self) -> &str {
60        self.0.expose_secret()
61    }
62}
63
64impl fmt::Debug for HttpAuthBearer {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.debug_tuple("HttpAuthBearer")
67            .field(&"[REDACTED]")
68            .finish()
69    }
70}
71
72impl fmt::Display for HttpAuthBearer {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(self.0.expose_secret())
75    }
76}
77
78impl PartialEq for HttpAuthBearer {
79    fn eq(&self, other: &Self) -> bool {
80        self.0.expose_secret() == other.0.expose_secret()
81    }
82}
83
84impl Eq for HttpAuthBearer {}
85
86#[cfg(test)]
87mod tests {
88    use alloc::{format, string::ToString};
89
90    use secrecy::ExposeSecret;
91
92    use crate::rfc6750::bearer::*;
93
94    #[test]
95    fn to_authorization_rfc_example() {
96        let token = HttpAuthBearer::new("mF_9.B5f-4.1JqM");
97        assert_eq!(token.to_authorization(), "Bearer mF_9.B5f-4.1JqM");
98    }
99
100    #[test]
101    fn to_authorization_has_bearer_prefix() {
102        let token = HttpAuthBearer::new("sometoken");
103        assert!(token.to_authorization().starts_with("Bearer "));
104    }
105
106    #[test]
107    fn from_authorization_roundtrip() {
108        let original = HttpAuthBearer::new("eyJhbGciOiJSUzI1NiJ9.example");
109        let header = original.to_authorization();
110        let parsed = HttpAuthBearer::from_authorization(&header).unwrap();
111        assert_eq!(parsed, original);
112    }
113
114    #[test]
115    fn from_authorization_missing_prefix() {
116        assert!(matches!(
117            HttpAuthBearer::from_authorization("Basic dXNlcjpwYXNz"),
118            Err(HttpAuthBearerError::MissingPrefix)
119        ));
120    }
121
122    #[test]
123    fn from_authorization_jwt_shaped_token() {
124        let value = "Bearer header.payload.signature";
125        let token = HttpAuthBearer::from_authorization(value).unwrap();
126        assert_eq!(token.expose_secret(), "header.payload.signature");
127    }
128
129    #[test]
130    fn display_yields_token_string() {
131        let token = HttpAuthBearer::new("abc123");
132        assert_eq!(token.to_string(), "abc123");
133    }
134
135    #[test]
136    fn debug_redacts_token() {
137        let token = HttpAuthBearer::new("super-secret-token");
138        let debug = format!("{token:?}");
139        assert!(
140            !debug.contains("super-secret-token"),
141            "token must not appear in debug"
142        );
143        assert!(debug.contains("[REDACTED]"));
144    }
145}