Skip to main content

netget/server/openid/
actions.rs

1//! OpenID Connect protocol actions for LLM integration
2
3use crate::llm::actions::{
4    protocol_trait::{ActionResult, Protocol, Server},
5    ActionDefinition, Parameter, ParameterDefinition,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{anyhow, Result};
11use serde_json::{json, Value as JsonValue};
12use std::sync::LazyLock;
13use tracing::{debug, error, info, warn};
14
15/// OpenID Connect request event - triggered when client sends an HTTP request to OIDC server
16pub static OPENID_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
17    EventType::new(
18        "openid_request",
19        "HTTP request received by OpenID Connect server. Answer according to endpoint_type.",
20        json!({
21            "type": "send_discovery_document",
22            "issuer": "http://localhost:8080",
23            "authorization_endpoint": "http://localhost:8080/authorize",
24            "token_endpoint": "http://localhost:8080/token",
25            "userinfo_endpoint": "http://localhost:8080/userinfo",
26            "jwks_uri": "http://localhost:8080/jwks.json"
27        }),
28    )
29    .with_alternative_example(json!({
30        "type": "send_token_response",
31        "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleTEifQ.eyJzdWIiOiJ1c2VyMTIzIn0.SIGNATURE",
32        "token_type": "Bearer",
33        "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleTEifQ.eyJzdWIiOiJ1c2VyMTIzIn0.SIGNATURE",
34        "expires_in": 3600,
35        "scope": "openid profile email"
36    }))
37    .with_alternative_example(json!({
38        "type": "send_userinfo_response",
39        "sub": "user123",
40        "name": "John Doe",
41        "email": "john@example.com",
42        "email_verified": true
43    }))
44    .with_alternative_example(json!({
45        "type": "send_error_response",
46        "error": "invalid_client",
47        "error_description": "Client authentication failed",
48        "status_code": 401
49    }))
50    // `call_llm` builds the model's tool list from `event.event_type.actions`, NOT from
51    // `get_sync_actions()`. This list was missing entirely: the model was told
52    // "No specific actions available for this event", got only set_memory/show_message/
53    // append_to_log, and every OIDC action it invented was rejected as unknown, retried
54    // twice, and failed — so the server answered every request with its 500 fallback.
55    .with_actions(OpenIdProtocol.get_sync_actions())
56    .with_log_template(
57        LogTemplate::new()
58            .with_info("{client_ip} OIDC {method} {path} ({endpoint_type}) ({duration_ms}ms)")
59            .with_debug("OIDC request from {client_ip}: {method} {path}, endpoint={endpoint_type}")
60            .with_trace("OIDC request: {json_pretty(.)}"),
61    )
62    .with_parameters(vec![
63        Parameter {
64            name: "method".to_string(),
65            type_hint: "string".to_string(),
66            description: "HTTP method (GET, POST, etc.)".to_string(),
67            required: true,
68        },
69        Parameter {
70            name: "path".to_string(),
71            type_hint: "string".to_string(),
72            description: "Request path (e.g., /.well-known/openid-configuration, /authorize, /token, /userinfo, /jwks.json)".to_string(),
73            required: true,
74        },
75        Parameter {
76            name: "query_params".to_string(),
77            type_hint: "object".to_string(),
78            description: "Query parameters as key-value pairs (for /authorize endpoint)".to_string(),
79            required: false,
80        },
81        Parameter {
82            name: "headers".to_string(),
83            type_hint: "object".to_string(),
84            description: "Request headers as key-value pairs".to_string(),
85            required: false,
86        },
87        Parameter {
88            name: "body".to_string(),
89            type_hint: "string".to_string(),
90            description: "Request body (for POST requests like /token)".to_string(),
91            required: false,
92        },
93        Parameter {
94            name: "form_data".to_string(),
95            type_hint: "object".to_string(),
96            description: "Body parsed as application/x-www-form-urlencoded, when the Content-Type says so. This is where /token parameters (grant_type, code, client_id, client_secret, redirect_uri) arrive.".to_string(),
97            required: false,
98        },
99        Parameter {
100            name: "configured_issuer".to_string(),
101            type_hint: "string".to_string(),
102            description: "Issuer set at startup or by a previous configure_provider, or null. \
103                          Use it in the discovery document and in `iss` claims so successive \
104                          responses agree."
105                .to_string(),
106            required: false,
107        },
108        Parameter {
109            name: "configured_scopes".to_string(),
110            type_hint: "array".to_string(),
111            description: "Scopes this provider is configured to support.".to_string(),
112            required: false,
113        },
114        Parameter {
115            name: "endpoint_type".to_string(),
116            type_hint: "string".to_string(),
117            description: "OIDC endpoint type: discovery, authorization, token, userinfo, jwks, or unknown".to_string(),
118            required: true,
119        },
120    ])
121});
122
123/// OpenID Connect protocol implementation
124#[derive(Clone)]
125pub struct OpenIdProtocol;
126
127impl OpenIdProtocol {
128    pub fn new() -> Self {
129        Self
130    }
131}
132
133impl Default for OpenIdProtocol {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139// Implement Protocol trait (common functionality)
140impl Protocol for OpenIdProtocol {
141    fn protocol_name(&self) -> &'static str {
142        "OpenID"
143    }
144    fn stack_name(&self) -> &'static str {
145        "ETH>IP>TCP>HTTP>OPENID"
146    }
147    fn keywords(&self) -> Vec<&'static str> {
148        vec!["openid", "oidc", "openid connect", "sso", "authentication"]
149    }
150    fn get_event_types(&self) -> Vec<EventType> {
151        vec![OPENID_REQUEST_EVENT.clone()]
152    }
153    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
154        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
155
156        ProtocolMetadataV2::builder()
157            .state(DevelopmentState::Experimental)
158            .implementation(
159                "Hyper HTTP/1.1 server that routes the five OIDC endpoints to the model. \
160                 Simulator: NetGet holds no signing key and performs no cryptography — an \
161                 id_token is whatever string the model returns, and the JWKS it serves is \
162                 whatever the model made up.",
163            )
164            .llm_control(
165                "Discovery document, authorization redirect, token/id_token strings, userinfo \
166                 claims, JWKS contents, and error responses.",
167            )
168            .e2e_testing("reqwest HTTP client")
169            .notes(
170                "Discovery, authorization, token, userinfo and JWKS endpoints are served, but no \
171                 ID token is signed and no access token is verified. Use it to exercise OIDC \
172                 relying parties, not to authenticate anyone.",
173            )
174            .max_inbound_bytes(crate::server::openid::MAX_REQUEST_BYTES)
175            .request_only("OpenID Connect is HTTP request/response; a server cannot send a peer anything unprompted")
176            .build()
177    }
178    fn description(&self) -> &'static str {
179        "OpenID Connect provider simulator (LLM-authored tokens; nothing is signed or verified)"
180    }
181    fn example_prompt(&self) -> &'static str {
182        "Start an OpenID Connect server for SSO on port 8080"
183    }
184    /// None. Every OIDC endpoint is request/response, so there is nothing an operator can
185    /// usefully initiate out of band.
186    ///
187    /// `configure_provider` used to live here and could never take effect: an async action's
188    /// `ActionResult` never reaches `handle_llm_response`, and `execute_action` is a `&self`
189    /// method on a unit struct with no access to `OpenIdState`, so the issuer it "set" was
190    /// discarded. It is a sync action now, offered on `openid_request`, where the handler
191    /// that owns the state can apply it.
192    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
193        Vec::new()
194    }
195    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
196        vec![
197                ActionDefinition {
198                    name: "send_discovery_document".to_string(),
199                    description: "Send OpenID Connect discovery document (/.well-known/openid-configuration)".to_string(),
200                    parameters: vec![
201                        Parameter {
202                            name: "issuer".to_string(),
203                            type_hint: "string".to_string(),
204                            description: "Issuer URL".to_string(),
205                            required: true,
206                        },
207                        Parameter {
208                            name: "authorization_endpoint".to_string(),
209                            type_hint: "string".to_string(),
210                            description: "Authorization endpoint URL".to_string(),
211                            required: true,
212                        },
213                        Parameter {
214                            name: "token_endpoint".to_string(),
215                            type_hint: "string".to_string(),
216                            description: "Token endpoint URL".to_string(),
217                            required: true,
218                        },
219                        Parameter {
220                            name: "userinfo_endpoint".to_string(),
221                            type_hint: "string".to_string(),
222                            description: "UserInfo endpoint URL".to_string(),
223                            required: true,
224                        },
225                        Parameter {
226                            name: "jwks_uri".to_string(),
227                            type_hint: "string".to_string(),
228                            description: "JWKS (JSON Web Key Set) endpoint URL".to_string(),
229                            required: true,
230                        },
231                        Parameter {
232                            name: "supported_scopes".to_string(),
233                            type_hint: "array".to_string(),
234                            description: "Supported OAuth scopes".to_string(),
235                            required: false,
236                        },
237                        Parameter {
238                            name: "supported_response_types".to_string(),
239                            type_hint: "array".to_string(),
240                            description: "Supported response types (e.g., [\"code\", \"id_token\", \"token id_token\"])".to_string(),
241                            required: false,
242                        },
243                        Parameter {
244                            name: "id_token_signing_alg_values_supported".to_string(),
245                            type_hint: "array".to_string(),
246                            description: "Signing algorithms to advertise for ID tokens (default: [\"RS256\"]). This is only an advertisement — NetGet never signs anything, so whatever you claim here must match the id_token strings and JWKS you return yourself.".to_string(),
247                            required: false,
248                        },
249                    ],
250                    example: serde_json::json!({
251                        "type": "send_discovery_document",
252                        "issuer": "http://localhost:8080",
253                        "authorization_endpoint": "http://localhost:8080/authorize",
254                        "token_endpoint": "http://localhost:8080/token",
255                        "userinfo_endpoint": "http://localhost:8080/userinfo",
256                        "jwks_uri": "http://localhost:8080/jwks.json",
257                        "supported_scopes": ["openid", "profile", "email"],
258                        "supported_response_types": ["code", "id_token", "token id_token"]
259                    }),
260                    log_template: Some(
261                        LogTemplate::new()
262                            .with_info("-> OIDC discovery: {issuer}")
263                            .with_debug("OIDC discovery document: issuer={issuer}"),
264                    ),
265                },
266                ActionDefinition {
267                    name: "send_authorization_response".to_string(),
268                    description: "Send authorization response (redirect with code or error)".to_string(),
269                    parameters: vec![
270                        Parameter {
271                            name: "redirect_uri".to_string(),
272                            type_hint: "string".to_string(),
273                            description: "Client redirect URI".to_string(),
274                            required: true,
275                        },
276                        Parameter {
277                            name: "code".to_string(),
278                            type_hint: "string".to_string(),
279                            description: "Authorization code (if successful)".to_string(),
280                            required: false,
281                        },
282                        Parameter {
283                            name: "state".to_string(),
284                            type_hint: "string".to_string(),
285                            description: "State parameter from request".to_string(),
286                            required: false,
287                        },
288                        Parameter {
289                            name: "error".to_string(),
290                            type_hint: "string".to_string(),
291                            description: "Error code (if failed, e.g., invalid_request, unauthorized_client)".to_string(),
292                            required: false,
293                        },
294                        Parameter {
295                            name: "error_description".to_string(),
296                            type_hint: "string".to_string(),
297                            description: "Human-readable error description".to_string(),
298                            required: false,
299                        },
300                    ],
301                    example: serde_json::json!({"type": "send_authorization_response", "redirect_uri": "https://client.example.com/callback", "code": "AUTH_CODE_123", "state": "xyz"}),
302                    log_template: Some(
303                        LogTemplate::new()
304                            .with_info("-> OIDC auth redirect")
305                            .with_debug("OIDC authorization response: redirect to {redirect_uri}"),
306                    ),
307                },
308                ActionDefinition {
309                    name: "send_token_response".to_string(),
310                    description: "Send token response (access_token, id_token, refresh_token)".to_string(),
311                    parameters: vec![
312                        Parameter {
313                            name: "access_token".to_string(),
314                            type_hint: "string".to_string(),
315                            description: "Access token (JWT or opaque)".to_string(),
316                            required: true,
317                        },
318                        Parameter {
319                            name: "token_type".to_string(),
320                            type_hint: "string".to_string(),
321                            description: "Token type (usually 'Bearer')".to_string(),
322                            required: true,
323                        },
324                        Parameter {
325                            name: "id_token".to_string(),
326                            type_hint: "string".to_string(),
327                            description: "ID token (JWT containing user claims)".to_string(),
328                            required: false,
329                        },
330                        Parameter {
331                            name: "refresh_token".to_string(),
332                            type_hint: "string".to_string(),
333                            description: "Refresh token (for obtaining new access tokens)".to_string(),
334                            required: false,
335                        },
336                        Parameter {
337                            name: "expires_in".to_string(),
338                            type_hint: "number".to_string(),
339                            description: "Token expiration time in seconds".to_string(),
340                            required: false,
341                        },
342                        Parameter {
343                            name: "scope".to_string(),
344                            type_hint: "string".to_string(),
345                            description: "Granted scopes (space-separated)".to_string(),
346                            required: false,
347                        },
348                    ],
349                    example: serde_json::json!({
350                        "type": "send_token_response",
351                        "access_token": "eyJhbGci...",
352                        "token_type": "Bearer",
353                        "id_token": "eyJhbGci...",
354                        "expires_in": 3600,
355                        "scope": "openid profile email"
356                    }),
357                    log_template: Some(
358                        LogTemplate::new()
359                            .with_info("-> OIDC token issued (expires={expires_in}s)")
360                            .with_debug("OIDC token response: type={token_type}, expires_in={expires_in}"),
361                    ),
362                },
363                ActionDefinition {
364                    name: "send_userinfo_response".to_string(),
365                    description: "Send user info response (user claims in JSON)".to_string(),
366                    parameters: vec![
367                        Parameter {
368                            name: "sub".to_string(),
369                            type_hint: "string".to_string(),
370                            description: "Subject identifier (unique user ID)".to_string(),
371                            required: true,
372                        },
373                        Parameter {
374                            name: "name".to_string(),
375                            type_hint: "string".to_string(),
376                            description: "Full name".to_string(),
377                            required: false,
378                        },
379                        Parameter {
380                            name: "email".to_string(),
381                            type_hint: "string".to_string(),
382                            description: "Email address".to_string(),
383                            required: false,
384                        },
385                        Parameter {
386                            name: "email_verified".to_string(),
387                            type_hint: "boolean".to_string(),
388                            description: "Email verified status".to_string(),
389                            required: false,
390                        },
391                        Parameter {
392                            name: "picture".to_string(),
393                            type_hint: "string".to_string(),
394                            description: "Profile picture URL".to_string(),
395                            required: false,
396                        },
397                        Parameter {
398                            name: "additional_claims".to_string(),
399                            type_hint: "object".to_string(),
400                            description: "Additional user claims as key-value pairs".to_string(),
401                            required: false,
402                        },
403                    ],
404                    example: serde_json::json!({
405                        "type": "send_userinfo_response",
406                        "sub": "user123",
407                        "name": "John Doe",
408                        "email": "john@example.com",
409                        "email_verified": true
410                    }),
411                    log_template: Some(
412                        LogTemplate::new()
413                            .with_info("-> OIDC userinfo: {sub}")
414                            .with_debug("OIDC userinfo response: sub={sub}, name={name}"),
415                    ),
416                },
417                ActionDefinition {
418                    name: "send_jwks_response".to_string(),
419                    description: "Send JWKS (JSON Web Key Set) response containing public keys for token verification".to_string(),
420                    parameters: vec![
421                        Parameter {
422                            name: "keys".to_string(),
423                            type_hint: "array".to_string(),
424                            description: "Array of JWK (JSON Web Key) objects. Each key should have: kty, use, kid, alg, n, e (for RSA)".to_string(),
425                            required: true,
426                        },
427                    ],
428                    example: serde_json::json!({
429                        "type": "send_jwks_response",
430                        "keys": [{
431                            "kty": "RSA",
432                            "use": "sig",
433                            "kid": "key1",
434                            "alg": "RS256",
435                            "n": "0vx7agoebGcQ...",
436                            "e": "AQAB"
437                        }]
438                    }),
439                    log_template: Some(
440                        LogTemplate::new()
441                            .with_info("-> OIDC JWKS ({keys_len} keys)")
442                            .with_debug("OIDC JWKS response: {keys_len} keys"),
443                    ),
444                },
445                ActionDefinition {
446                    name: "send_error_response".to_string(),
447                    description: "Send OAuth/OIDC error response".to_string(),
448                    parameters: vec![
449                        Parameter {
450                            name: "error".to_string(),
451                            type_hint: "string".to_string(),
452                            description: "Error code (e.g., invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type)".to_string(),
453                            required: true,
454                        },
455                        Parameter {
456                            name: "error_description".to_string(),
457                            type_hint: "string".to_string(),
458                            description: "Human-readable error description".to_string(),
459                            required: false,
460                        },
461                        Parameter {
462                            name: "status_code".to_string(),
463                            type_hint: "number".to_string(),
464                            description: "HTTP status code (default: 400)".to_string(),
465                            required: false,
466                        },
467                    ],
468                    example: serde_json::json!({"type": "send_error_response", "error": "invalid_client", "error_description": "Client authentication failed", "status_code": 401}),
469                    log_template: Some(
470                        LogTemplate::new()
471                            .with_info("-> OIDC error: {error}")
472                            .with_debug("OIDC error response: {error} - {error_description}"),
473                    ),
474                },
475                ActionDefinition {
476                    name: "configure_provider".to_string(),
477                    description: "Set the provider's issuer and supported scopes for the rest of this server's life. \
478                                  Produces no HTTP response of its own, so pair it with the action that answers \
479                                  this request; the values come back as `configured_issuer` / `configured_scopes` \
480                                  on every later openid_request.".to_string(),
481                    parameters: vec![
482                        Parameter {
483                            name: "issuer".to_string(),
484                            type_hint: "string".to_string(),
485                            description: "Issuer URL (e.g., http://localhost:8080)".to_string(),
486                            required: true,
487                        },
488                        Parameter {
489                            name: "supported_scopes".to_string(),
490                            type_hint: "array".to_string(),
491                            description: "Array of supported OAuth scopes (e.g., [\"openid\", \"profile\", \"email\"])".to_string(),
492                            required: false,
493                        },
494                    ],
495                    example: serde_json::json!({"type": "configure_provider", "issuer": "http://localhost:8080", "supported_scopes": ["openid", "profile", "email"]}),
496                    log_template: Some(
497                        LogTemplate::new()
498                            .with_info("-> OIDC configured: {issuer}")
499                            .with_debug("OIDC provider configured: issuer={issuer}"),
500                    ),
501                },
502            ]
503    }
504    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
505        vec![
506            ParameterDefinition {
507                name: "issuer".to_string(),
508                type_hint: "string".to_string(),
509                description:
510                    "Issuer URL for the OpenID Connect provider (e.g., http://localhost:8080)"
511                        .to_string(),
512                required: false,
513                example: serde_json::json!("http://localhost:8080"),
514                default: None,
515            },
516            ParameterDefinition {
517                name: "supported_scopes".to_string(),
518                type_hint: "array".to_string(),
519                description:
520                    "Supported OAuth scopes (default: [\"openid\", \"profile\", \"email\"])"
521                        .to_string(),
522                required: false,
523                example: serde_json::json!(["openid", "profile", "email"]),
524                default: None,
525            },
526        ]
527    }
528    fn group_name(&self) -> &'static str {
529        "Authentication"
530    }
531    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
532        use crate::llm::actions::StartupExamples;
533
534        StartupExamples::new(
535            // LLM mode: instruction-based
536            json!({
537                "type": "open_server",
538                "port": 8080,
539                "base_stack": "openid",
540                "instruction": "OpenID Connect provider. Handle discovery, authorization, token, and userinfo endpoints. Issue JWT tokens with proper claims"
541            }),
542            // Script mode: event_handlers with script handler
543            json!({
544                "type": "open_server",
545                "port": 8080,
546                "base_stack": "openid",
547                "event_handlers": [{
548                    "event_pattern": "openid_request",
549                    "handler": {
550                        "type": "script",
551                        "language": "python",
552                        "code": "endpoint = event.get('endpoint_type', '')\nif endpoint == 'discovery':\n    action('send_discovery_document', issuer='http://localhost:8080', authorization_endpoint='http://localhost:8080/authorize', token_endpoint='http://localhost:8080/token', userinfo_endpoint='http://localhost:8080/userinfo', jwks_uri='http://localhost:8080/jwks.json')\nelif endpoint == 'userinfo':\n    action('send_userinfo_response', sub='user123', name='Test User', email='test@example.com')\nelse:\n    action('send_error_response', error='unsupported_endpoint')"
553                    }
554                }]
555            }),
556            // Static mode: event_handlers with static actions
557            json!({
558                "type": "open_server",
559                "port": 8080,
560                "base_stack": "openid",
561                "event_handlers": [{
562                    "event_pattern": "openid_request",
563                    "handler": {
564                        "type": "static",
565                        "actions": [{
566                            "type": "send_discovery_document",
567                            "issuer": "http://localhost:8080",
568                            "authorization_endpoint": "http://localhost:8080/authorize",
569                            "token_endpoint": "http://localhost:8080/token",
570                            "userinfo_endpoint": "http://localhost:8080/userinfo",
571                            "jwks_uri": "http://localhost:8080/jwks.json"
572                        }]
573                    }
574                }]
575            }),
576        )
577    }
578}
579
580// Implement Server trait (server-specific functionality)
581impl Server for OpenIdProtocol {
582    fn spawn(
583        &self,
584        ctx: crate::protocol::SpawnContext,
585    ) -> std::pin::Pin<
586        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
587    > {
588        Box::pin(async move {
589            use crate::server::openid::OpenIdServer;
590            OpenIdServer::spawn_with_llm_actions(
591                ctx.legacy_listen_addr(),
592                ctx.llm_client,
593                ctx.state,
594                ctx.status_tx,
595                ctx.server_id,
596                ctx.startup_params,
597            )
598            .await
599        })
600    }
601    fn execute_action(&self, action: JsonValue) -> Result<ActionResult> {
602        let action_type = action["type"]
603            .as_str()
604            .ok_or_else(|| anyhow!("Missing action type"))?;
605
606        match action_type {
607            "send_discovery_document" => {
608                let issuer = action["issuer"]
609                    .as_str()
610                    .ok_or_else(|| anyhow!("Missing issuer parameter"))?;
611
612                let authorization_endpoint = action["authorization_endpoint"]
613                    .as_str()
614                    .ok_or_else(|| anyhow!("Missing authorization_endpoint parameter"))?;
615
616                let token_endpoint = action["token_endpoint"]
617                    .as_str()
618                    .ok_or_else(|| anyhow!("Missing token_endpoint parameter"))?;
619
620                let userinfo_endpoint = action["userinfo_endpoint"]
621                    .as_str()
622                    .ok_or_else(|| anyhow!("Missing userinfo_endpoint parameter"))?;
623
624                let jwks_uri = action["jwks_uri"]
625                    .as_str()
626                    .ok_or_else(|| anyhow!("Missing jwks_uri parameter"))?;
627
628                debug!("OpenID discovery document generated");
629
630                // Carry the optional fields only when the model actually supplied them.
631                // `action.get(k)` yields `Some(Value::Null)` for a key that is present but
632                // null and serializes an absent key as `null` too, so the downstream
633                // `.unwrap_or(default)` never fired: a discovery document that omitted
634                // `supported_response_types` went out with the REQUIRED
635                // `"response_types_supported": null`, which relying parties reject.
636                let mut data = serde_json::json!({
637                    "issuer": issuer,
638                    "authorization_endpoint": authorization_endpoint,
639                    "token_endpoint": token_endpoint,
640                    "userinfo_endpoint": userinfo_endpoint,
641                    "jwks_uri": jwks_uri,
642                });
643                for key in [
644                    "supported_scopes",
645                    "supported_response_types",
646                    "id_token_signing_alg_values_supported",
647                ] {
648                    if let Some(value) = action.get(key).filter(|v| !v.is_null()) {
649                        data[key] = value.clone();
650                    }
651                }
652
653                Ok(ActionResult::Custom {
654                    name: "send_discovery_document".to_string(),
655                    data,
656                })
657            }
658            "send_authorization_response" => {
659                let redirect_uri = action["redirect_uri"]
660                    .as_str()
661                    .ok_or_else(|| anyhow!("Missing redirect_uri parameter"))?;
662
663                info!(
664                    "OpenID authorization response: redirect to {}",
665                    redirect_uri
666                );
667
668                Ok(ActionResult::Custom {
669                    name: "send_authorization_response".to_string(),
670                    data: serde_json::json!({
671                        "redirect_uri": redirect_uri,
672                        "code": action.get("code"),
673                        "state": action.get("state"),
674                        "error": action.get("error"),
675                        "error_description": action.get("error_description"),
676                    }),
677                })
678            }
679            "send_token_response" => {
680                let _access_token = action["access_token"]
681                    .as_str()
682                    .ok_or_else(|| anyhow!("Missing access_token parameter"))?;
683
684                debug!("OpenID token response generated");
685
686                Ok(ActionResult::Custom {
687                    name: "send_token_response".to_string(),
688                    data: action.clone(),
689                })
690            }
691            "send_userinfo_response" => {
692                let sub = action["sub"]
693                    .as_str()
694                    .ok_or_else(|| anyhow!("Missing sub parameter"))?;
695
696                debug!("OpenID userinfo response for subject: {}", sub);
697
698                Ok(ActionResult::Custom {
699                    name: "send_userinfo_response".to_string(),
700                    data: action.clone(),
701                })
702            }
703            "send_jwks_response" => {
704                debug!("OpenID JWKS response generated");
705
706                Ok(ActionResult::Custom {
707                    name: "send_jwks_response".to_string(),
708                    data: action.clone(),
709                })
710            }
711            "send_error_response" => {
712                let error = action["error"]
713                    .as_str()
714                    .ok_or_else(|| anyhow!("Missing error parameter"))?;
715
716                warn!("OpenID error response: {}", error);
717
718                Ok(ActionResult::Custom {
719                    name: "send_error_response".to_string(),
720                    data: action.clone(),
721                })
722            }
723            "configure_provider" => {
724                info!("OpenID provider configuration updated");
725
726                Ok(ActionResult::Custom {
727                    name: "configure_provider".to_string(),
728                    data: action.clone(),
729                })
730            }
731            _ => {
732                error!("Unknown OpenID action: {}", action_type);
733                Err(anyhow!("Unknown action type: {}", action_type))
734            }
735        }
736    }
737}