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}
35
36pub fn extract_user_context(token: &str, issuer: &str) -> AuthResult<JwtUserContext> {
37    let claims = decode_session_claims(token, &ValidationPolicy::session_context(issuer))?;
38
39    let session_id = claims.session_id.ok_or(AuthError::MissingSessionId)?;
40    let role = *claims.scope.first().ok_or(AuthError::MissingScope)?;
41    let act_chain = claims
42        .act
43        .as_ref()
44        .map(systemprompt_models::auth::ActClaim::flatten_to_chain)
45        .unwrap_or_default();
46
47    Ok(JwtUserContext {
48        user_id: UserId::try_new(claims.sub).map_err(AuthError::InvalidSubject)?,
49        session_id,
50        role,
51        user_type: claims.user_type,
52        client_id: claims.client_id,
53        act_chain,
54        attributes: claims.attributes,
55        jti: (!claims.jti.is_empty()).then(|| AccessTokenId::new(claims.jti)),
56        exp: claims.exp,
57    })
58}