Skip to main content

arete_auth/
audience.rs

1//! Accepted-audience policy for session verification.
2//!
3//! A verifier usually accepts exactly one audience, but it may accept several —
4//! for example while an audience is being renamed, or when one verifier serves
5//! several distinct audiences. Membership is an exact match, never a prefix,
6//! suffix, or case-insensitive one, so a token minted for one audience is never
7//! accepted for another. The matched value reaches callers through
8//! [`crate::claims::AuthContext::audience`], where it can inform authorization
9//! decisions.
10
11use std::collections::BTreeSet;
12
13/// Why an [`AudienceSet`] could not be built.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
15pub enum AudienceSetError {
16    /// A verifier that accepts nothing can never authenticate anyone, and a
17    /// verifier that accepted *everything* would accept tokens minted for any
18    /// audience at all. Refusing the empty set makes the misconfiguration loud
19    /// at construction instead of silent at request time.
20    #[error("an audience set must contain at least one audience")]
21    Empty,
22    /// An empty audience string would match a token whose `aud` claim was
23    /// omitted or blank.
24    #[error("an audience must not be blank")]
25    BlankAudience,
26}
27
28/// The set of audiences a verifier will accept.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct AudienceSet(BTreeSet<String>);
31
32impl AudienceSet {
33    /// Accept exactly one audience — the common case.
34    pub fn single(audience: impl Into<String>) -> Self {
35        let audience = audience.into();
36        Self(BTreeSet::from([audience]))
37    }
38
39    /// Accept any audience in `audiences`.
40    ///
41    /// Rejects an empty set and blank entries rather than defaulting to
42    /// something permissive.
43    pub fn new<I, S>(audiences: I) -> Result<Self, AudienceSetError>
44    where
45        I: IntoIterator<Item = S>,
46        S: Into<String>,
47    {
48        let set: BTreeSet<String> = audiences.into_iter().map(Into::into).collect();
49        if set.is_empty() {
50            return Err(AudienceSetError::Empty);
51        }
52        if set.iter().any(|audience| audience.trim().is_empty()) {
53            return Err(AudienceSetError::BlankAudience);
54        }
55        Ok(Self(set))
56    }
57
58    /// Exact membership. Deliberately not a prefix or case-insensitive test: a
59    /// loose comparison would let a token minted for one audience be accepted
60    /// for another.
61    ///
62    /// A blank audience is never accepted, whatever the set contains.
63    /// [`Self::new`] refuses blank entries, but [`Self::single`] is infallible
64    /// so the existing verifier constructors can stay infallible, which leaves
65    /// a blank value reachable by misconfiguration. Rejecting here closes that
66    /// off at the point it matters and cannot be bypassed by any constructor:
67    /// a token whose `aud` claim was omitted or empty deserializes to `""` and
68    /// must never authenticate.
69    pub fn accepts(&self, audience: &str) -> bool {
70        if audience.trim().is_empty() {
71            return false;
72        }
73        self.0.contains(audience)
74    }
75
76    /// The accepted audiences, in sorted order.
77    pub fn iter(&self) -> impl Iterator<Item = &str> {
78        self.0.iter().map(String::as_str)
79    }
80
81    /// How many audiences are accepted. Always at least one.
82    pub fn len(&self) -> usize {
83        self.0.len()
84    }
85
86    /// Always false; retained so clippy does not ask for it alongside `len`.
87    pub fn is_empty(&self) -> bool {
88        false
89    }
90
91    /// The sole accepted audience, when there is exactly one.
92    pub fn as_single(&self) -> Option<&str> {
93        if self.0.len() == 1 {
94            self.0.iter().next().map(String::as_str)
95        } else {
96            None
97        }
98    }
99}
100
101impl From<String> for AudienceSet {
102    fn from(audience: String) -> Self {
103        Self::single(audience)
104    }
105}
106
107impl From<&str> for AudienceSet {
108    fn from(audience: &str) -> Self {
109        Self::single(audience)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn a_single_audience_accepts_only_itself() {
119        let set = AudienceSet::single("deployment-31");
120        assert!(set.accepts("deployment-31"));
121        assert!(!set.accepts("deployment-32"));
122        assert_eq!(set.len(), 1);
123        assert_eq!(set.as_single(), Some("deployment-31"));
124    }
125
126    #[test]
127    fn a_set_accepts_every_member_and_nothing_else() {
128        let set = AudienceSet::new(["deployment-31", "deployment-32", "deployment-40"])
129            .expect("non-empty");
130        for member in ["deployment-31", "deployment-32", "deployment-40"] {
131            assert!(set.accepts(member), "{member} should be accepted");
132        }
133        assert!(!set.accepts("deployment-99"));
134        assert_eq!(
135            set.as_single(),
136            None,
137            "a set with several audiences has no single audience"
138        );
139    }
140
141    #[test]
142    fn membership_is_exact() {
143        // A prefix or case-insensitive match would let a token minted for one
144        // audience be accepted for another.
145        let set = AudienceSet::single("deployment-3");
146        assert!(!set.accepts("deployment-31"));
147        assert!(!set.accepts("deployment"));
148        assert!(!set.accepts("DEPLOYMENT-3"));
149        assert!(!set.accepts(" deployment-3"));
150        assert!(!set.accepts(""));
151    }
152
153    #[test]
154    fn an_empty_or_blank_set_is_refused_at_construction() {
155        assert_eq!(
156            AudienceSet::new(Vec::<String>::new()).unwrap_err(),
157            AudienceSetError::Empty
158        );
159        assert_eq!(
160            AudienceSet::new([""]).unwrap_err(),
161            AudienceSetError::BlankAudience
162        );
163        assert_eq!(
164            AudienceSet::new(["deployment-31", "   "]).unwrap_err(),
165            AudienceSetError::BlankAudience
166        );
167    }
168
169    #[test]
170    fn a_blank_audience_never_authenticates_even_if_the_set_holds_one() {
171        // `single` is infallible so the scalar verifier constructors can be,
172        // which means a misconfigured deployment can build a set around "".
173        // A signed token carrying `aud: ""` must still be refused.
174        let set = AudienceSet::single("");
175        assert!(!set.accepts(""));
176        assert!(!set.accepts("   "));
177
178        // The same holds for a set that legitimately contains other audiences.
179        let set = AudienceSet::new(["deployment-31"]).expect("non-empty");
180        assert!(!set.accepts(""));
181        assert!(set.accepts("deployment-31"));
182    }
183
184    #[test]
185    fn duplicates_collapse() {
186        let set = AudienceSet::new(["deployment-31", "deployment-31"]).expect("non-empty");
187        assert_eq!(set.len(), 1);
188    }
189}