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_session_claims`] with
5//! [`ValidationPolicy::session_context`] (signature, RS256, `kid`, `exp`,
6//! `nbf` + leeway, the deployment issuer, first-party `aud`, act-chain depth,
7//! `user_type` re-derived from `scope`) — the same checks the A2A
8//! [`crate::AuthValidationService`] applies — and returns the subset of claims
9//! the request-context layer consumes ([`JwtUserContext`]). The caller then
10//! binds the token to a live session and user row in the database.
11//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use std::collections::BTreeMap;
16use systemprompt_identifiers::{AccessTokenId, Actor, ClientId, SessionId, UserId};
17use systemprompt_models::auth::{Permission, UserType};
18
19use super::validate::{ValidationPolicy, decode_session_claims};
20use crate::error::{AuthError, AuthResult};
21
22#[derive(Debug, Clone)]
23pub struct JwtUserContext {
24    pub user_id: UserId,
25    pub session_id: SessionId,
26    pub role: Permission,
27    pub user_type: UserType,
28    pub client_id: Option<ClientId>,
29    pub act_chain: Vec<Actor>,
30    // JSON: ABAC attribute bag — JWT claim values are policy-defined and schema-less.
31    pub attributes: BTreeMap<String, serde_json::Value>,
32    pub jti: Option<AccessTokenId>,
33    pub exp: i64,
34    pub purpose: Option<String>,
35}
36
37pub fn extract_user_context(token: &str, issuer: &str) -> AuthResult<JwtUserContext> {
38    let claims = decode_session_claims(token, &ValidationPolicy::session_context(issuer))?;
39
40    let session_id = claims.session_id.ok_or(AuthError::MissingSessionId)?;
41    let role = *claims.scope.first().ok_or(AuthError::MissingScope)?;
42    let act_chain = claims
43        .act
44        .as_ref()
45        .map(systemprompt_models::auth::ActClaim::flatten_to_chain)
46        .unwrap_or_default();
47
48    Ok(JwtUserContext {
49        user_id: UserId::try_new(claims.sub).map_err(AuthError::InvalidSubject)?,
50        session_id,
51        role,
52        user_type: claims.user_type,
53        client_id: claims.client_id,
54        act_chain,
55        attributes: claims.attributes,
56        jti: (!claims.jti.is_empty()).then(|| AccessTokenId::new(claims.jti)),
57        exp: claims.exp,
58        purpose: claims.purpose,
59    })
60}