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