systemprompt_security/auth/
hook_token.rs1use systemprompt_identifiers::{PluginId, UserId};
23use systemprompt_models::auth::{JwtAudience, Permission};
24
25use crate::error::{AuthError, AuthResult};
26use crate::jwt::{ValidationPolicy, decode_rs256_claims};
27
28#[derive(Debug, Clone)]
29pub struct ValidatedHookClaims {
30 pub plugin_id: PluginId,
31 pub subject: UserId,
32 pub scopes: Vec<Permission>,
33}
34
35#[derive(Debug)]
36pub struct HookTokenValidator {
37 issuer: String,
38}
39
40impl HookTokenValidator {
41 #[must_use]
42 pub const fn new(issuer: String) -> Self {
43 Self { issuer }
44 }
45
46 pub fn validate_govern(
47 &self,
48 token: &str,
49 request_plugin_id: Option<&str>,
50 ) -> AuthResult<ValidatedHookClaims> {
51 self.validate(
52 token,
53 Permission::HookGovern,
54 "hook:govern",
55 request_plugin_id,
56 )
57 }
58
59 pub fn validate_track(
60 &self,
61 token: &str,
62 request_plugin_id: Option<&str>,
63 ) -> AuthResult<ValidatedHookClaims> {
64 self.validate(
65 token,
66 Permission::HookTrack,
67 "hook:track",
68 request_plugin_id,
69 )
70 }
71
72 fn validate(
73 &self,
74 token: &str,
75 required_scope: Permission,
76 required_scope_name: &'static str,
77 request_plugin_id: Option<&str>,
78 ) -> AuthResult<ValidatedHookClaims> {
79 let policy = ValidationPolicy::issuer_scoped(&self.issuer, &[JwtAudience::Hook]);
80 let claims = decode_rs256_claims(token, &policy)?;
81
82 if !claims.scope.contains(&required_scope) {
83 return Err(AuthError::HookScopeMissing(required_scope_name));
84 }
85 let plugin_id = claims
86 .plugin_id
87 .clone()
88 .ok_or(AuthError::HookPluginIdMissing)?;
89 if let Some(expected) = request_plugin_id
90 && expected != plugin_id.as_str()
91 {
92 return Err(AuthError::HookPluginIdMismatch {
93 expected: expected.to_owned(),
94 actual: plugin_id,
95 });
96 }
97
98 Ok(ValidatedHookClaims {
99 plugin_id: PluginId::new(plugin_id),
100 subject: UserId::new(claims.sub),
101 scopes: claims.scope,
102 })
103 }
104}