alibi_plugins/oauth_provider/
discovery.rs1use alibi_core::store::OAuthClient;
3use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, CallbackContext};
4use async_trait::async_trait;
5use std::sync::Arc;
6#[async_trait]
8pub trait OAuthClientDiscovery: Send + Sync + std::fmt::Debug {
9 fn id(&self) -> &str;
11 fn matches(&self, client_id: &str) -> bool;
12 fn discovery_metadata(&self) -> serde_json::Map<String, serde_json::Value> {
15 serde_json::Map::new()
16 }
17 fn metadata_resource_fetch(&self) -> Option<Arc<dyn super::OAuthClientMetadataResourceFetch>> {
19 None
20 }
21
22 async fn resolve(
25 &self,
26 context: &CallbackContext,
27 client_id: &str,
28 existing: Option<OAuthClient>,
29 ) -> AuthResult<Option<OAuthClient>>;
30}
31#[derive(Clone)]
32pub(super) struct ClientDiscoveries(pub(super) Vec<Arc<dyn OAuthClientDiscovery>>);
33
34#[derive(Debug, Default)]
35pub(super) struct TrustedCache {
36 clients: std::sync::Mutex<std::collections::HashMap<String, OAuthClient>>,
37}
38impl TrustedCache {
39 fn get(&self, id: &str) -> AuthResult<Option<OAuthClient>> {
40 Ok(self
41 .clients
42 .lock()
43 .map_err(|_| AuthError::internal("Trusted client cache unavailable"))?
44 .get(id)
45 .cloned())
46 }
47 fn set(&self, id: &str, client: OAuthClient) -> AuthResult<()> {
48 _ = self
49 .clients
50 .lock()
51 .map_err(|_| AuthError::internal("Trusted client cache unavailable"))?
52 .insert(id.into(), client);
53 Ok(())
54 }
55}
56#[derive(Clone)]
57pub(super) struct TrustedClients {
58 pub(super) ids: std::collections::HashSet<String>,
59 pub(super) cache: Arc<TrustedCache>,
60}
61type TrustedView<'a> = (&'a std::collections::HashSet<String>, &'a TrustedCache);
62
63pub(super) async fn resolve<S: AuthSchema>(
64 ctx: &AuthContext<S>,
65 client_id: &str,
66) -> AuthResult<Option<OAuthClient>> {
67 let discoveries = ctx.extensions.get::<ClientDiscoveries>();
68 let configured = discoveries
69 .as_ref()
70 .map_or(&[][..], |sources| sources.0.as_slice());
71 let trusted = ctx.extensions.get::<TrustedClients>();
72 resolve_configured(
73 ctx,
74 client_id,
75 configured,
76 trusted
77 .as_ref()
78 .map(|state| (&state.ids, state.cache.as_ref())),
79 )
80 .await
81}
82
83impl super::OAuthProviderPlugin {
84 pub async fn get_client<S: AuthSchema>(
87 &self,
88 ctx: &AuthContext<S>,
89 client_id: &str,
90 ) -> AuthResult<Option<OAuthClient>> {
91 resolve_configured(
92 ctx,
93 client_id,
94 &self.config.client_discoveries,
95 Some((
96 &self.config.cached_trusted_clients,
97 self.trusted_clients.as_ref(),
98 )),
99 )
100 .await
101 }
102}
103
104async fn resolve_configured<S: AuthSchema>(
105 ctx: &AuthContext<S>,
106 client_id: &str,
107 configured: &[Arc<dyn OAuthClientDiscovery>],
108 trusted: Option<TrustedView<'_>>,
109) -> AuthResult<Option<OAuthClient>> {
110 if let Some((_, cache)) = trusted
111 && let Some(client) = cache.get(client_id)?
112 && client
113 .metadata
114 .get("client_discovery_id")
115 .and_then(serde_json::Value::as_str)
116 .is_none_or(str::is_empty)
117 {
118 return Ok(Some(client));
119 }
120 let existing = ctx.database.get_oauth_client(client_id).await?;
121 if let Some(owner) = existing
122 .as_ref()
123 .and_then(|client| client.metadata.get("client_discovery_id"))
124 .and_then(serde_json::Value::as_str)
125 .filter(|owner| !owner.is_empty())
126 {
127 let Some(discovery) = configured.iter().find(|discovery| discovery.id() == owner) else {
128 return Ok(None);
129 };
130 if !discovery.matches(client_id) {
131 return Ok(None);
132 }
133 return discovery
134 .resolve(&CallbackContext::new(ctx, None), client_id, existing)
135 .await;
136 }
137 if let Some(client) = existing {
138 if let Some((ids, cache)) = trusted
139 && ids.contains(client_id)
140 {
141 cache.set(client_id, client.clone())?;
142 }
143 return Ok(Some(client));
144 }
145 for discovery in configured {
146 if discovery.matches(client_id) {
147 let resolved = discovery
148 .resolve(&CallbackContext::new(ctx, None), client_id, None)
149 .await?;
150 if resolved.is_some() {
151 return Ok(resolved);
152 }
153 }
154 }
155 Ok(None)
156}