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