systemprompt_security/jwt/
validate.rs1use jsonwebtoken::{Algorithm, Validation, decode, decode_header};
29use systemprompt_models::auth::{JwtAudience, JwtClaims, MAX_ACT_CHAIN_DEPTH, UserType};
30
31use crate::error::{AuthError, AuthResult};
32use crate::keys::authority;
33
34pub const JWT_LEEWAY_SECONDS: u64 = 30;
35
36#[derive(Debug, Clone)]
37pub struct ValidationPolicy<'a> {
38 leeway_seconds: u64,
39 issuer: &'a str,
40 audiences: &'a [JwtAudience],
41}
42
43impl<'a> ValidationPolicy<'a> {
44 #[must_use]
45 pub const fn session_context(issuer: &'a str) -> Self {
46 Self::issuer_scoped(issuer, JwtAudience::FIRST_PARTY)
47 }
48
49 #[must_use]
50 pub const fn issuer_scoped(issuer: &'a str, audiences: &'a [JwtAudience]) -> Self {
51 Self {
52 leeway_seconds: JWT_LEEWAY_SECONDS,
53 issuer,
54 audiences,
55 }
56 }
57}
58
59pub fn decode_rs256_claims(token: &str, policy: &ValidationPolicy<'_>) -> AuthResult<JwtClaims> {
60 if policy.audiences.is_empty() {
61 return Err(AuthError::EmptyAudiencePolicy);
62 }
63
64 let header = decode_header(token).map_err(AuthError::InvalidToken)?;
65 if header.alg != Algorithm::RS256 {
66 return Err(AuthError::UnsupportedAlgorithm {
67 got: format!("{:?}", header.alg),
68 });
69 }
70 let kid = header.kid.as_deref().ok_or(AuthError::MissingKid)?;
71 let key = authority::decoding_key_for_kid(kid)
72 .map_err(AuthError::KeyLookup)?
73 .ok_or_else(|| AuthError::UnknownKid(kid.to_owned()))?;
74
75 let mut validation = Validation::new(Algorithm::RS256);
76 validation.validate_exp = true;
77 validation.validate_nbf = true;
78 validation.leeway = policy.leeway_seconds;
79 validation.set_issuer(&[policy.issuer]);
80 let audience_strs: Vec<&str> = policy.audiences.iter().map(JwtAudience::as_str).collect();
81 validation.set_audience(&audience_strs);
82
83 decode::<JwtClaims>(token, key, &validation)
84 .map(|data| data.claims)
85 .map_err(AuthError::InvalidToken)
86}
87
88pub fn decode_session_claims(token: &str, policy: &ValidationPolicy<'_>) -> AuthResult<JwtClaims> {
89 let claims = decode_rs256_claims(token, policy)?;
90
91 if let Some(act) = claims.act.as_ref() {
92 let depth = act.depth();
93 if depth > MAX_ACT_CHAIN_DEPTH {
94 return Err(AuthError::ActChainTooDeep {
95 depth,
96 max: MAX_ACT_CHAIN_DEPTH,
97 });
98 }
99 }
100
101 let derived_type = UserType::from_permissions(&claims.scope);
102 if derived_type != claims.user_type {
103 return Err(AuthError::UserTypeMismatch {
104 claimed: claims.user_type,
105 derived: derived_type,
106 });
107 }
108
109 Ok(claims)
110}