1pub mod actions;
3
4pub use actions::OpenIdConnectClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::sync::mpsc;
10use tracing::{error, info};
11use urlencoding;
12
13use crate::client::http_fetch::FetchClient;
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::openidconnect::actions::{
16 OIDC_CLIENT_DISCOVERED_EVENT, OIDC_CLIENT_TOKEN_RECEIVED_EVENT,
17 OIDC_CLIENT_USERINFO_RECEIVED_EVENT,
18};
19use crate::llm::actions::client_trait::ClientActionResult;
20use crate::llm::ollama_client::OllamaClient;
21use crate::llm::ClientLlmResult;
22use crate::logging::emit::Log;
23use crate::protocol::Event;
24use crate::state::app_state::AppState;
25use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
26use crate::state::{AccessLogOwner, ClientId, ClientStatus};
27
28#[cfg(not(target_arch = "wasm32"))]
31use openidconnect::reqwest::async_http_client as http_hook;
32
33#[cfg(target_arch = "wasm32")]
39async fn http_hook(
40 request: openidconnect::HttpRequest,
41) -> std::result::Result<openidconnect::HttpResponse, crate::client::http_fetch::FetchError> {
42 let client = FetchClient::transport(std::time::Duration::from_secs(30));
43 let headers = request
44 .headers
45 .iter()
46 .map(|(name, value)| (name.as_str().to_string(), value.as_bytes().to_vec()))
47 .collect();
48 let (status, headers, body) = crate::client::http_fetch::round_trip_parts(
49 &client,
50 request.method.as_str(),
51 request.url.as_str(),
52 headers,
53 request.body,
54 )
55 .await?;
56 let mut map = openidconnect::http::HeaderMap::new();
57 for (name, value) in headers {
58 if let (Ok(name), Ok(value)) = (
59 openidconnect::http::header::HeaderName::from_bytes(name.as_bytes()),
60 openidconnect::http::HeaderValue::from_bytes(&value),
61 ) {
62 map.append(name, value);
63 }
64 }
65 let status_code = openidconnect::http::StatusCode::from_u16(status).map_err(|e| {
66 crate::client::http_fetch::FetchError(anyhow::anyhow!("status {status}: {e}"))
67 })?;
68 Ok(openidconnect::HttpResponse {
69 status_code,
70 headers: map,
71 body: body.to_vec(),
72 })
73}
74
75fn plain_http_client() -> FetchClient {
79 #[cfg(not(target_arch = "wasm32"))]
80 let client = FetchClient::from_reqwest(reqwest::Client::new());
81 #[cfg(target_arch = "wasm32")]
82 let client = FetchClient::transport(std::time::Duration::from_secs(30));
83 client
84}
85
86use openidconnect::{
87 core::{CoreClient, CoreProviderMetadata, CoreTokenResponse, CoreUserInfoClaims},
88 ClientId as OidcClientId, ClientSecret, IssuerUrl, OAuth2TokenResponse, ResourceOwnerPassword,
89 ResourceOwnerUsername, Scope,
90};
91
92enum Applied {
99 Ran(String),
101 Disconnect,
103}
104
105#[derive(Clone, Copy)]
107enum Dispatch {
108 Inline,
110 Deferred,
114}
115
116pub struct OpenIdConnectClient;
118
119impl OpenIdConnectClient {
120 pub async fn connect_with_llm_actions(
122 remote_addr: String,
123 llm_client: OllamaClient,
124 app_state: Arc<AppState>,
125 status_tx: mpsc::UnboundedSender<String>,
126 client_id: ClientId,
127 startup_params: Option<crate::protocol::StartupParams>,
128 ) -> Result<SocketAddr> {
129 info!(
130 "OpenID Connect client {} initializing for {}",
131 client_id, remote_addr
132 );
133
134 let (flow, startup_scopes) = match &startup_params {
143 Some(params) => (
144 params.get_optional_string("flow")?,
145 params.get_optional_string("scopes")?,
146 ),
147 None => (None, None),
148 };
149 let flow_action = match flow.as_deref() {
150 None => None,
151 Some("device_code") => Some(serde_json::json!({
152 "type": "start_device_flow", "scopes": startup_scopes })),
153 Some("authorization_code") => Some(serde_json::json!({
154 "type": "start_authorization_code_flow", "scopes": startup_scopes })),
155 Some("client_credentials") => Some(serde_json::json!({
156 "type": "exchange_client_credentials", "scopes": startup_scopes })),
157 Some("password") => {
158 return Err(anyhow::anyhow!(
159 "OpenID Connect client: `flow: \"password\"` cannot be started from \
160 startup parameters - the resource-owner password flow needs a username \
161 and a password, and this client only accepts those on the \
162 `exchange_password` action. Drive it with that action (the dashboard's \
163 [ exchange_password ] row, or an instruction telling the model to use \
164 it) and leave `flow` unset."
165 ));
166 }
167 Some(other) => {
168 return Err(anyhow::anyhow!(
169 "OpenID Connect client: unknown `flow` value '{other}'. Expected one of \
170 device_code, authorization_code, client_credentials (password is \
171 action-driven; see the error for flow=\"password\")."
172 ));
173 }
174 };
175
176 crate::client::http_fetch::check_url(&remote_addr)?;
179
180 app_state
186 .with_client_mut(client_id, |client| {
187 if let Some(serde_json::Value::Object(params)) = client.startup_params.clone() {
188 for (key, value) in params {
189 if client.get_protocol_field(&key).is_none() {
190 client.set_protocol_field(key, value);
191 }
192 }
193 }
194 client
195 .set_protocol_field("provider_url".to_string(), serde_json::json!(remote_addr));
196 })
197 .await;
198
199 app_state
201 .update_client_status(client_id, ClientStatus::Connected)
202 .await;
203 Log::new(Some(&status_tx)).info(format!(
204 "OpenID Connect client {} ready for {}",
205 client_id, remote_addr
206 ));
207 let _ = status_tx.send("__UPDATE_UI__".to_string());
208
209 let command_rx =
217 crate::client::command_support::register_command_channel(&app_state, client_id).await;
218 let cmd_task = tokio::spawn(Self::command_loop(
219 command_rx,
220 client_id,
221 app_state.clone(),
222 llm_client.clone(),
223 status_tx.clone(),
224 ));
225 app_state.register_client_task(client_id, cmd_task).await;
226
227 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
229 let protocol = Arc::new(OpenIdConnectClientProtocol::new());
230
231 if let Err(e) = Self::discover_and_call_llm(
233 &remote_addr,
234 client_id,
235 &llm_client,
236 &app_state,
237 &status_tx,
238 &instruction,
239 protocol,
240 )
241 .await
242 {
243 Log::new(Some(&status_tx))
244 .error(format!("Failed to discover OIDC configuration: {}", e));
245 }
246 }
247
248 if let Some(action) = flow_action {
256 let flow_state = app_state.clone();
257 let flow_llm = llm_client.clone();
258 let flow_status = status_tx.clone();
259 let flow_task = tokio::spawn(async move {
260 let protocol = Arc::new(OpenIdConnectClientProtocol::new());
261 if let Err(e) = Self::execute_llm_action(
262 action,
263 client_id,
264 &flow_llm,
265 &flow_state,
266 &flow_status,
267 protocol,
268 )
269 .await
270 {
271 Log::new(Some(&flow_status)).error(format!(
272 "OpenID Connect client {}: the `flow` startup parameter's flow failed: {}",
273 client_id, e
274 ));
275 }
276 });
277 app_state.register_client_task(client_id, flow_task).await;
278 }
279
280 Ok("0.0.0.0:0".parse().unwrap())
282 }
283
284 async fn discover_and_call_llm(
286 provider_url: &str,
287 client_id: ClientId,
288 llm_client: &OllamaClient,
289 app_state: &Arc<AppState>,
290 status_tx: &mpsc::UnboundedSender<String>,
291 instruction: &str,
292 protocol: Arc<OpenIdConnectClientProtocol>,
293 ) -> Result<()> {
294 info!("Discovering OIDC configuration for {}", provider_url);
295
296 let issuer_url = IssuerUrl::new(provider_url.to_string()).context("Invalid issuer URL")?;
298
299 let provider_metadata = CoreProviderMetadata::discover_async(issuer_url.clone(), http_hook)
300 .await
301 .context("Failed to discover OIDC provider metadata")?;
302
303 Log::new(Some(status_tx))
304 .info(format!("Discovered OIDC provider: {}", issuer_url.as_str()));
305
306 app_state
308 .with_client_mut(client_id, |client| {
309 client.set_protocol_field(
310 "provider_metadata".to_string(),
311 serde_json::json!({
312 "issuer": issuer_url.as_str(),
313 "authorization_endpoint": provider_metadata.authorization_endpoint().as_str(),
314 "token_endpoint": provider_metadata.token_endpoint().map(|u| u.as_str()),
315 "userinfo_endpoint": provider_metadata.userinfo_endpoint().map(|u| u.as_str()),
316 }),
317 );
318 })
319 .await;
320
321 let event = Event::new(
323 &OIDC_CLIENT_DISCOVERED_EVENT,
324 serde_json::json!({
325 "issuer": issuer_url.as_str(),
326 "authorization_endpoint": provider_metadata.authorization_endpoint().as_str(),
327 "token_endpoint": provider_metadata.token_endpoint().map(|u| u.as_str()),
328 "userinfo_endpoint": provider_metadata.userinfo_endpoint().map(|u| u.as_str()),
329 "supported_scopes": provider_metadata.scopes_supported()
330 .map(|scopes| scopes.iter().map(|s| s.as_str()).collect::<Vec<_>>()),
331 }),
332 );
333
334 let memory = app_state
335 .get_memory_for_client(client_id)
336 .await
337 .unwrap_or_default();
338
339 match call_llm_for_client(
340 llm_client,
341 app_state,
342 client_id.to_string(),
343 instruction,
344 &memory,
345 Some(&event),
346 protocol.as_ref(),
347 status_tx,
348 )
349 .await
350 {
351 Ok(ClientLlmResult {
352 actions,
353 memory_updates,
354 }) => {
355 if let Some(mem) = memory_updates {
357 app_state.set_memory_for_client(client_id, mem).await;
358 }
359
360 for action in actions {
362 if let Err(e) = Self::execute_llm_action(
363 action,
364 client_id,
365 llm_client,
366 app_state,
367 status_tx,
368 protocol.clone(),
369 )
370 .await
371 {
372 Log::new(Some(status_tx))
373 .error(format!("Failed to execute OIDC action: {}", e));
374 }
375 }
376 }
377 Err(e) => {
378 error!("LLM error for OIDC client {}: {}", client_id, e);
379 }
380 }
381
382 Ok(())
383 }
384
385 fn execute_llm_action<'a>(
390 action: serde_json::Value,
391 client_id: ClientId,
392 llm_client: &'a OllamaClient,
393 app_state: &'a Arc<AppState>,
394 status_tx: &'a mpsc::UnboundedSender<String>,
395 protocol: Arc<OpenIdConnectClientProtocol>,
396 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
397 Box::pin(async move {
398 use crate::llm::actions::Client;
399
400 let result = protocol.execute_action(action.clone())?;
401 match Self::apply_action(
402 result,
403 Dispatch::Inline,
404 client_id,
405 llm_client,
406 app_state,
407 status_tx,
408 protocol,
409 )
410 .await?
411 {
412 Applied::Ran(detail) => info!("OIDC client {}: {}", client_id, detail),
413 Applied::Disconnect => {
414 app_state.remove_client(client_id).await;
419 }
420 }
421 Ok(())
422 })
423 }
424
425 #[allow(clippy::too_many_arguments)]
428 fn apply_action<'a>(
429 result: ClientActionResult,
430 dispatch: Dispatch,
431 client_id: ClientId,
432 llm_client: &'a OllamaClient,
433 app_state: &'a Arc<AppState>,
434 status_tx: &'a mpsc::UnboundedSender<String>,
435 protocol: Arc<OpenIdConnectClientProtocol>,
436 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Applied>> + Send + 'a>> {
437 Box::pin(async move {
438 match result {
439 ClientActionResult::Custom { name, data } => match name.as_str() {
440 "oidc_discover" => {
441 let provider_url = app_state
442 .with_client_mut(client_id, |client| {
443 client
444 .get_protocol_field("provider_url")
445 .and_then(|v| v.as_str())
446 .map(|s| s.to_string())
447 })
448 .await
449 .flatten()
450 .context("No provider URL found")?;
451 let instruction = app_state
452 .get_instruction_for_client(client_id)
453 .await
454 .unwrap_or_default();
455 Self::discover_and_call_llm(
456 &provider_url,
457 client_id,
458 llm_client,
459 app_state,
460 status_tx,
461 &instruction,
462 protocol,
463 )
464 .await?;
465 Ok(Applied::Ran(format!(
466 "oidc_discover: provider metadata fetched from {provider_url}"
467 )))
468 }
469 "oidc_device_flow" => {
470 Self::start_device_flow(
471 client_id, data, llm_client, app_state, status_tx, protocol,
472 )
473 .await?;
474 Ok(Applied::Ran(
475 "oidc_device_flow: device code issued, polling started".to_string(),
476 ))
477 }
478 "oidc_authorization_code" => {
479 Self::start_authorization_code_flow(
480 client_id, data, llm_client, app_state, status_tx, protocol,
481 )
482 .await?;
483 Ok(Applied::Ran(
484 "oidc_authorization_code: authorization URL built, callback server \
485 waiting"
486 .to_string(),
487 ))
488 }
489 "oidc_password_flow" => {
490 Self::exchange_password(
491 client_id, data, llm_client, app_state, status_tx, protocol, dispatch,
492 )
493 .await?;
494 Ok(Applied::Ran(
495 Self::token_state_detail("oidc_password_flow", client_id, app_state)
496 .await,
497 ))
498 }
499 "oidc_client_credentials" => {
500 Self::exchange_client_credentials(
501 client_id, data, llm_client, app_state, status_tx, protocol, dispatch,
502 )
503 .await?;
504 Ok(Applied::Ran(
505 Self::token_state_detail(
506 "oidc_client_credentials",
507 client_id,
508 app_state,
509 )
510 .await,
511 ))
512 }
513 "oidc_refresh_token" => {
514 Self::refresh_token(
515 client_id, llm_client, app_state, status_tx, protocol, dispatch,
516 )
517 .await?;
518 Ok(Applied::Ran(
519 Self::token_state_detail("oidc_refresh_token", client_id, app_state)
520 .await,
521 ))
522 }
523 "oidc_fetch_userinfo" => {
524 Self::fetch_userinfo(
525 client_id, llm_client, app_state, status_tx, protocol, dispatch,
526 )
527 .await?;
528 Ok(Applied::Ran(
529 "oidc_fetch_userinfo: UserInfo endpoint answered".to_string(),
530 ))
531 }
532 other => Err(anyhow::anyhow!("Unknown OIDC action: {}", other)),
533 },
534 ClientActionResult::Disconnect => {
535 info!("OIDC client {} disconnecting", client_id);
536 app_state
537 .update_client_status(client_id, ClientStatus::Disconnected)
538 .await;
539 let _ = status_tx.send("__UPDATE_UI__".to_string());
540 Ok(Applied::Disconnect)
541 }
542 _ => Err(anyhow::anyhow!("Unsupported action result type")),
543 }
544 })
545 }
546
547 async fn token_state_detail(
549 name: &str,
550 client_id: ClientId,
551 app_state: &Arc<AppState>,
552 ) -> String {
553 let has_token = app_state
554 .with_client_mut(client_id, |client| {
555 client.get_protocol_field("access_token").is_some()
556 })
557 .await
558 .unwrap_or(false);
559 if has_token {
560 format!("{name}: completed, an access token is stored")
561 } else {
562 format!(
563 "{name}: completed but no access token was stored - the provider did not \
564 issue one (see netget.log)"
565 )
566 }
567 }
568
569 async fn command_loop(
576 mut command_rx: mpsc::Receiver<ClientCommand>,
577 client_id: ClientId,
578 app_state: Arc<AppState>,
579 llm_client: OllamaClient,
580 status_tx: mpsc::UnboundedSender<String>,
581 ) {
582 use crate::llm::actions::protocol_trait::Protocol;
583 use crate::llm::actions::Client;
584
585 let protocol = Arc::new(OpenIdConnectClientProtocol::new());
586
587 while let Some(command) = command_rx.recv().await {
588 let action = command.action.clone();
589 let outcome = match protocol.execute_action(action.clone()) {
590 Err(e) => Ok(ClientSendOutcome::Rejected {
591 error: e.to_string(),
592 }),
593 Ok(result) => Self::apply_action(
594 result,
595 Dispatch::Deferred,
596 client_id,
597 &llm_client,
598 &app_state,
599 &status_tx,
600 protocol.clone(),
601 )
602 .await
603 .map(|applied| match applied {
604 Applied::Disconnect => ClientSendOutcome::Disconnected,
605 Applied::Ran(detail) => ClientSendOutcome::Executed { detail },
606 }),
607 };
608
609 let outcome_json = match &outcome {
610 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
611 Err(e) => serde_json::json!({"error": e.to_string()}),
612 };
613 app_state
614 .record_access_log(
615 AccessLogOwner::Client(client_id.as_u32()),
616 protocol.protocol_name(),
617 None,
618 "injected_action",
619 action,
620 vec![outcome_json],
621 )
622 .await;
623
624 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
625 if let Err(e) = &outcome {
626 error!("OIDC client {} injected action failed: {}", client_id, e);
627 let _ = status_tx.send(format!(
628 "[WARN] Client {} injected action failed: {}",
629 client_id, e
630 ));
631 }
632 let _ = status_tx.send("__UPDATE_UI__".to_string());
633 crate::client::command_support::reply(command, outcome);
634
635 if disconnect {
636 break;
637 }
638 }
639
640 app_state.remove_client_handle(client_id).await;
641 let _ = status_tx.send("__UPDATE_UI__".to_string());
642 info!("OIDC client {} command loop ended", client_id);
643 }
644
645 async fn start_device_flow(
647 client_id: ClientId,
648 data: serde_json::Value,
649 llm_client: &OllamaClient,
650 app_state: &Arc<AppState>,
651 status_tx: &mpsc::UnboundedSender<String>,
652 protocol: Arc<OpenIdConnectClientProtocol>,
653 ) -> Result<()> {
654 Log::new(Some(status_tx)).info(format!(
655 "Starting device code flow for client {}",
656 client_id
657 ));
658
659 let (provider_url, oidc_client_id, oidc_client_secret) = app_state
661 .with_client_mut(client_id, |client| {
662 let provider = client
663 .get_protocol_field("provider_url")
664 .and_then(|v| v.as_str())
665 .map(|s| s.to_string())?;
666 let client_id_str = client
667 .get_protocol_field("client_id")
668 .and_then(|v| v.as_str())
669 .map(|s| s.to_string())
670 .unwrap_or_else(|| "default-client-id".to_string());
671 let client_secret_str = client
672 .get_protocol_field("client_secret")
673 .and_then(|v| v.as_str())
674 .map(|s| s.to_string());
675 Some((provider, client_id_str, client_secret_str))
676 })
677 .await
678 .flatten()
679 .context("No provider URL found")?;
680
681 let scopes = data
682 .get("scopes")
683 .and_then(|v| v.as_str())
684 .unwrap_or("openid");
685
686 let issuer_url = IssuerUrl::new(provider_url)?;
687 let provider_metadata =
688 CoreProviderMetadata::discover_async(issuer_url.clone(), http_hook).await?;
689
690 let device_auth_url = format!("{}/device/code", issuer_url.as_str().trim_end_matches('/'));
692 Log::new(Some(status_tx)).info(format!(
693 "Device authorization endpoint: {}",
694 device_auth_url
695 ));
696
697 let mut params = vec![("client_id", oidc_client_id.as_str()), ("scope", scopes)];
699
700 if let Some(ref secret) = oidc_client_secret {
701 params.push(("client_secret", secret.as_str()));
702 }
703
704 let http_client = plain_http_client();
706 let response = http_client
707 .post(&device_auth_url)
708 .form(¶ms)
709 .send()
710 .await
711 .context("Failed to send device authorization request")?;
712
713 if !response.status().is_success() {
714 let error_text = response
715 .text()
716 .await
717 .unwrap_or_else(|_| "Unknown error".to_string());
718 return Err(anyhow::anyhow!(
719 "Device authorization failed: {}",
720 error_text
721 ));
722 }
723
724 let device_response: serde_json::Value = response
725 .json()
726 .await
727 .context("Failed to parse device authorization response")?;
728
729 let device_code = device_response
731 .get("device_code")
732 .and_then(|v| v.as_str())
733 .context("Missing device_code in response")?
734 .to_string();
735
736 let user_code = device_response
737 .get("user_code")
738 .and_then(|v| v.as_str())
739 .context("Missing user_code in response")?
740 .to_string();
741
742 let verification_uri = device_response
743 .get("verification_uri")
744 .and_then(|v| v.as_str())
745 .context("Missing verification_uri in response")?
746 .to_string();
747
748 let verification_uri_complete = device_response
749 .get("verification_uri_complete")
750 .and_then(|v| v.as_str())
751 .map(|s| s.to_string());
752
753 let interval = device_response
754 .get("interval")
755 .and_then(|v| v.as_u64())
756 .unwrap_or(5);
757
758 let expires_in = device_response
759 .get("expires_in")
760 .and_then(|v| v.as_u64())
761 .unwrap_or(300);
762
763 let log = Log::new(Some(status_tx));
765 let _ = status_tx.send("========================================".to_string());
766 log.info("Device Code Flow - User Action Required".to_string());
767 let _ = status_tx.send("========================================".to_string());
768 log.info("1. Open this URL in your browser:".to_string());
769 log.info(format!(" {}", verification_uri));
770 if let Some(complete_uri) = verification_uri_complete {
771 log.info(format!(" Or use this direct link: {}", complete_uri));
772 }
773 log.info(format!("2. Enter this code: {}", user_code));
774 let _ = status_tx.send("========================================".to_string());
775 log.info("Waiting for authorization...".to_string());
776
777 let token_endpoint = provider_metadata
779 .token_endpoint()
780 .context("No token endpoint in provider metadata")?
781 .as_str()
782 .to_string();
783
784 let app_state_clone = app_state.clone();
786 let llm_client_clone = llm_client.clone();
787 let status_tx_clone = status_tx.clone();
788 let protocol_clone = protocol.clone();
789 let oidc_client_id_clone = oidc_client_id.clone();
790 let oidc_client_secret_clone = oidc_client_secret.clone();
791
792 let task_registrar = app_state.clone();
795 let task_handle = tokio::spawn(async move {
796 let start_time = crate::utils::clock::Instant::now();
797 let mut poll_count = 0;
798 let interval_duration = std::time::Duration::from_secs(interval);
799 let expires_duration = std::time::Duration::from_secs(expires_in);
800 let log = Log::new(Some(&status_tx_clone));
801
802 loop {
803 if start_time.elapsed() > expires_duration {
805 log.error("Device code expired. Please try again.".to_string());
806 break;
807 }
808
809 tokio::time::sleep(interval_duration).await;
811 poll_count += 1;
812
813 log.info(format!(
814 "Polling for authorization (attempt {})...",
815 poll_count
816 ));
817
818 let mut token_params = vec![
820 ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
821 ("device_code", device_code.as_str()),
822 ("client_id", oidc_client_id_clone.as_str()),
823 ];
824
825 if let Some(ref secret) = oidc_client_secret_clone {
826 token_params.push(("client_secret", secret.as_str()));
827 }
828
829 let http_client = plain_http_client();
831 match http_client
832 .post(&token_endpoint)
833 .form(&token_params)
834 .send()
835 .await
836 {
837 Ok(response) => {
838 if response.status().is_success() {
839 match response.json::<serde_json::Value>().await {
841 Ok(token_json) => {
842 log.info(
843 "Authorization successful! Received tokens.".to_string(),
844 );
845
846 if let Err(e) = Self::store_tokens_from_json(
848 client_id,
849 &token_json,
850 &llm_client_clone,
851 &app_state_clone,
852 &status_tx_clone,
853 protocol_clone,
854 )
855 .await
856 {
857 log.error(format!("Failed to store tokens: {}", e));
858 }
859 break;
860 }
861 Err(e) => {
862 log.error(format!("Failed to parse token response: {}", e));
863 break;
864 }
865 }
866 } else {
867 match response.json::<serde_json::Value>().await {
869 Ok(error_json) => {
870 let error_code = error_json
871 .get("error")
872 .and_then(|v| v.as_str())
873 .unwrap_or("unknown");
874
875 match error_code {
876 "authorization_pending" => {
877 continue;
879 }
880 "slow_down" => {
881 log.info("Slowing down polling rate...".to_string());
883 tokio::time::sleep(interval_duration).await;
884 continue;
885 }
886 "expired_token" => {
887 log.error("Device code expired.".to_string());
888 break;
889 }
890 "access_denied" => {
891 log.error("User denied authorization.".to_string());
892 break;
893 }
894 _ => {
895 log.error(format!(
896 "Authorization error: {}",
897 error_code
898 ));
899 break;
900 }
901 }
902 }
903 Err(_) => {
904 log.error("Failed to parse error response".to_string());
905 break;
906 }
907 }
908 }
909 }
910 Err(e) => {
911 log.error(format!("Polling failed: {}", e));
912 break;
913 }
914 }
915 }
916 });
917 task_registrar
918 .register_client_task(client_id, task_handle)
919 .await;
920
921 Ok(())
922 }
923
924 async fn store_tokens_from_json(
926 client_id: ClientId,
927 token_json: &serde_json::Value,
928 llm_client: &OllamaClient,
929 app_state: &Arc<AppState>,
930 status_tx: &mpsc::UnboundedSender<String>,
931 protocol: Arc<OpenIdConnectClientProtocol>,
932 ) -> Result<()> {
933 let access_token = token_json
934 .get("access_token")
935 .and_then(|v| v.as_str())
936 .context("Missing access_token")?;
937 let id_token = token_json
938 .get("id_token")
939 .and_then(|v| v.as_str())
940 .map(|s| s.to_string());
941 let refresh_token = token_json
942 .get("refresh_token")
943 .and_then(|v| v.as_str())
944 .map(|s| s.to_string());
945 let expires_in = token_json.get("expires_in").and_then(|v| v.as_u64());
946 let token_type = token_json
947 .get("token_type")
948 .and_then(|v| v.as_str())
949 .unwrap_or("Bearer");
950
951 Log::new(Some(status_tx)).info(format!("Received tokens (expires_in: {:?}s)", expires_in));
952
953 app_state
955 .with_client_mut(client_id, |client| {
956 client.set_protocol_field(
957 "access_token".to_string(),
958 serde_json::json!(access_token),
959 );
960 if let Some(id) = &id_token {
961 client.set_protocol_field("id_token".to_string(), serde_json::json!(id));
962 }
963 if let Some(refresh) = &refresh_token {
964 client.set_protocol_field(
965 "refresh_token".to_string(),
966 serde_json::json!(refresh),
967 );
968 }
969 })
970 .await;
971
972 let event = Event::new(
983 &OIDC_CLIENT_TOKEN_RECEIVED_EVENT,
984 serde_json::json!({
985 "access_token": "[REDACTED]",
986 "id_token": if id_token.is_some() { "[REDACTED]" } else { "" },
987 "refresh_token": if refresh_token.is_some() { "[REDACTED]" } else { "" },
988 "expires_in": expires_in,
989 "token_type": token_type,
990 }),
991 );
992 Self::notify_event(
993 client_id,
994 event,
995 Dispatch::Inline,
996 llm_client,
997 app_state,
998 status_tx,
999 protocol,
1000 )
1001 .await;
1002
1003 Ok(())
1004 }
1005
1006 async fn start_authorization_code_flow(
1008 client_id: ClientId,
1009 data: serde_json::Value,
1010 llm_client: &OllamaClient,
1011 app_state: &Arc<AppState>,
1012 status_tx: &mpsc::UnboundedSender<String>,
1013 protocol: Arc<OpenIdConnectClientProtocol>,
1014 ) -> Result<()> {
1015 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1016 use tokio::net::TcpListener;
1017
1018 Log::new(Some(status_tx)).info(format!(
1019 "Starting authorization code flow for client {}",
1020 client_id
1021 ));
1022
1023 let (provider_url, oidc_client_id, oidc_client_secret) = app_state
1025 .with_client_mut(client_id, |client| {
1026 let provider = client
1027 .get_protocol_field("provider_url")
1028 .and_then(|v| v.as_str())
1029 .map(|s| s.to_string())?;
1030 let client_id_str = client
1031 .get_protocol_field("client_id")
1032 .and_then(|v| v.as_str())
1033 .map(|s| s.to_string())
1034 .unwrap_or_else(|| "default-client-id".to_string());
1035 let client_secret_str = client
1036 .get_protocol_field("client_secret")
1037 .and_then(|v| v.as_str())
1038 .map(|s| s.to_string());
1039 Some((provider, client_id_str, client_secret_str))
1040 })
1041 .await
1042 .flatten()
1043 .context("No provider URL found")?;
1044
1045 let scopes = data
1046 .get("scopes")
1047 .and_then(|v| v.as_str())
1048 .unwrap_or("openid profile email");
1049
1050 let callback_port = crate::client::wire_values::number::<u16>(&data, "port", 8080)?;
1051
1052 let issuer_url = IssuerUrl::new(provider_url)?;
1053 let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, http_hook).await?;
1054
1055 let auth_endpoint = provider_metadata.authorization_endpoint().as_str();
1057
1058 use rand::Rng;
1060 let state: String = rand::thread_rng()
1061 .sample_iter(&rand::distributions::Alphanumeric)
1062 .take(32)
1063 .map(char::from)
1064 .collect();
1065
1066 let redirect_uri = format!("http://localhost:{}/callback", callback_port);
1068
1069 let auth_url = format!(
1071 "{}?response_type=code&client_id={}&redirect_uri={}&scope={}&state={}",
1072 auth_endpoint,
1073 urlencoding::encode(&oidc_client_id),
1074 urlencoding::encode(&redirect_uri),
1075 urlencoding::encode(scopes),
1076 urlencoding::encode(&state)
1077 );
1078
1079 let log = Log::new(Some(status_tx));
1081 let _ = status_tx.send("========================================".to_string());
1082 log.info("Authorization Code Flow - User Action Required".to_string());
1083 let _ = status_tx.send("========================================".to_string());
1084 log.info("1. Open this URL in your browser:".to_string());
1085 log.info(format!(" {}", auth_url));
1086 log.info(format!(
1087 "2. After authorization, the browser will redirect to localhost:{}",
1088 callback_port
1089 ));
1090 let _ = status_tx.send("========================================".to_string());
1091 log.info(format!(
1092 "Starting local callback server on port {}...",
1093 callback_port
1094 ));
1095
1096 let listener = TcpListener::bind(format!("127.0.0.1:{}", callback_port))
1098 .await
1099 .context(format!(
1100 "Failed to bind to port {}. Port may be in use.",
1101 callback_port
1102 ))?;
1103
1104 log.info(format!(
1105 "Callback server listening on http://127.0.0.1:{}/callback",
1106 callback_port
1107 ));
1108 log.info("Waiting for authorization...".to_string());
1109
1110 let token_endpoint = provider_metadata
1112 .token_endpoint()
1113 .context("No token endpoint in provider metadata")?
1114 .as_str()
1115 .to_string();
1116
1117 let app_state_clone = app_state.clone();
1119 let llm_client_clone = llm_client.clone();
1120 let status_tx_clone = status_tx.clone();
1121 let protocol_clone = protocol.clone();
1122 let state_clone = state.clone();
1123
1124 let task_registrar = app_state.clone();
1127 let task_handle = tokio::spawn(async move {
1128 let log = Log::new(Some(&status_tx_clone));
1129 match listener.accept().await {
1131 Ok((mut socket, _addr)) => {
1132 log.info("Received callback request...".to_string());
1133
1134 let mut buffer = vec![0u8; 4096];
1136 match socket.read(&mut buffer).await {
1137 Ok(n) => {
1138 let request = String::from_utf8_lossy(&buffer[..n]);
1139
1140 if let Some(query_line) = request.lines().next() {
1142 if let Some(query_str) = query_line.split_whitespace().nth(1) {
1143 if let Some(query_params) = query_str.split('?').nth(1) {
1144 let mut code = None;
1145 let mut returned_state = None;
1146 let mut error = None;
1147
1148 for param in query_params.split('&') {
1149 let parts: Vec<&str> = param.split('=').collect();
1150 if parts.len() == 2 {
1151 match parts[0] {
1152 "code" => code = Some(parts[1].to_string()),
1153 "state" => {
1154 returned_state = Some(parts[1].to_string())
1155 }
1156 "error" => error = Some(parts[1].to_string()),
1157 _ => {}
1158 }
1159 }
1160 }
1161
1162 let response = if error.is_some() {
1164 "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\n\r\n<html><body><h1>Authorization Failed</h1><p>An error occurred during authorization. You can close this window.</p></body></html>"
1165 } else if code.is_some() {
1166 "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>"
1167 } else {
1168 "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\n\r\n<html><body><h1>Invalid Request</h1><p>Missing authorization code. You can close this window.</p></body></html>"
1169 };
1170
1171 let _ = socket.write_all(response.as_bytes()).await;
1172
1173 if let Some(error_msg) = error {
1175 log.error(format!(
1176 "Authorization failed: {}",
1177 error_msg
1178 ));
1179 return;
1180 }
1181
1182 if let Some(auth_code) = code {
1183 if returned_state.as_deref()
1185 != Some(state_clone.as_str())
1186 {
1187 log.error(
1188 "State mismatch - possible CSRF attack"
1189 .to_string(),
1190 );
1191 return;
1192 }
1193
1194 log.info("Authorization code received, exchanging for tokens...".to_string());
1195
1196 let mut token_params = vec![
1198 ("grant_type", "authorization_code"),
1199 ("code", auth_code.as_str()),
1200 ("redirect_uri", redirect_uri.as_str()),
1201 ("client_id", oidc_client_id.as_str()),
1202 ];
1203
1204 if let Some(ref secret) = oidc_client_secret {
1205 token_params
1206 .push(("client_secret", secret.as_str()));
1207 }
1208
1209 let http_client = plain_http_client();
1210 match http_client
1211 .post(&token_endpoint)
1212 .form(&token_params)
1213 .send()
1214 .await
1215 {
1216 Ok(response) => {
1217 if response.status().is_success() {
1218 match response
1219 .json::<serde_json::Value>()
1220 .await
1221 {
1222 Ok(token_json) => {
1223 log.info("Successfully exchanged code for tokens!".to_string());
1224
1225 if let Err(e) =
1226 Self::store_tokens_from_json(
1227 client_id,
1228 &token_json,
1229 &llm_client_clone,
1230 &app_state_clone,
1231 &status_tx_clone,
1232 protocol_clone,
1233 )
1234 .await
1235 {
1236 log.error(format!("Failed to store tokens: {}", e));
1237 }
1238 }
1239 Err(e) => {
1240 log.error(format!("Failed to parse token response: {}", e));
1241 }
1242 }
1243 } else {
1244 let error_text =
1245 response.text().await.unwrap_or_else(
1246 |_| "Unknown error".to_string(),
1247 );
1248 log.error(format!(
1249 "Token exchange failed: {}",
1250 error_text
1251 ));
1252 }
1253 }
1254 Err(e) => {
1255 log.error(format!(
1256 "Failed to exchange code: {}",
1257 e
1258 ));
1259 }
1260 }
1261 }
1262 }
1263 }
1264 }
1265 }
1266 Err(e) => {
1267 log.error(format!("Failed to read request: {}", e));
1268 }
1269 }
1270 }
1271 Err(e) => {
1272 log.error(format!("Failed to accept connection: {}", e));
1273 }
1274 }
1275 });
1276 task_registrar
1277 .register_client_task(client_id, task_handle)
1278 .await;
1279
1280 Ok(())
1281 }
1282
1283 #[allow(clippy::too_many_arguments)]
1285 async fn exchange_password(
1286 client_id: ClientId,
1287 data: serde_json::Value,
1288 llm_client: &OllamaClient,
1289 app_state: &Arc<AppState>,
1290 status_tx: &mpsc::UnboundedSender<String>,
1291 protocol: Arc<OpenIdConnectClientProtocol>,
1292 dispatch: Dispatch,
1293 ) -> Result<()> {
1294 let username = data
1295 .get("username")
1296 .and_then(|v| v.as_str())
1297 .context("Missing username")?;
1298 let password = data
1299 .get("password")
1300 .and_then(|v| v.as_str())
1301 .context("Missing password")?;
1302 let scopes = data
1303 .get("scopes")
1304 .and_then(|v| v.as_str())
1305 .unwrap_or("openid");
1306
1307 Log::new(Some(status_tx)).info(format!(
1308 "Exchanging password for tokens (user: {})",
1309 username
1310 ));
1311
1312 let (oidc_client_id, oidc_client_secret, provider_url) = app_state
1314 .with_client_mut(client_id, |client| {
1315 let client_id_str = client
1316 .get_protocol_field("client_id")
1317 .and_then(|v| v.as_str())
1318 .map(|s| s.to_string())
1319 .unwrap_or_else(|| "default-client-id".to_string());
1320 let client_secret_str = client
1321 .get_protocol_field("client_secret")
1322 .and_then(|v| v.as_str())
1323 .map(|s| s.to_string());
1324 let provider = client
1325 .get_protocol_field("provider_url")
1326 .and_then(|v| v.as_str())
1327 .map(|s| s.to_string())
1328 .unwrap_or_default();
1329 (client_id_str, client_secret_str, provider)
1330 })
1331 .await
1332 .unwrap_or_else(|| ("default-client-id".to_string(), None, String::new()));
1333
1334 let issuer_url = IssuerUrl::new(provider_url)?;
1335 let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, http_hook).await?;
1336
1337 let client = if let Some(secret) = oidc_client_secret {
1338 CoreClient::from_provider_metadata(
1339 provider_metadata,
1340 OidcClientId::new(oidc_client_id),
1341 Some(ClientSecret::new(secret)),
1342 )
1343 } else {
1344 CoreClient::from_provider_metadata(
1345 provider_metadata,
1346 OidcClientId::new(oidc_client_id),
1347 None,
1348 )
1349 };
1350
1351 let username_param = ResourceOwnerUsername::new(username.to_string());
1353 let password_param = ResourceOwnerPassword::new(password.to_string());
1354 let mut token_request = client.exchange_password(&username_param, &password_param);
1355
1356 for scope in scopes.split_whitespace() {
1358 token_request = token_request.add_scope(Scope::new(scope.to_string()));
1359 }
1360
1361 let token_response = token_request
1362 .request_async(http_hook)
1363 .await
1364 .context("Failed to exchange password for tokens")?;
1365
1366 Self::store_and_notify_tokens(
1368 client_id,
1369 &token_response,
1370 llm_client,
1371 app_state,
1372 status_tx,
1373 protocol,
1374 dispatch,
1375 )
1376 .await?;
1377
1378 Ok(())
1379 }
1380
1381 #[allow(clippy::too_many_arguments)]
1383 async fn exchange_client_credentials(
1384 client_id: ClientId,
1385 data: serde_json::Value,
1386 llm_client: &OllamaClient,
1387 app_state: &Arc<AppState>,
1388 status_tx: &mpsc::UnboundedSender<String>,
1389 protocol: Arc<OpenIdConnectClientProtocol>,
1390 dispatch: Dispatch,
1391 ) -> Result<()> {
1392 let scopes = data.get("scopes").and_then(|v| v.as_str()).unwrap_or("");
1393
1394 Log::new(Some(status_tx))
1395 .info("Exchanging client credentials for access token".to_string());
1396
1397 let (oidc_client_id, oidc_client_secret, provider_url) = app_state
1399 .with_client_mut(client_id, |client| {
1400 let client_id_str = client
1401 .get_protocol_field("client_id")
1402 .and_then(|v| v.as_str())
1403 .map(|s| s.to_string())
1404 .context("Missing client_id")
1405 .ok()?;
1406 let client_secret_str = client
1407 .get_protocol_field("client_secret")
1408 .and_then(|v| v.as_str())
1409 .map(|s| s.to_string())
1410 .context("Missing client_secret for confidential client")
1411 .ok()?;
1412 let provider = client
1413 .get_protocol_field("provider_url")
1414 .and_then(|v| v.as_str())
1415 .map(|s| s.to_string())?;
1416 Some((client_id_str, client_secret_str, provider))
1417 })
1418 .await
1419 .flatten()
1420 .context("Missing client configuration")?;
1421
1422 let issuer_url = IssuerUrl::new(provider_url)?;
1423 let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, http_hook).await?;
1424
1425 let client = CoreClient::from_provider_metadata(
1426 provider_metadata,
1427 OidcClientId::new(oidc_client_id),
1428 Some(ClientSecret::new(oidc_client_secret)),
1429 );
1430
1431 let mut token_request = client.exchange_client_credentials();
1433
1434 for scope in scopes.split_whitespace() {
1436 if !scope.is_empty() {
1437 token_request = token_request.add_scope(Scope::new(scope.to_string()));
1438 }
1439 }
1440
1441 let token_response = token_request
1442 .request_async(http_hook)
1443 .await
1444 .context("Failed to exchange client credentials")?;
1445
1446 Self::store_and_notify_tokens(
1448 client_id,
1449 &token_response,
1450 llm_client,
1451 app_state,
1452 status_tx,
1453 protocol,
1454 dispatch,
1455 )
1456 .await?;
1457
1458 Ok(())
1459 }
1460
1461 async fn refresh_token(
1463 client_id: ClientId,
1464 llm_client: &OllamaClient,
1465 app_state: &Arc<AppState>,
1466 status_tx: &mpsc::UnboundedSender<String>,
1467 protocol: Arc<OpenIdConnectClientProtocol>,
1468 dispatch: Dispatch,
1469 ) -> Result<()> {
1470 Log::new(Some(status_tx)).info("Refreshing access token".to_string());
1471
1472 let (refresh_token_str, oidc_client_id, oidc_client_secret, provider_url) = app_state
1474 .with_client_mut(client_id, |client| {
1475 let refresh = client
1476 .get_protocol_field("refresh_token")
1477 .and_then(|v| v.as_str())
1478 .map(|s| s.to_string())
1479 .context("No refresh token available")
1480 .ok()?;
1481 let client_id_str = client
1482 .get_protocol_field("client_id")
1483 .and_then(|v| v.as_str())
1484 .map(|s| s.to_string())?;
1485 let client_secret_str = client
1486 .get_protocol_field("client_secret")
1487 .and_then(|v| v.as_str())
1488 .map(|s| s.to_string());
1489 let provider = client
1490 .get_protocol_field("provider_url")
1491 .and_then(|v| v.as_str())
1492 .map(|s| s.to_string())?;
1493 Some((refresh, client_id_str, client_secret_str, provider))
1494 })
1495 .await
1496 .flatten()
1497 .context("Missing refresh token or client configuration")?;
1498
1499 let issuer_url = IssuerUrl::new(provider_url)?;
1500 let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, http_hook).await?;
1501
1502 let client = if let Some(secret) = oidc_client_secret {
1503 CoreClient::from_provider_metadata(
1504 provider_metadata,
1505 OidcClientId::new(oidc_client_id),
1506 Some(ClientSecret::new(secret)),
1507 )
1508 } else {
1509 CoreClient::from_provider_metadata(
1510 provider_metadata,
1511 OidcClientId::new(oidc_client_id),
1512 None,
1513 )
1514 };
1515
1516 use openidconnect::RefreshToken;
1517 let token_response = client
1518 .exchange_refresh_token(&RefreshToken::new(refresh_token_str))
1519 .request_async(http_hook)
1520 .await
1521 .context("Failed to refresh token")?;
1522
1523 Self::store_and_notify_tokens(
1525 client_id,
1526 &token_response,
1527 llm_client,
1528 app_state,
1529 status_tx,
1530 protocol,
1531 dispatch,
1532 )
1533 .await?;
1534
1535 Ok(())
1536 }
1537
1538 async fn fetch_userinfo(
1540 client_id: ClientId,
1541 llm_client: &OllamaClient,
1542 app_state: &Arc<AppState>,
1543 status_tx: &mpsc::UnboundedSender<String>,
1544 protocol: Arc<OpenIdConnectClientProtocol>,
1545 dispatch: Dispatch,
1546 ) -> Result<()> {
1547 Log::new(Some(status_tx)).info("Fetching UserInfo".to_string());
1548
1549 let (access_token_str, oidc_client_id, oidc_client_secret, provider_url) = app_state
1551 .with_client_mut(client_id, |client| {
1552 let access = client
1553 .get_protocol_field("access_token")
1554 .and_then(|v| v.as_str())
1555 .map(|s| s.to_string())
1556 .context("No access token available")
1557 .ok()?;
1558 let client_id_str = client
1559 .get_protocol_field("client_id")
1560 .and_then(|v| v.as_str())
1561 .map(|s| s.to_string())?;
1562 let client_secret_str = client
1563 .get_protocol_field("client_secret")
1564 .and_then(|v| v.as_str())
1565 .map(|s| s.to_string());
1566 let provider = client
1567 .get_protocol_field("provider_url")
1568 .and_then(|v| v.as_str())
1569 .map(|s| s.to_string())?;
1570 Some((access, client_id_str, client_secret_str, provider))
1571 })
1572 .await
1573 .flatten()
1574 .context("Missing access token or client configuration")?;
1575
1576 let issuer_url = IssuerUrl::new(provider_url)?;
1577 let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, http_hook).await?;
1578
1579 let client = if let Some(secret) = oidc_client_secret {
1580 CoreClient::from_provider_metadata(
1581 provider_metadata,
1582 OidcClientId::new(oidc_client_id),
1583 Some(ClientSecret::new(secret)),
1584 )
1585 } else {
1586 CoreClient::from_provider_metadata(
1587 provider_metadata,
1588 OidcClientId::new(oidc_client_id),
1589 None,
1590 )
1591 };
1592
1593 use openidconnect::AccessToken;
1594 let userinfo: CoreUserInfoClaims = client
1595 .user_info(AccessToken::new(access_token_str), None)
1596 .context("UserInfo endpoint not available")?
1597 .request_async(http_hook)
1598 .await
1599 .context("Failed to fetch UserInfo")?;
1600
1601 Log::new(Some(status_tx)).info(format!(
1602 "Received UserInfo for subject: {:?}",
1603 userinfo.subject()
1604 ));
1605
1606 let event = Event::new(
1608 &OIDC_CLIENT_USERINFO_RECEIVED_EVENT,
1609 serde_json::json!({
1610 "sub": userinfo.subject().as_str(),
1611 "claims": serde_json::to_value(&userinfo).unwrap_or(serde_json::json!({})),
1612 }),
1613 );
1614 Self::notify_event(
1615 client_id, event, dispatch, llm_client, app_state, status_tx, protocol,
1616 )
1617 .await;
1618
1619 Ok(())
1620 }
1621
1622 #[allow(clippy::too_many_arguments)]
1624 async fn store_and_notify_tokens(
1625 client_id: ClientId,
1626 token_response: &CoreTokenResponse,
1627 llm_client: &OllamaClient,
1628 app_state: &Arc<AppState>,
1629 status_tx: &mpsc::UnboundedSender<String>,
1630 protocol: Arc<OpenIdConnectClientProtocol>,
1631 dispatch: Dispatch,
1632 ) -> Result<()> {
1633 let access_token = token_response.access_token().secret();
1634 let id_token = token_response
1635 .extra_fields()
1636 .id_token()
1637 .map(|t| t.to_string());
1638 let refresh_token = token_response
1639 .refresh_token()
1640 .map(|t| t.secret().to_string());
1641 let expires_in = token_response.expires_in().map(|d| d.as_secs());
1642 let token_type = token_response.token_type().as_ref();
1643
1644 Log::new(Some(status_tx)).info(format!("Received tokens (expires_in: {:?}s)", expires_in));
1645
1646 app_state
1648 .with_client_mut(client_id, |client| {
1649 client.set_protocol_field(
1650 "access_token".to_string(),
1651 serde_json::json!(access_token),
1652 );
1653 if let Some(id) = &id_token {
1654 client.set_protocol_field("id_token".to_string(), serde_json::json!(id));
1655 }
1656 if let Some(refresh) = &refresh_token {
1657 client.set_protocol_field(
1658 "refresh_token".to_string(),
1659 serde_json::json!(refresh),
1660 );
1661 }
1662 })
1663 .await;
1664
1665 let event = Event::new(
1674 &OIDC_CLIENT_TOKEN_RECEIVED_EVENT,
1675 serde_json::json!({
1676 "access_token": "[REDACTED]",
1677 "id_token": if id_token.is_some() { "[REDACTED]" } else { "" },
1678 "refresh_token": if refresh_token.is_some() { "[REDACTED]" } else { "" },
1679 "expires_in": expires_in,
1680 "token_type": token_type,
1681 }),
1682 );
1683 Self::notify_event(
1684 client_id, event, dispatch, llm_client, app_state, status_tx, protocol,
1685 )
1686 .await;
1687
1688 Ok(())
1689 }
1690
1691 #[allow(clippy::too_many_arguments)]
1693 async fn notify_event(
1694 client_id: ClientId,
1695 event: Event,
1696 dispatch: Dispatch,
1697 llm_client: &OllamaClient,
1698 app_state: &Arc<AppState>,
1699 status_tx: &mpsc::UnboundedSender<String>,
1700 protocol: Arc<OpenIdConnectClientProtocol>,
1701 ) {
1702 match dispatch {
1703 Dispatch::Inline => {
1704 Self::raise_event(
1705 client_id,
1706 event,
1707 llm_client.clone(),
1708 app_state.clone(),
1709 status_tx.clone(),
1710 protocol,
1711 )
1712 .await
1713 }
1714 Dispatch::Deferred => {
1715 let handle = tokio::spawn(Self::raise_event(
1716 client_id,
1717 event,
1718 llm_client.clone(),
1719 app_state.clone(),
1720 status_tx.clone(),
1721 protocol,
1722 ));
1723 app_state.register_client_task(client_id, handle).await;
1725 }
1726 }
1727 }
1728
1729 async fn raise_event(
1731 client_id: ClientId,
1732 event: Event,
1733 llm_client: OllamaClient,
1734 app_state: Arc<AppState>,
1735 status_tx: mpsc::UnboundedSender<String>,
1736 protocol: Arc<OpenIdConnectClientProtocol>,
1737 ) {
1738 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
1739 return;
1740 };
1741 let memory = app_state
1742 .get_memory_for_client(client_id)
1743 .await
1744 .unwrap_or_default();
1745
1746 match call_llm_for_client(
1747 &llm_client,
1748 &app_state,
1749 client_id.to_string(),
1750 &instruction,
1751 &memory,
1752 Some(&event),
1753 protocol.as_ref(),
1754 &status_tx,
1755 )
1756 .await
1757 {
1758 Ok(ClientLlmResult {
1759 actions,
1760 memory_updates,
1761 }) => {
1762 if let Some(mem) = memory_updates {
1763 app_state.set_memory_for_client(client_id, mem).await;
1764 }
1765
1766 for action in actions {
1768 if let Err(e) = Self::execute_llm_action(
1769 action,
1770 client_id,
1771 &llm_client,
1772 &app_state,
1773 &status_tx,
1774 protocol.clone(),
1775 )
1776 .await
1777 {
1778 error!("Failed to execute follow-up action: {}", e);
1779 }
1780 }
1781 }
1782 Err(e) => {
1783 error!("LLM error for OIDC client {}: {}", client_id, e);
1784 }
1785 }
1786 }
1787}