Skip to main content

aauth_protocol/
mission.rs

1//! Missions (AAuth §Mission): what a person approved an agent to do. A
2//! mission is named everywhere afterwards by its `s256`, the hash of its
3//! exact bytes, so nobody can change what was approved without changing its
4//! name.
5
6use base64::engine::general_purpose::URL_SAFE_NO_PAD;
7use base64::Engine;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// A mission as a Person Server approved it, checked against its hash.
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13pub struct ApprovedMission {
14    /// `BASE64URL(SHA-256(mission bytes))`: the `mission_s256` later tokens carry.
15    pub s256: String,
16    /// The decoded mission document.
17    pub mission: Value,
18}
19
20impl ApprovedMission {
21    /// The mission an approval body carries (`s256`, and `mission` as
22    /// base64url bytes), when its bytes hash to the `s256` it names.
23    ///
24    /// ```
25    /// # use aauth_protocol::mission::ApprovedMission;
26    /// use base64::Engine;
27    /// let bytes = br#"{"description":"Sydney","approved_resources":["https://api.booking.example"]}"#;
28    /// let body = serde_json::json!({
29    ///     "s256": aauth_protocol::s256(bytes),
30    ///     "mission": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes),
31    /// });
32    /// let mission = ApprovedMission::from_approval(&body).unwrap();
33    /// assert_eq!(mission.approved_resources(), ["https://api.booking.example"]);
34    /// ```
35    pub fn from_approval(body: &Value) -> Option<Self> {
36        let s256 = body.get("s256")?.as_str()?;
37        let bytes = URL_SAFE_NO_PAD
38            .decode(body.get("mission")?.as_str()?.trim_end_matches('='))
39            .ok()?;
40        (crate::s256(&bytes) == s256).then(|| Self {
41            s256: s256.to_string(),
42            mission: serde_json::from_slice(&bytes).unwrap_or(Value::Null),
43        })
44    }
45
46    /// The resources the mission was approved for.
47    pub fn approved_resources(&self) -> Vec<String> {
48        self.mission
49            .get("approved_resources")
50            .and_then(Value::as_array)
51            .map(|list| {
52                list.iter()
53                    .filter_map(|r| r.as_str().map(str::to_string))
54                    .collect()
55            })
56            .unwrap_or_default()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use serde_json::json;
64
65    #[test]
66    fn a_mission_whose_bytes_do_not_match_its_hash_is_refused() {
67        let bytes = br#"{"agent":"a"}"#;
68        let granted =
69            json!({ "s256": crate::s256(bytes), "mission": URL_SAFE_NO_PAD.encode(bytes) });
70        assert!(ApprovedMission::from_approval(&granted).is_some());
71        let mut tampered = granted.clone();
72        tampered["s256"] = json!("not-the-hash");
73        assert!(ApprovedMission::from_approval(&tampered).is_none());
74    }
75}