1use 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
15pub 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 .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#[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
139impl 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 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 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 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 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
580impl 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 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}