Skip to main content

systemprompt_models/auth/
token_authority.rs

1//! The deployment's token authority: verification and signing of its own
2//! RS256 JWTs behind one object-safe trait.
3//!
4//! Extension code holds only `&dyn ExtensionContext`; it reaches the
5//! deployment's keys and issuer through [`TokenAuthority`] and never through
6//! the key material itself. The trait is `dyn`-compatible on purpose: the
7//! implementor lives in the infra layer (`systemprompt_security`), the
8//! consumers live in domain, app and extension crates that must not depend on
9//! it, and the object is handed across the `ExtensionContext` seam.
10//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use chrono::Duration;
15use systemprompt_identifiers::{AccessTokenId, ClientId, PluginId, SessionId};
16use systemprompt_traits::BoxedSource;
17use thiserror::Error;
18
19use super::{AuthenticatedUser, JwtAudience, JwtClaims, Permission, TokenInteraction};
20
21#[derive(Debug, Error)]
22#[non_exhaustive]
23pub enum TokenError {
24    #[error("token `kid` `{0}` does not match any known signing key")]
25    UnknownKid(String),
26
27    #[error("token signed with `{got}`; only RS256 is accepted")]
28    UnsupportedAlgorithm { got: String },
29
30    #[error("token carries no `kid` naming the active signing key")]
31    KidMismatch,
32
33    #[error("token has expired")]
34    Expired,
35
36    #[error("token signature is invalid")]
37    InvalidSignature,
38
39    #[error("token audience is not accepted")]
40    InvalidAudience,
41
42    #[error("token issuer is not this deployment")]
43    InvalidIssuer,
44
45    #[error("token is malformed: {0}")]
46    Malformed(#[source] BoxedSource),
47
48    #[error("token rejected by policy: {0}")]
49    Rejected(#[source] BoxedSource),
50
51    #[error("token lifetime of {seconds}s is outside the permitted range")]
52    InvalidLifetime { seconds: i64 },
53
54    #[error("token signing failed: {0}")]
55    Signing(#[source] BoxedSource),
56}
57
58pub type TokenResult<T> = Result<T, TokenError>;
59
60#[derive(Debug, Clone)]
61pub struct UserJwtRequest {
62    pub user: AuthenticatedUser,
63    pub session_id: SessionId,
64    pub jti: AccessTokenId,
65    pub permissions: Vec<Permission>,
66    pub audience: Vec<JwtAudience>,
67    pub ttl: Duration,
68    pub client_id: Option<ClientId>,
69    pub purpose: Option<String>,
70    pub plugin_id: Option<PluginId>,
71    pub interaction: Option<TokenInteraction>,
72    pub browser_session: bool,
73}
74
75pub trait TokenAuthority: Send + Sync + std::fmt::Debug {
76    fn verify_session_cookie(&self, token: &str) -> TokenResult<JwtClaims>;
77
78    fn verify_bearer(&self, token: &str, audiences: &[JwtAudience]) -> TokenResult<JwtClaims>;
79
80    fn verify_hook_token(&self, token: &str) -> TokenResult<JwtClaims>;
81
82    // JSON: an extension-defined claim set has no fixed schema; the signer only
83    // serialises it
84    fn sign_claims(&self, claims: &serde_json::Value) -> TokenResult<String>;
85
86    fn mint_user_jwt(&self, req: &UserJwtRequest) -> TokenResult<String>;
87}