systemprompt_security/auth/
hook_token.rs1use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
20use systemprompt_models::auth::{JwtAudience, JwtClaims, Permission};
21
22use crate::error::{AuthError, AuthResult};
23
24#[derive(Debug, Clone)]
27pub struct ValidatedHookClaims {
28 pub plugin_id: String,
29 pub subject: String,
30 pub scopes: Vec<Permission>,
31}
32
33#[derive(Debug)]
34pub struct HookTokenValidator {
35 secret: String,
36 issuer: String,
37}
38
39impl HookTokenValidator {
40 #[must_use]
41 pub const fn new(secret: String, issuer: String) -> Self {
42 Self { secret, 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 mut validation = Validation::new(Algorithm::HS256);
81 validation.set_issuer(&[&self.issuer]);
82 validation.set_audience(&[JwtAudience::Hook.as_str()]);
83
84 let token_data = decode::<JwtClaims>(
85 token,
86 &DecodingKey::from_secret(self.secret.as_bytes()),
87 &validation,
88 )
89 .map_err(AuthError::InvalidToken)?;
90
91 let claims = token_data.claims;
92
93 if !claims.aud.iter().any(|a| matches!(a, JwtAudience::Hook)) {
94 return Err(AuthError::HookAudienceMissing);
95 }
96 if !claims.scope.contains(&required_scope) {
97 return Err(AuthError::HookScopeMissing(required_scope_name));
98 }
99 let plugin_id = claims
100 .plugin_id
101 .clone()
102 .ok_or(AuthError::HookPluginIdMissing)?;
103 if let Some(expected) = request_plugin_id
104 && expected != plugin_id
105 {
106 return Err(AuthError::HookPluginIdMismatch {
107 expected: expected.to_string(),
108 actual: plugin_id,
109 });
110 }
111
112 Ok(ValidatedHookClaims {
113 plugin_id,
114 subject: claims.sub,
115 scopes: claims.scope,
116 })
117 }
118}