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