Skip to main content

netget/server/oauth2/
actions.rs

1//! OAuth2 protocol actions implementation
2
3use crate::llm::actions::{
4    protocol_trait::{ActionResult, Protocol, Server},
5    ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14/// OAuth2 protocol action handler
15pub struct OAuth2Protocol;
16
17impl OAuth2Protocol {
18    pub fn new() -> Self {
19        Self
20    }
21}
22
23// Implement Protocol trait (common functionality)
24impl Protocol for OAuth2Protocol {
25    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
26        // OAuth2 has no async actions - it's purely request-response
27        Vec::new()
28    }
29    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
30        vec![
31            oauth2_authorize_response_action(),
32            oauth2_token_response_action(),
33            oauth2_introspect_response_action(),
34            oauth2_error_response_action(),
35        ]
36    }
37    fn protocol_name(&self) -> &'static str {
38        "OAuth2"
39    }
40    fn get_event_types(&self) -> Vec<EventType> {
41        get_oauth2_event_types()
42    }
43    fn stack_name(&self) -> &'static str {
44        "ETH>IP>TCP>HTTP>OAuth2"
45    }
46    fn keywords(&self) -> Vec<&'static str> {
47        vec![
48            "oauth2",
49            "oauth",
50            "oauth 2.0",
51            "via oauth2",
52            "authorization server",
53        ]
54    }
55    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
56        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
57
58        ProtocolMetadataV2::builder()
59            .state(DevelopmentState::Experimental)
60            .implementation(
61                "Manual RFC 6749/7662/7009 endpoints over hyper HTTP/1.1. Simulator: tokens are \
62                 opaque strings the model invents, nothing is signed, nothing is stored, and no \
63                 credential is ever verified.",
64            )
65            .llm_control(
66                "Whether to approve an authorization request, what token strings to return, and \
67                 what a token introspects as. Client id/secret are handed to the model as text; \
68                 the server itself checks nothing.",
69            )
70            .e2e_testing("reqwest HTTP client - OAuth2 flow testing")
71            .notes(
72                "Not an authorization server: no signing keys, no token database, no client \
73                 registry, no PKCE, no TLS. Suitable for exercising OAuth2 clients and for \
74                 honeypots, never for guarding anything real.",
75            )
76            .max_inbound_bytes(crate::server::oauth2::MAX_REQUEST_BYTES)
77            .request_only("OAuth 2.0 is HTTP request/response; a server cannot send a peer anything unprompted")
78            .build()
79    }
80    fn description(&self) -> &'static str {
81        "OAuth2 authorization-server simulator (LLM decides; no tokens are signed or stored)"
82    }
83    fn example_prompt(&self) -> &'static str {
84        "Act as an OAuth2 server on port 8080. Accept client 'testapp' with secret 'secret123'. Issue tokens with 1-hour expiry."
85    }
86    fn group_name(&self) -> &'static str {
87        "AI & API"
88    }
89
90    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
91        use crate::llm::actions::StartupExamples;
92        use serde_json::json;
93
94        // Deterministic: issue a fixed authorization code, echoing the client's
95        // state parameter, no LLM call. This is an explicit operator decision to
96        // always approve — the LLM path must instead default to refusal.
97        let script = r#"import json, sys
98data = json.load(sys.stdin)
99event = data["event"]
100if data["event_type_id"] == "oauth2_authorize":
101    actions = [{"type": "oauth2_authorize_response",
102                "code": "AUTH_CODE_xyz123",
103                "state": event.get("state", "")}]
104else:
105    actions = []
106print(json.dumps({"actions": actions}))"#;
107
108        StartupExamples::new(
109            // LLM mode: LLM handles OAuth2 authorization
110            json!({
111                "type": "open_server",
112                "port": 8080,
113                "base_stack": "oauth2",
114                "instruction": "Act as OAuth2 authorization server. Accept client 'testapp' with secret 'secret123'. Issue tokens with 1-hour expiry."
115            }),
116            // Script mode: Code-based OAuth2 handling
117            json!({
118                "type": "open_server",
119                "port": 8080,
120                "base_stack": "oauth2",
121                "event_handlers": [{
122                    "event_pattern": "oauth2_authorize",
123                    "handler": {
124                        "type": "script",
125                        "language": "python",
126                        "code": script
127                    }
128                }]
129            }),
130            // Static mode: Fixed OAuth2 responses
131            json!({
132                "type": "open_server",
133                "port": 8080,
134                "base_stack": "oauth2",
135                "event_handlers": [
136                    {
137                        "event_pattern": "oauth2_authorize",
138                        "handler": {
139                            "type": "static",
140                            "actions": [{
141                                "type": "oauth2_authorize_response",
142                                "code": "AUTH_CODE_xyz123",
143                                "state": "random_state"
144                            }]
145                        }
146                    },
147                    {
148                        "event_pattern": "oauth2_token",
149                        "handler": {
150                            "type": "static",
151                            "actions": [{
152                                "type": "oauth2_token_response",
153                                "access_token": "ACCESS_TOKEN_xyz123",
154                                "token_type": "Bearer",
155                                "expires_in": 3600
156                            }]
157                        }
158                    }
159                ]
160            }),
161        )
162    }
163}
164
165// Implement Server trait (server-specific functionality)
166impl Server for OAuth2Protocol {
167    fn spawn(
168        &self,
169        ctx: crate::protocol::SpawnContext,
170    ) -> std::pin::Pin<
171        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
172    > {
173        Box::pin(async move {
174            use crate::server::oauth2::OAuth2Server;
175            OAuth2Server::spawn_with_llm_actions(
176                ctx.legacy_listen_addr(),
177                ctx.llm_client,
178                ctx.state,
179                ctx.status_tx,
180                ctx.server_id,
181            )
182            .await
183        })
184    }
185    fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
186        let action_type = action
187            .get("type")
188            .and_then(|v| v.as_str())
189            .context("Missing 'type' field in action")?;
190
191        match action_type {
192            "oauth2_authorize_response" => self.execute_oauth2_authorize_response(action),
193            "oauth2_token_response" => self.execute_oauth2_token_response(action),
194            "oauth2_introspect_response" => self.execute_oauth2_introspect_response(action),
195            "oauth2_error_response" => self.execute_oauth2_error_response(action),
196            _ => Err(anyhow::anyhow!("Unknown OAuth2 action: {action_type}")),
197        }
198    }
199}
200
201/// Envelope key identifying which OAuth2 action produced an [`ActionResult::Output`].
202///
203/// Every executor below wraps its payload as `{"oauth2_result": <kind>, ...}` so the
204/// HTTP layer in `mod.rs` can tell an *approval* from a *denial*. Before this existed the
205/// handlers just fished a `code` field out of whatever JSON came back, so an
206/// `oauth2_error_response` — the only way the model can say "no" — parsed as "no code
207/// present", fell through to the hardcoded default, and the client received a redirect
208/// carrying a perfectly usable authorization code. A denial silently became an approval.
209pub const OAUTH2_RESULT_KEY: &str = "oauth2_result";
210
211impl OAuth2Protocol {
212    /// Execute oauth2_authorize_response sync action
213    fn execute_oauth2_authorize_response(&self, action: serde_json::Value) -> Result<ActionResult> {
214        let code = action
215            .get("code")
216            .and_then(|v| v.as_str())
217            .context("Missing 'code' field")?;
218        let state = action.get("state").and_then(|v| v.as_str());
219
220        let response = json!({
221            OAUTH2_RESULT_KEY: "authorize",
222            "code": code,
223            "state": state,
224        });
225        let bytes = serde_json::to_vec(&response)?;
226        Ok(ActionResult::Output(bytes))
227    }
228
229    /// Execute oauth2_token_response sync action
230    fn execute_oauth2_token_response(&self, action: serde_json::Value) -> Result<ActionResult> {
231        let access_token = action
232            .get("access_token")
233            .and_then(|v| v.as_str())
234            .context("Missing 'access_token' field")?;
235        let token_type = action
236            .get("token_type")
237            .and_then(|v| v.as_str())
238            .unwrap_or("Bearer");
239        let expires_in = action
240            .get("expires_in")
241            .and_then(|v| v.as_i64())
242            .unwrap_or(3600);
243
244        let mut response = json!({
245            OAUTH2_RESULT_KEY: "token",
246            "access_token": access_token,
247            "token_type": token_type,
248            "expires_in": expires_in
249        });
250
251        // Optional fields
252        if let Some(refresh_token) = action.get("refresh_token").and_then(|v| v.as_str()) {
253            response["refresh_token"] = json!(refresh_token);
254        }
255        if let Some(scope) = action.get("scope").and_then(|v| v.as_str()) {
256            response["scope"] = json!(scope);
257        }
258
259        let bytes = serde_json::to_vec(&response)?;
260        Ok(ActionResult::Output(bytes))
261    }
262
263    /// Execute oauth2_introspect_response sync action
264    fn execute_oauth2_introspect_response(
265        &self,
266        action: serde_json::Value,
267    ) -> Result<ActionResult> {
268        let active = action
269            .get("active")
270            .and_then(|v| v.as_bool())
271            .unwrap_or(false);
272
273        let mut response = json!({
274            OAUTH2_RESULT_KEY: "introspect",
275            "active": active
276        });
277
278        // If active, include additional fields
279        if active {
280            if let Some(scope) = action.get("scope").and_then(|v| v.as_str()) {
281                response["scope"] = json!(scope);
282            }
283            if let Some(client_id) = action.get("client_id").and_then(|v| v.as_str()) {
284                response["client_id"] = json!(client_id);
285            }
286            if let Some(exp) = action.get("exp").and_then(|v| v.as_i64()) {
287                response["exp"] = json!(exp);
288            }
289            if let Some(token_type) = action.get("token_type").and_then(|v| v.as_str()) {
290                response["token_type"] = json!(token_type);
291            }
292        }
293
294        let bytes = serde_json::to_vec(&response)?;
295        Ok(ActionResult::Output(bytes))
296    }
297
298    /// Execute oauth2_error_response sync action
299    fn execute_oauth2_error_response(&self, action: serde_json::Value) -> Result<ActionResult> {
300        let error = action
301            .get("error")
302            .and_then(|v| v.as_str())
303            .context("Missing 'error' field")?;
304
305        let mut response = json!({
306            OAUTH2_RESULT_KEY: "error",
307            "error": error
308        });
309
310        if let Some(error_description) = action.get("error_description").and_then(|v| v.as_str()) {
311            response["error_description"] = json!(error_description);
312        }
313        if let Some(error_uri) = action.get("error_uri").and_then(|v| v.as_str()) {
314            response["error_uri"] = json!(error_uri);
315        }
316        // RFC 6749 §5.2: the token/introspection endpoints answer an error with 400, except
317        // invalid_client which is 401. The model may override, but the default must never be
318        // the 200 that the previous code sent for every error.
319        let status = action
320            .get("status_code")
321            .and_then(|v| v.as_u64())
322            .unwrap_or(if error == "invalid_client" { 401 } else { 400 });
323        response["status_code"] = json!(status);
324
325        let bytes = serde_json::to_vec(&response)?;
326        Ok(ActionResult::Output(bytes))
327    }
328}
329
330// ============================================================================
331// OAuth2 Action Definitions
332// ============================================================================
333
334fn oauth2_authorize_response_action() -> ActionDefinition {
335    ActionDefinition {
336        name: "oauth2_authorize_response".to_string(),
337        description: "Send authorization code response".to_string(),
338        parameters: vec![
339            Parameter {
340                name: "code".to_string(),
341                type_hint: "string".to_string(),
342                description: "Authorization code to return".to_string(),
343                required: true,
344            },
345            Parameter {
346                name: "state".to_string(),
347                type_hint: "string".to_string(),
348                description: "State parameter from request".to_string(),
349                required: false,
350            },
351        ],
352        example: json!({
353            "type": "oauth2_authorize_response",
354            "code": "AUTH_CODE_xyz123",
355            "state": "random_state"
356        }),
357        log_template: Some(
358            LogTemplate::new()
359                .with_info("-> OAuth2 authorize code issued")
360                .with_debug("OAuth2 oauth2_authorize_response: code issued"),
361        ),
362    }
363}
364
365fn oauth2_token_response_action() -> ActionDefinition {
366    ActionDefinition {
367        name: "oauth2_token_response".to_string(),
368        description: "Send access token response".to_string(),
369        parameters: vec![
370            Parameter {
371                name: "access_token".to_string(),
372                type_hint: "string".to_string(),
373                description: "Access token to issue".to_string(),
374                required: true,
375            },
376            Parameter {
377                name: "token_type".to_string(),
378                type_hint: "string".to_string(),
379                description: "Token type (typically 'Bearer')".to_string(),
380                required: false,
381            },
382            Parameter {
383                name: "expires_in".to_string(),
384                type_hint: "number".to_string(),
385                description: "Token lifetime in seconds".to_string(),
386                required: false,
387            },
388            Parameter {
389                name: "refresh_token".to_string(),
390                type_hint: "string".to_string(),
391                description: "Refresh token (optional)".to_string(),
392                required: false,
393            },
394            Parameter {
395                name: "scope".to_string(),
396                type_hint: "string".to_string(),
397                description: "Granted scopes (space-separated)".to_string(),
398                required: false,
399            },
400        ],
401        example: json!({
402            "type": "oauth2_token_response",
403            "access_token": "ACCESS_xyz123",
404            "token_type": "Bearer",
405            "expires_in": 3600,
406            "refresh_token": "REFRESH_xyz123",
407            "scope": "read write"
408        }),
409        log_template: Some(
410            LogTemplate::new()
411                .with_info("-> OAuth2 token issued type={token_type} expires={expires_in}s")
412                .with_debug("OAuth2 oauth2_token_response: type={token_type}, expires_in={expires_in}, scope={scope}"),
413        ),
414    }
415}
416
417fn oauth2_introspect_response_action() -> ActionDefinition {
418    ActionDefinition {
419        name: "oauth2_introspect_response".to_string(),
420        description: "Send token introspection response".to_string(),
421        parameters: vec![
422            Parameter {
423                name: "active".to_string(),
424                type_hint: "boolean".to_string(),
425                description: "Whether token is active".to_string(),
426                required: true,
427            },
428            Parameter {
429                name: "scope".to_string(),
430                type_hint: "string".to_string(),
431                description: "Token scopes".to_string(),
432                required: false,
433            },
434            Parameter {
435                name: "client_id".to_string(),
436                type_hint: "string".to_string(),
437                description: "Client ID".to_string(),
438                required: false,
439            },
440            Parameter {
441                name: "exp".to_string(),
442                type_hint: "number".to_string(),
443                description: "Expiration timestamp".to_string(),
444                required: false,
445            },
446            Parameter {
447                name: "token_type".to_string(),
448                type_hint: "string".to_string(),
449                description: "Token type, typically 'Bearer' (only echoed back when active=true)"
450                    .to_string(),
451                required: false,
452            },
453        ],
454        example: json!({
455            "type": "oauth2_introspect_response",
456            "active": true,
457            "scope": "read write",
458            "client_id": "client123",
459            "token_type": "Bearer",
460            "exp": 1234567890
461        }),
462        log_template: Some(
463            LogTemplate::new()
464                .with_info("-> OAuth2 introspect active={active}")
465                .with_debug("OAuth2 oauth2_introspect_response: active={active}, client_id={client_id}, scope={scope}"),
466        ),
467    }
468}
469
470fn oauth2_error_response_action() -> ActionDefinition {
471    ActionDefinition {
472        name: "oauth2_error_response".to_string(),
473        description: "Send OAuth2 error response".to_string(),
474        parameters: vec![
475            Parameter {
476                name: "error".to_string(),
477                type_hint: "string".to_string(),
478                description: "Error code (e.g., 'invalid_request', 'unauthorized_client')"
479                    .to_string(),
480                required: true,
481            },
482            Parameter {
483                name: "error_description".to_string(),
484                type_hint: "string".to_string(),
485                description: "Human-readable error description".to_string(),
486                required: false,
487            },
488            Parameter {
489                name: "error_uri".to_string(),
490                type_hint: "string".to_string(),
491                description: "URI with error information".to_string(),
492                required: false,
493            },
494            Parameter {
495                name: "status_code".to_string(),
496                type_hint: "number".to_string(),
497                description: "HTTP status for the error (default: 400, or 401 for invalid_client). \
498                              Ignored on /authorize, which answers errors with a redirect per RFC 6749 §4.1.2.1."
499                    .to_string(),
500                required: false,
501            },
502        ],
503        example: json!({
504            "type": "oauth2_error_response",
505            "error": "invalid_client",
506            "error_description": "Client authentication failed"
507        }),
508        log_template: Some(
509            LogTemplate::new()
510                .with_info("-> OAuth2 error {error}")
511                .with_debug(
512                    "OAuth2 oauth2_error_response: error={error}, description={error_description}",
513                ),
514        ),
515    }
516}
517
518// ============================================================================
519// OAuth2 Event Type Constants
520// ============================================================================
521
522/// OAuth2 authorize event - triggered when client requests authorization
523pub static OAUTH2_AUTHORIZE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
524    EventType::new(
525        "oauth2_authorize",
526        "OAuth2 authorization request received",
527        json!({
528            "type": "oauth2_authorize_response",
529            "code": "AUTH_CODE_xyz123",
530            "state": "echo the state parameter from the request"
531        }),
532    )
533    .with_alternative_example(json!({
534        "type": "oauth2_error_response",
535        "error": "unauthorized_client",
536        "error_description": "Client 'evilapp' is not registered"
537    }))
538    .with_parameters(vec![
539        Parameter {
540            name: "response_type".to_string(),
541            type_hint: "string".to_string(),
542            description: "Response type (e.g., 'code', 'token')".to_string(),
543            required: true,
544        },
545        Parameter {
546            name: "client_id".to_string(),
547            type_hint: "string".to_string(),
548            description: "Client identifier".to_string(),
549            required: true,
550        },
551        Parameter {
552            name: "redirect_uri".to_string(),
553            type_hint: "string".to_string(),
554            description: "Redirection URI".to_string(),
555            required: false,
556        },
557        Parameter {
558            name: "scope".to_string(),
559            type_hint: "string".to_string(),
560            description: "Requested scopes".to_string(),
561            required: false,
562        },
563        Parameter {
564            name: "state".to_string(),
565            type_hint: "string".to_string(),
566            description: "State parameter for CSRF protection".to_string(),
567            required: false,
568        },
569    ])
570    // These must be the *real* definitions. `call_llm` advertises `event.event_type.actions`,
571    // not `get_sync_actions()`, so the stubs that used to live here (`parameters: vec![]`,
572    // `example: json!({})`) were the entire vocabulary the model got: it was told
573    // `oauth2_authorize_response` existed but never told that it takes a `code`.
574    .with_actions(vec![
575        oauth2_authorize_response_action(),
576        oauth2_error_response_action(),
577    ])
578    .with_log_template(
579        LogTemplate::new()
580            .with_info("OAuth2 authorize {response_type} from {client_id}")
581            .with_debug("OAuth2 authorize {response_type} from {client_id}, scope={scope}")
582            .with_trace("OAuth2 authorize: {json_pretty(.)}"),
583    )
584});
585
586/// OAuth2 token event - triggered when client requests access token
587pub static OAUTH2_TOKEN_EVENT: LazyLock<EventType> = LazyLock::new(|| {
588    EventType::new(
589        "oauth2_token",
590        "OAuth2 token request received",
591        json!({
592            "type": "oauth2_token_response",
593            "access_token": "ACCESS_xyz123",
594            "token_type": "Bearer",
595            "expires_in": 3600,
596            "scope": "read write"
597        }),
598    )
599    .with_alternative_example(json!({
600        "type": "oauth2_error_response",
601        "error": "invalid_grant",
602        "error_description": "Authorization code has expired"
603    }))
604    .with_parameters(vec![
605        Parameter {
606            name: "grant_type".to_string(),
607            type_hint: "string".to_string(),
608            description:
609                "Grant type (authorization_code, refresh_token, password, client_credentials)"
610                    .to_string(),
611            required: true,
612        },
613        Parameter {
614            name: "code".to_string(),
615            type_hint: "string".to_string(),
616            description: "Authorization code (for authorization_code grant)".to_string(),
617            required: false,
618        },
619        Parameter {
620            name: "redirect_uri".to_string(),
621            type_hint: "string".to_string(),
622            description: "Redirection URI".to_string(),
623            required: false,
624        },
625        Parameter {
626            name: "client_id".to_string(),
627            type_hint: "string".to_string(),
628            description: "Client identifier".to_string(),
629            required: false,
630        },
631        Parameter {
632            name: "client_secret".to_string(),
633            type_hint: "string".to_string(),
634            description: "Client secret".to_string(),
635            required: false,
636        },
637        Parameter {
638            name: "refresh_token".to_string(),
639            type_hint: "string".to_string(),
640            description: "Refresh token (for refresh_token grant)".to_string(),
641            required: false,
642        },
643        Parameter {
644            name: "username".to_string(),
645            type_hint: "string".to_string(),
646            description: "Username (for password grant)".to_string(),
647            required: false,
648        },
649        Parameter {
650            name: "password".to_string(),
651            type_hint: "string".to_string(),
652            description: "Password (for password grant)".to_string(),
653            required: false,
654        },
655        Parameter {
656            name: "scope".to_string(),
657            type_hint: "string".to_string(),
658            description: "Requested scopes".to_string(),
659            required: false,
660        },
661    ])
662    .with_actions(vec![
663        oauth2_token_response_action(),
664        oauth2_error_response_action(),
665    ])
666    .with_log_template(
667        LogTemplate::new()
668            .with_info("OAuth2 token {grant_type}")
669            .with_debug("OAuth2 token {grant_type}, client={client_id}")
670            .with_trace("OAuth2 token: {json_pretty(.)}"),
671    )
672});
673
674/// OAuth2 introspect event - triggered when token introspection is requested
675pub static OAUTH2_INTROSPECT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
676    EventType::new(
677        "oauth2_introspect",
678        "OAuth2 token introspection request",
679        json!({
680            "type": "oauth2_introspect_response",
681            "active": true,
682            "scope": "read write",
683            "client_id": "client123",
684            "token_type": "Bearer",
685            "exp": 1234567890
686        }),
687    )
688    .with_alternative_example(json!({
689        "type": "oauth2_introspect_response",
690        "active": false
691    }))
692    .with_parameters(vec![
693        Parameter {
694            name: "token".to_string(),
695            type_hint: "string".to_string(),
696            description: "Token to introspect".to_string(),
697            required: true,
698        },
699        Parameter {
700            name: "token_type_hint".to_string(),
701            type_hint: "string".to_string(),
702            description: "Hint about token type (access_token, refresh_token)".to_string(),
703            required: false,
704        },
705    ])
706    .with_actions(vec![
707        oauth2_introspect_response_action(),
708        oauth2_error_response_action(),
709    ])
710    .with_log_template(
711        LogTemplate::new()
712            .with_info("OAuth2 introspect")
713            .with_debug("OAuth2 introspect, token_type={token_type_hint}")
714            .with_trace("OAuth2 introspect: {json_pretty(.)}"),
715    )
716});
717
718/// OAuth2 revoke event - triggered when token revocation is requested
719pub static OAUTH2_REVOKE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
720    EventType::new(
721        "oauth2_revoke",
722        "OAuth2 token revocation request. RFC 7009 §2.2 requires the server to answer 200 \
723         whether or not the token existed, so the response body is fixed and no protocol \
724         action can change it; note the revocation in memory if it matters later.",
725        json!({
726            "type": "append_memory",
727            "value": "revoked token: ACCESS_xyz123"
728        }),
729    )
730    // Deliberately no protocol actions, hence with_no_actions() rather than an empty list:
731    // the reply is fixed by RFC 7009 and mod.rs sends it regardless of what the model says.
732    // Left empty, `call_llm` would log a BUG and (in debug builds) trip a debug_assert.
733    .with_no_actions()
734    .with_parameters(vec![
735        Parameter {
736            name: "token".to_string(),
737            type_hint: "string".to_string(),
738            description: "Token to revoke".to_string(),
739            required: true,
740        },
741        Parameter {
742            name: "token_type_hint".to_string(),
743            type_hint: "string".to_string(),
744            description: "Hint about token type (access_token, refresh_token)".to_string(),
745            required: false,
746        },
747    ])
748    .with_log_template(
749        LogTemplate::new()
750            .with_info("OAuth2 revoke")
751            .with_debug("OAuth2 revoke, token_type={token_type_hint}")
752            .with_trace("OAuth2 revoke: {json_pretty(.)}"),
753    )
754});
755
756/// Get OAuth2 event types
757pub fn get_oauth2_event_types() -> Vec<EventType> {
758    vec![
759        OAUTH2_AUTHORIZE_EVENT.clone(),
760        OAUTH2_TOKEN_EVENT.clone(),
761        OAUTH2_INTROSPECT_EVENT.clone(),
762        OAUTH2_REVOKE_EVENT.clone(),
763    ]
764}