Skip to main content

aauth_protocol/
requirement.rs

1//! The `AAuth-Requirement` response header (AAuth §Requirement Responses):
2//! what a resource or server needs before it will answer.
3
4use crate::sf::{self, Member};
5
6/// The header's name, lowercase as the `http` crate wants it.
7pub const HEADER: &str = "aauth-requirement";
8
9/// What a `401` (or a Person Server's `202`) asks for.
10///
11/// ```
12/// # use aauth_protocol::Requirement;
13/// let header = r#"requirement=auth-token;resource-token="eyJ.eyJ.sig""#;
14/// assert_eq!(
15///     Requirement::parse(header),
16///     Some(Requirement::AuthToken { resource_token: "eyJ.eyJ.sig".into() })
17/// );
18/// assert_eq!(Requirement::Approval.to_header(), "requirement=approval");
19/// ```
20#[derive(Clone, Debug, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum Requirement {
23    /// Present an agent token.
24    AgentToken,
25    /// Present a person token from the agent's Person Server.
26    PersonToken,
27    /// Exchange this resource token at the Person Server for an auth token.
28    AuthToken {
29        /// The resource token to exchange.
30        resource_token: String,
31    },
32    /// A human is deciding; poll the response's `Location`.
33    Approval,
34    /// Send the person to `url` with `code`.
35    Interaction {
36        /// Where the person goes, when the server names one.
37        url: Option<String>,
38        /// The code the person enters or carries there.
39        code: String,
40    },
41    /// The server needs the agent to clarify its request.
42    Clarification,
43    /// The server needs claims about the person.
44    Claims,
45}
46
47impl Requirement {
48    /// Parse a header value; `None` for one that is malformed, unknown, or
49    /// missing the parameter its requirement needs.
50    pub fn parse(raw: &str) -> Option<Self> {
51        let dict = sf::parse_dictionary(raw.trim()).ok()?;
52        let (_, Member::Item(item)) = dict.iter().find(|(k, _)| k == "requirement")? else {
53            return None;
54        };
55        let param = |name: &str| {
56            item.params
57                .iter()
58                .find(|(k, _)| k == name)
59                .and_then(|(_, v)| v.as_text())
60                .map(str::to_string)
61        };
62        Some(match item.bare.as_text()? {
63            "agent-token" => Self::AgentToken,
64            "person-token" => Self::PersonToken,
65            "auth-token" => Self::AuthToken {
66                resource_token: param("resource-token")?,
67            },
68            "approval" => Self::Approval,
69            "interaction" => Self::Interaction {
70                url: param("url"),
71                code: param("code")?,
72            },
73            "clarification" => Self::Clarification,
74            "claims" => Self::Claims,
75            _ => return None,
76        })
77    }
78
79    /// The requirement's registered value, such as `auth-token`.
80    pub fn name(&self) -> &'static str {
81        match self {
82            Self::AgentToken => "agent-token",
83            Self::PersonToken => "person-token",
84            Self::AuthToken { .. } => "auth-token",
85            Self::Approval => "approval",
86            Self::Interaction { .. } => "interaction",
87            Self::Clarification => "clarification",
88            Self::Claims => "claims",
89        }
90    }
91
92    /// The header value.
93    pub fn to_header(&self) -> String {
94        let mut out = format!("requirement={}", self.name());
95        match self {
96            Self::AuthToken { resource_token } => {
97                out.push_str(&format!(";resource-token={}", sf::quote(resource_token)));
98            }
99            Self::Interaction { url, code } => {
100                if let Some(url) = url {
101                    out.push_str(&format!(";url={}", sf::quote(url)));
102                }
103                out.push_str(&format!(";code={}", sf::quote(code)));
104            }
105            _ => {}
106        }
107        out
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn requirements_parse_from_the_header() {
117        assert_eq!(
118            Requirement::parse(r#"requirement=auth-token; resource-token="a.b.c""#),
119            Some(Requirement::AuthToken {
120                resource_token: "a.b.c".into()
121            })
122        );
123        assert_eq!(
124            Requirement::parse("requirement=person-token"),
125            Some(Requirement::PersonToken)
126        );
127        assert_eq!(Requirement::parse("requirement=auth-token"), None);
128        assert_eq!(Requirement::parse("requirement=dance"), None);
129        assert_eq!(Requirement::parse("garbage(("), None);
130    }
131
132    #[test]
133    fn a_built_header_parses_back() {
134        for requirement in [
135            Requirement::AuthToken {
136                resource_token: "a.b.c".into(),
137            },
138            Requirement::Interaction {
139                url: Some("https://r.example/i".into()),
140                code: "ABCD-1234".into(),
141            },
142            Requirement::Approval,
143        ] {
144            assert_eq!(
145                Requirement::parse(&requirement.to_header()),
146                Some(requirement)
147            );
148        }
149    }
150}