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