Skip to main content

systemprompt_security/jwt/
decode.rs

1//! Bearer-token decode for request-context middleware.
2//!
3//! [`extract_user_context`] decodes via
4//! [`super::validate::decode_rs256_claims`]
5//! with [`ValidationPolicy::session_context`] (signature, RS256, `kid`, `exp`,
6//! `nbf` + leeway), then re-derives `user_type` from `scope` so a forged or
7//! mis-minted type claim cannot ride past the gate, and returns the subset of
8//! claims the request-context layer consumes ([`JwtUserContext`]). Issuer and
9//! audience pinning is left to the stateful validators that hold deployment
10//! config ([`crate::AuthValidationService`]); this path instead binds the
11//! token to a live session and user row in the database after decode.
12
13use std::collections::BTreeMap;
14use systemprompt_identifiers::{Actor, ClientId, SessionId, UserId};
15use systemprompt_models::auth::{Permission, UserType};
16
17use super::validate::{ValidationPolicy, decode_rs256_claims};
18use crate::error::{AuthError, AuthResult};
19
20#[derive(Debug, Clone)]
21pub struct JwtUserContext {
22    pub user_id: UserId,
23    pub session_id: SessionId,
24    pub role: Permission,
25    pub user_type: UserType,
26    pub client_id: Option<ClientId>,
27    pub act_chain: Vec<Actor>,
28    pub attributes: BTreeMap<String, serde_json::Value>,
29    pub jti: String,
30    pub exp: i64,
31}
32
33pub fn extract_user_context(token: &str) -> AuthResult<JwtUserContext> {
34    let claims = decode_rs256_claims(token, &ValidationPolicy::session_context())?;
35
36    let session_id = claims.session_id.ok_or(AuthError::MissingSessionId)?;
37    let role = *claims.scope.first().ok_or(AuthError::MissingScope)?;
38    let derived_type = UserType::from_permissions(&claims.scope);
39    if derived_type != claims.user_type {
40        return Err(AuthError::UserTypeMismatch {
41            claimed: claims.user_type,
42            derived: derived_type,
43        });
44    }
45    let act_chain = claims
46        .act
47        .as_ref()
48        .map(systemprompt_models::auth::ActClaim::flatten_to_chain)
49        .unwrap_or_default();
50
51    Ok(JwtUserContext {
52        user_id: UserId::new(claims.sub),
53        session_id,
54        role,
55        user_type: derived_type,
56        client_id: claims.client_id,
57        act_chain,
58        attributes: claims.attributes,
59        jti: claims.jti,
60        exp: claims.exp,
61    })
62}