io_http/rfc6750/
bearer.rs1use core::fmt;
20
21use alloc::{format, string::String};
22
23use secrecy::{ExposeSecret, SecretString};
24use thiserror::Error;
25
26#[derive(Debug, Error)]
28pub enum HttpAuthBearerError {
29 #[error("Missing `Bearer ` prefix in Authorization value")]
30 MissingPrefix,
31}
32
33#[derive(Clone)]
35pub struct HttpAuthBearer(SecretString);
36
37impl HttpAuthBearer {
38 pub fn new(token: impl Into<String>) -> Self {
40 Self(SecretString::from(token.into()))
41 }
42
43 pub fn to_authorization(&self) -> String {
45 format!("Bearer {}", self.0.expose_secret())
46 }
47
48 pub fn from_authorization(value: &str) -> Result<Self, HttpAuthBearerError> {
50 value
51 .strip_prefix("Bearer ")
52 .ok_or(HttpAuthBearerError::MissingPrefix)
53 .map(|token| Self(SecretString::from(String::from(token))))
54 }
55}
56
57impl ExposeSecret<str> for HttpAuthBearer {
58 fn expose_secret(&self) -> &str {
59 self.0.expose_secret()
60 }
61}
62
63impl fmt::Debug for HttpAuthBearer {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.debug_tuple("HttpAuthBearer")
66 .field(&"[REDACTED]")
67 .finish()
68 }
69}
70
71impl fmt::Display for HttpAuthBearer {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 f.write_str(self.0.expose_secret())
74 }
75}
76
77impl PartialEq for HttpAuthBearer {
78 fn eq(&self, other: &Self) -> bool {
79 self.0.expose_secret() == other.0.expose_secret()
80 }
81}
82
83impl Eq for HttpAuthBearer {}
84
85#[cfg(test)]
86mod tests {
87 use alloc::{format, string::ToString};
88
89 use secrecy::ExposeSecret;
90
91 use crate::rfc6750::bearer::*;
92
93 #[test]
94 fn to_authorization_rfc_example() {
95 let token = HttpAuthBearer::new("mF_9.B5f-4.1JqM");
96 assert_eq!(token.to_authorization(), "Bearer mF_9.B5f-4.1JqM");
97 }
98
99 #[test]
100 fn to_authorization_has_bearer_prefix() {
101 let token = HttpAuthBearer::new("sometoken");
102 assert!(token.to_authorization().starts_with("Bearer "));
103 }
104
105 #[test]
106 fn from_authorization_roundtrip() {
107 let original = HttpAuthBearer::new("eyJhbGciOiJSUzI1NiJ9.example");
108 let header = original.to_authorization();
109 let parsed = HttpAuthBearer::from_authorization(&header).unwrap();
110 assert_eq!(parsed, original);
111 }
112
113 #[test]
114 fn from_authorization_missing_prefix() {
115 assert!(matches!(
116 HttpAuthBearer::from_authorization("Basic dXNlcjpwYXNz"),
117 Err(HttpAuthBearerError::MissingPrefix)
118 ));
119 }
120
121 #[test]
122 fn from_authorization_jwt_shaped_token() {
123 let value = "Bearer header.payload.signature";
124 let token = HttpAuthBearer::from_authorization(value).unwrap();
125 assert_eq!(token.expose_secret(), "header.payload.signature");
126 }
127
128 #[test]
129 fn display_yields_token_string() {
130 let token = HttpAuthBearer::new("abc123");
131 assert_eq!(token.to_string(), "abc123");
132 }
133
134 #[test]
135 fn debug_redacts_token() {
136 let token = HttpAuthBearer::new("super-secret-token");
137 let debug = format!("{token:?}");
138 assert!(
139 !debug.contains("super-secret-token"),
140 "token must not appear in debug"
141 );
142 assert!(debug.contains("[REDACTED]"));
143 }
144}