1use 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
14pub struct OAuth2Protocol;
16
17impl OAuth2Protocol {
18 pub fn new() -> Self {
19 Self
20 }
21}
22
23impl Protocol for OAuth2Protocol {
25 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
26 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 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 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 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 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
165impl 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
201pub const OAUTH2_RESULT_KEY: &str = "oauth2_result";
210
211impl OAuth2Protocol {
212 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 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 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 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 {
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 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 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
330fn 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
518pub 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 .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
586pub 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
674pub 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
718pub 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 .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
756pub 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}