1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use base64::Engine;
6use serde_json::{Map, Value};
7
8use crate::error::{Error, Result};
9use crate::jwk::{Algorithm, Jwk};
10use crate::keys::SigningKey;
11
12#[derive(Clone, Debug)]
14pub struct Decoded {
15 pub header: Map<String, Value>,
17 pub claims: Map<String, Value>,
19 signing_input: String,
20 signature: Vec<u8>,
21}
22
23impl Decoded {
24 pub fn typ(&self) -> Option<&str> {
26 self.header.get("typ").and_then(Value::as_str)
27 }
28
29 pub fn alg(&self) -> Option<&str> {
31 self.header.get("alg").and_then(Value::as_str)
32 }
33
34 pub fn kid(&self) -> Option<&str> {
36 self.header.get("kid").and_then(Value::as_str)
37 }
38
39 pub fn claim_str(&self, name: &str) -> Option<&str> {
41 self.claims
42 .get(name)
43 .and_then(Value::as_str)
44 .filter(|s| !s.is_empty())
45 }
46
47 pub fn verify_signature(&self, key: &Jwk, algorithm: Algorithm) -> bool {
49 key.verify(algorithm, self.signing_input.as_bytes(), &self.signature)
50 }
51}
52
53fn decode_part(part: &str) -> Result<Map<String, Value>> {
54 let bytes = URL_SAFE_NO_PAD
55 .decode(part.trim_end_matches('='))
56 .map_err(|_| Error::Invalid("JWT part is not base64url".into()))?;
57 match serde_json::from_slice(&bytes) {
58 Ok(Value::Object(map)) => Ok(map),
59 _ => Err(Error::Invalid("JWT part is not a JSON object".into())),
60 }
61}
62
63pub fn decode(jwt: &str) -> Result<Decoded> {
65 let mut parts = jwt.split('.');
66 let (Some(header), Some(claims), Some(signature), None) =
67 (parts.next(), parts.next(), parts.next(), parts.next())
68 else {
69 return Err(Error::Invalid("not a compact JWS".into()));
70 };
71 Ok(Decoded {
72 header: decode_part(header)?,
73 claims: decode_part(claims)?,
74 signing_input: format!("{header}.{claims}"),
75 signature: URL_SAFE_NO_PAD
76 .decode(signature)
77 .map_err(|_| Error::Invalid("JWT signature is not base64url".into()))?,
78 })
79}
80
81pub fn encode(key: &SigningKey, mut header: Map<String, Value>, claims: &Value) -> String {
84 header.insert("alg".into(), Value::from(key.algorithm().as_str()));
85 let input = format!(
86 "{}.{}",
87 URL_SAFE_NO_PAD.encode(Value::Object(header).to_string()),
88 URL_SAFE_NO_PAD.encode(claims.to_string())
89 );
90 let signature = key.sign(input.as_bytes());
91 format!("{input}.{}", URL_SAFE_NO_PAD.encode(signature))
92}
93
94pub fn remaining_secs(jwt: &str, now: i64, margin: i64) -> Option<u64> {
97 let payload = jwt.split('.').nth(1)?;
98 let bytes = URL_SAFE_NO_PAD.decode(payload).ok()?;
99 let exp = serde_json::from_slice::<Value>(&bytes)
100 .ok()?
101 .get("exp")?
102 .as_i64()?;
103 let left = exp - now - margin;
104 (left > 0).then_some(left as u64)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use serde_json::json;
111
112 #[test]
113 fn a_minted_jwt_verifies_and_a_tampered_one_does_not() {
114 let key = SigningKey::generate();
115 let header = json!({"typ": "aa-agent+jwt"}).as_object().unwrap().clone();
116 let jwt = encode(&key, header, &json!({"sub": "a"}));
117 let decoded = decode(&jwt).unwrap();
118 assert_eq!(decoded.alg(), Some("Ed25519"));
119 assert!(decoded.verify_signature(&key.public_jwk(), Algorithm::Ed25519));
120
121 let mut parts: Vec<&str> = jwt.split('.').collect();
122 let forged = URL_SAFE_NO_PAD.encode(json!({"sub": "b"}).to_string());
123 parts[1] = &forged;
124 let tampered = decode(&parts.join(".")).unwrap();
125 assert!(!tampered.verify_signature(&key.public_jwk(), Algorithm::Ed25519));
126 }
127
128 #[test]
129 fn a_token_is_kept_until_shortly_before_it_expires() {
130 let enc = |v: Value| URL_SAFE_NO_PAD.encode(v.to_string());
131 let jwt = |exp: i64| {
132 format!(
133 "{}.{}.sig",
134 enc(json!({"alg": "ES256"})),
135 enc(json!({"exp": exp}))
136 )
137 };
138 assert_eq!(remaining_secs(&jwt(1_100), 1_000, 30), Some(70));
139 assert_eq!(remaining_secs(&jwt(1_020), 1_000, 30), None);
140 assert_eq!(remaining_secs("garbage", 1_000, 30), None);
141 }
142}