http_authentication/schemes/bearer/
challenge.rs

1use 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
9//
10const 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//
16#[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        //
93        // TODO, Optional
94        // Ref https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#basic
95        //
96        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//
130#[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
145//
146impl 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//
153//
154//
155#[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}