1use std::collections::BTreeMap;
18
19use serde::{Deserialize, Deserializer, Serialize};
20
21use crate::error::{ActError, ActResult};
22use crate::scope::Scope;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct Confirmation {
32 pub key_fingerprint: String,
34 pub alg: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct Delegation {
45 pub allow_delegation: bool,
47 pub max_depth: u8,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub delegatable_scopes: Option<Vec<Scope>>,
53 pub allow_any_delegate: bool,
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub allowed_delegates: Vec<String>,
59 #[serde(default)]
62 pub min_ttl_reduction_seconds: i64,
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct ActClaims {
72 pub jti: String,
75
76 pub sub: String,
78
79 pub iss: String,
81
82 #[serde(deserialize_with = "audience_from_string_or_array")]
88 pub aud: Vec<String>,
89
90 pub iat: i64,
92
93 #[serde(default)]
95 pub nbf: i64,
96
97 pub exp: i64,
99
100 pub tenant_id: String,
102
103 pub scope: Vec<Scope>,
105
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub cnf: Option<Confirmation>,
109
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub delegation: Option<Delegation>,
113
114 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
122 pub ext: BTreeMap<String, ciborium::Value>,
123}
124
125fn audience_from_string_or_array<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
127where
128 D: Deserializer<'de>,
129{
130 #[derive(Deserialize)]
131 #[serde(untagged)]
132 enum OneOrMany {
133 One(String),
134 Many(Vec<String>),
135 }
136
137 match OneOrMany::deserialize(deserializer)? {
138 OneOrMany::One(single) => Ok(vec![single]),
139 OneOrMany::Many(many) => Ok(many),
140 }
141}
142
143impl ActClaims {
144 pub fn validate_structure(&self) -> ActResult<()> {
156 for (name, value) in [
157 ("jti", &self.jti),
158 ("sub", &self.sub),
159 ("iss", &self.iss),
160 ("tenant_id", &self.tenant_id),
161 ] {
162 if value.trim().is_empty() {
163 return Err(ActError::EmptyClaim(name));
164 }
165 }
166
167 if self.aud.is_empty() || self.aud.iter().any(|a| a.trim().is_empty()) {
168 return Err(ActError::EmptyClaim("aud"));
169 }
170
171 if self.nbf >= self.exp {
172 return Err(ActError::EmptyValidityWindow {
173 nbf: self.nbf,
174 exp: self.exp,
175 });
176 }
177
178 Ok(())
179 }
180
181 #[must_use]
186 pub fn authorizes(&self, requested: &Scope) -> bool {
187 self.scope.iter().any(|granted| granted.covers(requested))
188 }
189
190 #[must_use]
192 pub fn has_audience(&self, audience: &str) -> bool {
193 self.aud.iter().any(|a| a == audience)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn claims() -> ActClaims {
202 ActClaims {
203 jti: "018f4c1e-0000-7000-8000-000000000000".to_string(),
204 sub: "did:oas:l1fe:agent:alpha".to_string(),
205 iss: "arsenal:broker:test-1".to_string(),
206 aud: vec!["omerta".to_string()],
207 iat: 1_000,
208 nbf: 1_000,
209 exp: 2_000,
210 tenant_id: "acme".to_string(),
211 scope: vec![],
212 cnf: None,
213 delegation: None,
214 ext: BTreeMap::new(),
215 }
216 }
217
218 #[test]
219 fn rejects_blank_required_claims() {
220 for mutate in [
221 (|c: &mut ActClaims| c.jti.clear()) as fn(&mut ActClaims),
222 |c: &mut ActClaims| c.sub.clear(),
223 |c: &mut ActClaims| c.iss.clear(),
224 |c: &mut ActClaims| c.tenant_id.clear(),
225 |c: &mut ActClaims| c.aud.clear(),
226 ] {
227 let mut subject = claims();
228 mutate(&mut subject);
229 assert!(subject.validate_structure().is_err());
230 }
231 }
232
233 #[test]
234 fn rejects_whitespace_only_claims() {
235 let mut subject = claims();
238 subject.iss = " ".to_string();
239 assert!(matches!(
240 subject.validate_structure(),
241 Err(ActError::EmptyClaim("iss"))
242 ));
243 }
244
245 #[test]
246 fn rejects_empty_validity_window() {
247 let mut subject = claims();
248 subject.nbf = subject.exp;
249 assert!(matches!(
250 subject.validate_structure(),
251 Err(ActError::EmptyValidityWindow { .. })
252 ));
253 }
254
255 #[test]
256 fn accepts_audience_as_bare_string_or_array() -> ActResult<()> {
257 #[derive(Serialize)]
258 struct BareAud {
259 aud: &'static str,
260 }
261 #[derive(Deserialize)]
262 struct Probe {
263 #[serde(deserialize_with = "audience_from_string_or_array")]
264 aud: Vec<String>,
265 }
266
267 let mut buf = Vec::new();
268 ciborium::into_writer(&BareAud { aud: "omerta" }, &mut buf)
269 .map_err(|e| ActError::Encode(e.to_string()))?;
270 let probe: Probe = ciborium::from_reader(buf.as_slice())
271 .map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
272 assert_eq!(probe.aud, vec!["omerta".to_string()]);
273
274 let mut buf = Vec::new();
275 ciborium::into_writer(&BareAud { aud: "omerta" }, &mut buf)
276 .map_err(|e| ActError::Encode(e.to_string()))?;
277 assert!(!buf.is_empty());
278 Ok(())
279 }
280
281 #[test]
282 fn timestamps_encode_as_integers() -> ActResult<()> {
283 #[derive(Deserialize)]
285 struct Probe {
286 iat: ciborium::Value,
287 exp: ciborium::Value,
288 }
289
290 let mut buf = Vec::new();
291 ciborium::into_writer(&claims(), &mut buf).map_err(|e| ActError::Encode(e.to_string()))?;
292 let probe: Probe = ciborium::from_reader(buf.as_slice())
293 .map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
294
295 assert!(probe.iat.is_integer(), "iat must be an integer");
296 assert!(probe.exp.is_integer(), "exp must be an integer");
297 Ok(())
298 }
299
300 #[test]
301 fn identifiers_encode_as_text() -> ActResult<()> {
302 #[derive(Deserialize)]
303 struct Probe {
304 jti: ciborium::Value,
305 sub: ciborium::Value,
306 }
307
308 let mut buf = Vec::new();
309 ciborium::into_writer(&claims(), &mut buf).map_err(|e| ActError::Encode(e.to_string()))?;
310 let probe: Probe = ciborium::from_reader(buf.as_slice())
311 .map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
312
313 assert!(probe.jti.is_text(), "jti must be text, not UUID bytes");
314 assert!(probe.sub.is_text(), "sub must be text");
315 Ok(())
316 }
317
318 #[test]
319 fn unknown_top_level_keys_are_ignored() -> ActResult<()> {
320 let mut map = vec![
323 (
324 ciborium::Value::Text("jti".into()),
325 ciborium::Value::Text("id".into()),
326 ),
327 (
328 ciborium::Value::Text("sub".into()),
329 ciborium::Value::Text("did:oas:l1fe:agent:alpha".into()),
330 ),
331 (
332 ciborium::Value::Text("iss".into()),
333 ciborium::Value::Text("issuer".into()),
334 ),
335 (
336 ciborium::Value::Text("aud".into()),
337 ciborium::Value::Text("omerta".into()),
338 ),
339 (
340 ciborium::Value::Text("iat".into()),
341 ciborium::Value::from(1),
342 ),
343 (
344 ciborium::Value::Text("nbf".into()),
345 ciborium::Value::from(1),
346 ),
347 (
348 ciborium::Value::Text("exp".into()),
349 ciborium::Value::from(2),
350 ),
351 (
352 ciborium::Value::Text("tenant_id".into()),
353 ciborium::Value::Text("acme".into()),
354 ),
355 (
356 ciborium::Value::Text("scope".into()),
357 ciborium::Value::Array(vec![]),
358 ),
359 ];
360 map.push((
361 ciborium::Value::Text("claim_from_the_future".into()),
362 ciborium::Value::from(42),
363 ));
364
365 let mut buf = Vec::new();
366 ciborium::into_writer(&ciborium::Value::Map(map), &mut buf)
367 .map_err(|e| ActError::Encode(e.to_string()))?;
368
369 let decoded: ActClaims = ciborium::from_reader(buf.as_slice())
370 .map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
371 assert_eq!(decoded.tenant_id, "acme");
372 Ok(())
373 }
374}