http_authentication/schemes/bearer/
challenge.rs1use alloc::{boxed::Box, string::String, vec};
2
3use http_auth::ChallengeRef;
4
5use crate::{
6 schemes::NAME_BEARER as NAME, CHALLENGE_PARAM_REALM as PARAM_REALM, COMMA, D_Q_M, EQ_S, SP,
7};
8
9const PARAM_SCOPE: &str = "scope";
11const PARAM_ERROR: &str = "error";
12const PARAM_ERROR_DESCRIPTION: &str = "error_description";
13const PARAM_ERROR_URI: &str = "error_uri";
14
15#[derive(Debug, Clone)]
17pub struct Challenge {
18 pub realm: Box<str>,
19 pub scope: Option<Box<str>>,
20 pub error: Option<Box<str>>,
21 pub error_description: Option<Box<str>>,
22 pub error_uri: Option<Box<str>>,
23}
24
25impl Challenge {
26 pub fn new(realm: impl AsRef<str>) -> Self {
27 Self {
28 realm: realm.as_ref().into(),
29 scope: None,
30 error: None,
31 error_description: None,
32 error_uri: None,
33 }
34 }
35
36 fn internal_to_string(&self) -> String {
37 let mut s = String::with_capacity(30);
38 s.push_str(NAME);
39 s.push(SP);
40
41 s.push_str(PARAM_REALM);
42 s.push(EQ_S);
43 s.push(D_Q_M);
44 s.push_str(self.realm.as_ref());
45 s.push(D_Q_M);
46
47 let mut params = vec![];
48 if let Some(scope) = &self.scope {
49 params.push((PARAM_SCOPE, true, scope));
50 }
51 if let Some(error) = &self.error {
52 params.push((PARAM_ERROR, true, error));
53 }
54 if let Some(error_description) = &self.error_description {
55 params.push((PARAM_ERROR_DESCRIPTION, true, error_description));
56 }
57 if let Some(error_uri) = &self.error_uri {
58 params.push((PARAM_ERROR_URI, true, error_uri));
59 }
60
61 for (k, is_quoted, v) in params {
62 s.push(COMMA);
63 s.push(SP);
64 s.push_str(k);
65 s.push(EQ_S);
66 if is_quoted {
67 s.push(D_Q_M);
68 }
69 s.push_str(v.as_ref());
70 if is_quoted {
71 s.push(D_Q_M);
72 }
73 }
74
75 s
76 }
77}
78
79impl TryFrom<&ChallengeRef<'_>> for Challenge {
80 type Error = ChallengeParseError;
81
82 fn try_from(c: &ChallengeRef<'_>) -> Result<Self, Self::Error> {
83 if !c.scheme.eq_ignore_ascii_case(NAME) {
84 return Err(ChallengeParseError::SchemeMismatch);
85 }
86
87 let realm = c
88 .params
89 .iter()
90 .find(|(k, _)| k.eq_ignore_ascii_case(PARAM_REALM))
91 .map(|(_, v)| v.as_escaped().into());
92 let realm = realm.unwrap_or_default();
97
98 let scope = c
99 .params
100 .iter()
101 .find(|(k, _)| k.eq_ignore_ascii_case(PARAM_SCOPE))
102 .map(|(_, v)| v.as_escaped().into());
103 let error = c
104 .params
105 .iter()
106 .find(|(k, _)| k.eq_ignore_ascii_case(PARAM_ERROR))
107 .map(|(_, v)| v.as_escaped().into());
108 let error_description = c
109 .params
110 .iter()
111 .find(|(k, _)| k.eq_ignore_ascii_case(PARAM_ERROR_DESCRIPTION))
112 .map(|(_, v)| v.as_escaped().into());
113 let error_uri = c
114 .params
115 .iter()
116 .find(|(k, _)| k.eq_ignore_ascii_case(PARAM_ERROR_URI))
117 .map(|(_, v)| v.as_escaped().into());
118
119 Ok(Self {
120 realm,
121 scope,
122 error,
123 error_description,
124 error_uri,
125 })
126 }
127}
128
129#[derive(Debug)]
131pub enum ChallengeParseError {
132 SchemeMismatch,
133 Other(&'static str),
134}
135
136impl core::fmt::Display for ChallengeParseError {
137 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
138 write!(f, "{self:?}")
139 }
140}
141
142#[cfg(feature = "std")]
143impl std::error::Error for ChallengeParseError {}
144
145impl core::fmt::Display for Challenge {
147 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148 write!(f, "{}", self.internal_to_string())
149 }
150}
151
152#[cfg(test)]
156pub(crate) const DEMO_CHALLENGE_STR_SIMPLE: &str = r#"Bearer realm="example""#;
157#[cfg(test)]
158pub(crate) const DEMO_CHALLENGE_STR: &str = r#"Bearer realm="example", error="invalid_token", error_description="The access token expired""#;
159#[cfg(test)]
160pub(crate) const DEMO_CHALLENGE_REALM_STR: &str = "example";
161#[cfg(test)]
162pub(crate) const DEMO_CHALLENGE_ERROR_STR: &str = "invalid_token";
163#[cfg(test)]
164pub(crate) const DEMO_CHALLENGE_ERROR_DESCRIPTION_STR: &str = "The access token expired";
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 use alloc::string::ToString as _;
171
172 use http_auth::ParamValue;
173
174 #[test]
175 fn test_try_from_challenge_ref() {
176 let mut c = ChallengeRef::new(NAME);
177 c.params
178 .push((PARAM_REALM, ParamValue::try_from_escaped("foo").unwrap()));
179 c.params.push((
180 PARAM_SCOPE,
181 ParamValue::try_from_escaped("openid profile email").unwrap(),
182 ));
183 c.params.push((
184 PARAM_ERROR,
185 ParamValue::try_from_escaped("invalid_token").unwrap(),
186 ));
187 c.params.push((
188 PARAM_ERROR_DESCRIPTION,
189 ParamValue::try_from_escaped("The access token expired").unwrap(),
190 ));
191 c.params.push((
192 PARAM_ERROR_URI,
193 ParamValue::try_from_escaped("https://example.com").unwrap(),
194 ));
195
196 let c = Challenge::try_from(&c).unwrap();
197 assert_eq!(c.realm, "foo".into());
198 assert_eq!(c.scope, Some("openid profile email".into()));
199 assert_eq!(c.error, Some("invalid_token".into()));
200 assert_eq!(c.error_description, Some("The access token expired".into()));
201 assert_eq!(c.error_uri, Some("https://example.com".into()));
202 }
203
204 #[test]
205 fn test_render() {
206 let mut c = Challenge::new(DEMO_CHALLENGE_REALM_STR);
207 assert_eq!(c.to_string(), DEMO_CHALLENGE_STR_SIMPLE);
208
209 c.error = Some(DEMO_CHALLENGE_ERROR_STR.into());
210 c.error_description = Some(DEMO_CHALLENGE_ERROR_DESCRIPTION_STR.into());
211 assert_eq!(c.to_string(), DEMO_CHALLENGE_STR);
212 }
213}