1use serde_json::{json, Map, Value};
7
8use crate::discovery::KeyResolver;
9use crate::error::TokenError;
10use crate::identifier::is_server_identifier;
11use crate::jwk::{Algorithm, Jwk};
12use crate::jwt;
13use crate::keys::SigningKey;
14use crate::metadata::dwk;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18#[non_exhaustive]
19pub enum TokenKind {
20 Agent,
22 Person,
25 Auth,
27 Resource,
29}
30
31impl TokenKind {
32 pub fn typ(self) -> &'static str {
34 match self {
35 TokenKind::Agent => "aa-agent+jwt",
36 TokenKind::Person => "aa-person+jwt",
37 TokenKind::Auth => "aa-auth+jwt",
38 TokenKind::Resource => "aa-resource+jwt",
39 }
40 }
41
42 pub fn from_typ(typ: &str) -> Option<Self> {
44 [
45 TokenKind::Agent,
46 TokenKind::Person,
47 TokenKind::Auth,
48 TokenKind::Resource,
49 ]
50 .into_iter()
51 .find(|k| k.typ() == typ)
52 }
53
54 fn error_code(self) -> &'static str {
55 match self {
56 TokenKind::Agent => "invalid_agent_token",
57 TokenKind::Person => "invalid_person_token",
58 TokenKind::Auth => "invalid_auth_token",
59 TokenKind::Resource => "invalid_resource_token",
60 }
61 }
62}
63
64pub fn mint(key: &SigningKey, kind: TokenKind, claims: &Value) -> String {
84 let header = json!({ "typ": kind.typ(), "kid": key.thumbprint() });
85 jwt::encode(key, header.as_object().cloned().unwrap_or_default(), claims)
86}
87
88#[derive(Clone, Debug)]
91pub struct VerifyTokenOptions<'a> {
92 pub jwt: &'a str,
94 pub http_signature_thumbprint: &'a str,
96 pub resource: &'a str,
98 pub accept: &'a [TokenKind],
101 pub now: i64,
103 pub clock_tolerance: i64,
105}
106
107impl<'a> VerifyTokenOptions<'a> {
108 pub fn new(
110 jwt: &'a str,
111 http_signature_thumbprint: &'a str,
112 resource: &'a str,
113 accept: &'a [TokenKind],
114 ) -> Self {
115 Self {
116 jwt,
117 http_signature_thumbprint,
118 resource,
119 accept,
120 now: crate::now_unix(),
121 clock_tolerance: 60,
122 }
123 }
124}
125
126#[derive(Clone, Debug)]
129#[non_exhaustive]
130pub struct VerifiedToken {
131 pub kind: TokenKind,
133 pub iss: String,
135 pub dwk: String,
137 pub sub: String,
139 pub cnf_jwk: Jwk,
141 pub iat: i64,
143 pub exp: i64,
145 pub claims: Map<String, Value>,
147}
148
149impl VerifiedToken {
150 pub fn claim_str(&self, name: &str) -> Option<&str> {
152 self.claims
153 .get(name)
154 .and_then(Value::as_str)
155 .filter(|s| !s.is_empty())
156 }
157
158 pub fn jti(&self) -> Option<&str> {
160 self.claim_str("jti")
161 }
162
163 pub fn ps(&self) -> Option<&str> {
165 self.claim_str("ps")
166 }
167
168 pub fn mission_s256(&self) -> Option<&str> {
170 self.claim_str("mission_s256")
171 }
172
173 pub fn tenant(&self) -> Option<&str> {
175 self.claim_str("tenant")
176 }
177
178 pub fn scope(&self) -> Option<&str> {
180 self.claim_str("scope")
181 }
182}
183
184fn aud_matches(aud: Option<&Value>, resource: &str) -> bool {
185 match aud {
186 Some(Value::String(a)) => a == resource,
187 Some(Value::Array(list)) => list.iter().any(|a| a.as_str() == Some(resource)),
188 _ => false,
189 }
190}
191
192pub async fn verify_token(
203 options: &VerifyTokenOptions<'_>,
204 resolver: &dyn KeyResolver,
205) -> Result<VerifiedToken, TokenError> {
206 if options.accept.is_empty() {
207 return Err(TokenError::new(
208 "invalid_configuration",
209 "accept names no token kinds",
210 ));
211 }
212 if !is_server_identifier(options.resource) {
213 return Err(TokenError::new(
214 "invalid_configuration",
215 format!("resource {} is not a server identifier", options.resource),
216 ));
217 }
218 let decoded = jwt::decode(options.jwt)
219 .map_err(|_| TokenError::new("malformed_token", "not a well-formed JWT"))?;
220 let typ = decoded.typ().unwrap_or_default();
221 let kind = TokenKind::from_typ(typ)
222 .filter(|k| *k != TokenKind::Resource)
223 .ok_or_else(|| {
224 TokenError::new("unsupported_token_type", format!("unknown JWT typ {typ:?}"))
225 })?;
226 if !options.accept.contains(&kind) {
227 return Err(TokenError::new(
228 "token_type_not_accepted",
229 format!("a {typ} is not accepted here"),
230 ));
231 }
232 let code = kind.error_code();
233 let alg = decoded.alg().unwrap_or_default();
234 let algorithm = Algorithm::from_name(alg).ok_or_else(|| {
235 TokenError::new(
236 code,
237 format!("JWT alg {alg:?} is not a fully-specified algorithm this verifier supports"),
238 )
239 })?;
240
241 let required = |name: &str| {
242 decoded
243 .claim_str(name)
244 .map(str::to_string)
245 .ok_or_else(|| TokenError::new(code, format!("missing required claim: {name}")))
246 };
247 let iss = required("iss")?;
248 let token_dwk = required("dwk")?;
249 let number = |name: &str| {
250 decoded
251 .claims
252 .get(name)
253 .and_then(Value::as_i64)
254 .ok_or_else(|| TokenError::new(code, format!("missing required claim: {name}")))
255 };
256 let iat = number("iat")?;
257 let exp = number("exp")?;
258 let expected_dwk: &[&str] = match kind {
259 TokenKind::Agent => &[dwk::AGENT],
260 TokenKind::Person => &[dwk::PERSON],
261 _ => &[dwk::PERSON, dwk::ACCESS],
262 };
263 if !expected_dwk.contains(&token_dwk.as_str()) {
264 return Err(TokenError::new(
265 code,
266 format!("unexpected dwk {token_dwk:?}"),
267 ));
268 }
269 let cnf: Jwk = decoded
270 .claims
271 .get("cnf")
272 .and_then(|c| c.get("jwk"))
273 .cloned()
274 .and_then(|j| serde_json::from_value(j).ok())
275 .ok_or_else(|| TokenError::new(code, "missing required claim: cnf.jwk"))?;
276 cnf.check_confirmation_key()
277 .map_err(|e| TokenError::new(code, e.to_string()))?;
278 let sub = required("sub")?;
279 match kind {
280 TokenKind::Person => {
281 required("aud")?;
282 required("jti")?;
283 }
284 TokenKind::Auth => {
285 if decoded.claims.get("aud").is_none() {
286 return Err(TokenError::new(code, "missing required claim: aud"));
287 }
288 required("ps")?;
289 }
290 _ => {}
291 }
292
293 let thumbprint = cnf
294 .thumbprint()
295 .map_err(|e| TokenError::new(code, e.to_string()))?;
296 if !crate::constant_time_eq(&thumbprint, options.http_signature_thumbprint) {
297 return Err(TokenError::new(
298 "key_binding_failed",
299 "cnf.jwk thumbprint does not match the HTTP signature key",
300 ));
301 }
302
303 if !is_server_identifier(&iss) {
304 return Err(TokenError::new(
305 code,
306 format!("iss is not a server identifier: {iss}"),
307 ));
308 }
309 let jwks = resolver
310 .jwks(&iss, &token_dwk)
311 .await
312 .map_err(|e| TokenError::new(code, e.to_string()))?;
313 let key = match decoded.kid() {
314 Some(kid) if !kid.is_empty() => jwks
315 .keys
316 .iter()
317 .find(|k| k.kid.as_deref() == Some(kid))
318 .ok_or_else(|| TokenError::new(code, format!("no key {kid:?} in the issuer's JWKS")))?,
319 _ if jwks.keys.len() == 1 => &jwks.keys[0],
320 _ => {
321 return Err(TokenError::new(
322 code,
323 "the JWT names no kid and the issuer publishes more than one key",
324 ))
325 }
326 };
327 if key.alg.as_deref().is_some_and(|a| a != alg) {
328 return Err(TokenError::new(
329 code,
330 "the issuer's key alg disagrees with the JWT's",
331 ));
332 }
333 if !decoded.verify_signature(key, algorithm) {
334 return Err(TokenError::new(code, "JWT signature verification failed"));
335 }
336 if options.now >= exp {
337 return Err(TokenError::new("token_expired", "token has expired"));
338 }
339 if decoded
340 .claims
341 .get("nbf")
342 .and_then(Value::as_i64)
343 .is_some_and(|nbf| options.now < nbf)
344 {
345 return Err(TokenError::new(code, "token is not yet valid"));
346 }
347 if iat > options.now + options.clock_tolerance {
348 return Err(TokenError::new(
349 "clock_skew",
350 format!(
351 "token iat is {}s ahead of this verifier's clock",
352 iat - options.now
353 ),
354 ));
355 }
356 if matches!(kind, TokenKind::Person | TokenKind::Auth)
357 && !aud_matches(decoded.claims.get("aud"), options.resource)
358 {
359 return Err(TokenError::new(
360 "aud_mismatch",
361 "token aud is not this resource",
362 ));
363 }
364 match kind {
365 TokenKind::Agent => {
366 if let Some(ps) = decoded.claim_str("ps") {
367 if !is_server_identifier(ps) {
368 return Err(TokenError::new(
369 code,
370 format!("ps is not a server identifier: {ps}"),
371 ));
372 }
373 }
374 }
375 TokenKind::Person => {
376 if decoded.claims.contains_key("scope") || decoded.claims.contains_key("account") {
377 return Err(TokenError::new(
378 code,
379 "a person token MUST NOT carry scope or account",
380 ));
381 }
382 }
383 TokenKind::Auth => {
384 if !decoded.claim_str("ps").is_some_and(is_server_identifier) {
385 return Err(TokenError::new(code, "ps is not a server identifier"));
386 }
387 }
388 TokenKind::Resource => {}
389 }
390 Ok(VerifiedToken {
391 kind,
392 iss,
393 dwk: token_dwk,
394 sub,
395 cnf_jwk: cnf,
396 iat,
397 exp,
398 claims: decoded.claims,
399 })
400}
401
402#[cfg(test)]
403pub(crate) mod tests {
404 use super::*;
405 use crate::discovery::{JwksResolver, StaticFetch};
406 use std::sync::Arc;
407
408 pub(crate) const AP: &str = "https://agents.example";
409
410 pub(crate) fn provider(ap_key: &SigningKey) -> JwksResolver {
411 let fetch = StaticFetch::default();
412 fetch.insert(
413 format!("{AP}/.well-known/aauth-agent.json"),
414 json!({ "issuer": AP, "jwks_uri": format!("{AP}/jwks.json") }),
415 );
416 fetch.insert(
417 format!("{AP}/jwks.json"),
418 serde_json::to_value(ap_key.jwks()).unwrap(),
419 );
420 JwksResolver::new(Arc::new(fetch))
421 }
422
423 pub(crate) fn agent_token(ap_key: &SigningKey, session: &SigningKey, exp: i64) -> String {
424 mint(
425 ap_key,
426 TokenKind::Agent,
427 &json!({
428 "iss": AP, "dwk": "aauth-agent.json", "sub": "aauth:travel@agents.example",
429 "iat": 1_000, "exp": exp, "cnf": { "jwk": session.public_jwk() },
430 }),
431 )
432 }
433
434 fn options<'a>(jwt: &'a str, thumbprint: &'a str, now: i64) -> VerifyTokenOptions<'a> {
435 VerifyTokenOptions {
436 jwt,
437 http_signature_thumbprint: thumbprint,
438 resource: "https://ps.example",
439 accept: &[TokenKind::Agent],
440 now,
441 clock_tolerance: 60,
442 }
443 }
444
445 #[tokio::test]
446 async fn an_agent_token_verifies_against_its_providers_keys() {
447 let (ap, session) = (SigningKey::generate(), SigningKey::generate());
448 let token = agent_token(&ap, &session, 5_000);
449 let verified = verify_token(
450 &options(&token, session.thumbprint(), 1_000),
451 &provider(&ap),
452 )
453 .await
454 .unwrap();
455 assert_eq!(verified.sub, "aauth:travel@agents.example");
456 assert_eq!(verified.kind, TokenKind::Agent);
457 }
458
459 #[tokio::test]
460 async fn a_token_bound_to_another_key_is_refused() {
461 let (ap, session) = (SigningKey::generate(), SigningKey::generate());
462 let token = agent_token(&ap, &session, 5_000);
463 let other = SigningKey::generate();
464 let refused = verify_token(&options(&token, other.thumbprint(), 1_000), &provider(&ap))
465 .await
466 .unwrap_err();
467 assert_eq!(refused.code, "key_binding_failed");
468 }
469
470 #[tokio::test]
471 async fn expiry_is_judged_only_once_the_signature_verifies() {
472 let (ap, session) = (SigningKey::generate(), SigningKey::generate());
473 let expired = agent_token(&ap, &session, 1_500);
474 let refused = verify_token(
475 &options(&expired, session.thumbprint(), 2_000),
476 &provider(&ap),
477 )
478 .await
479 .unwrap_err();
480 assert_eq!(refused.code, "token_expired");
481
482 let forged = agent_token(&SigningKey::generate(), &session, 1_500);
483 let refused = verify_token(
484 &options(&forged, session.thumbprint(), 2_000),
485 &provider(&ap),
486 )
487 .await
488 .unwrap_err();
489 assert_eq!(refused.code, "invalid_agent_token");
490 }
491
492 #[tokio::test]
493 async fn a_kind_not_accepted_here_is_refused() {
494 let (ap, session) = (SigningKey::generate(), SigningKey::generate());
495 let token = agent_token(&ap, &session, 5_000);
496 let mut opts = options(&token, session.thumbprint(), 1_000);
497 opts.accept = &[TokenKind::Auth];
498 let refused = verify_token(&opts, &provider(&ap)).await.unwrap_err();
499 assert_eq!(refused.code, "token_type_not_accepted");
500 }
501}