Skip to main content

micromegas_auth/
oidc_client.rs

1//! Canonical login-flow OIDC client construction.
2//!
3//! This module owns discovery of an OIDC provider's metadata and
4//! construction of the `openidconnect` client used for the authorization
5//! code (login) flow. It is the single place where this happens; consumers
6//! (e.g. `analytics-web-srv`) should discover a provider once, cache the
7//! resulting [`crate::oidc_client::DiscoveredProvider`], and call
8//! [`crate::oidc_client::DiscoveredProvider::build_client`]
9//! whenever they need a configured client.
10//!
11//! This is deliberately separate from `crate::oidc`'s internal
12//! `discover_async` call used to fetch JWKS for JWT validation: the two serve
13//! different lazy caches for different purposes (login-flow client vs. JWKS
14//! validation).
15
16use crate::oidc::create_http_client;
17use anyhow::{Result, anyhow};
18use openidconnect::core::{CoreClient, CoreProviderMetadata};
19use openidconnect::{ClientId, IssuerUrl, RedirectUrl};
20use std::sync::Arc;
21
22/// Fully-parameterized openidconnect client with endpoints set from
23/// discovered provider metadata (public client, PKCE — no client secret).
24pub type ConfiguredCoreClient = openidconnect::Client<
25    openidconnect::EmptyAdditionalClaims,
26    openidconnect::core::CoreAuthDisplay,
27    openidconnect::core::CoreGenderClaim,
28    openidconnect::core::CoreJweContentEncryptionAlgorithm,
29    openidconnect::core::CoreJsonWebKey,
30    openidconnect::core::CoreAuthPrompt,
31    openidconnect::StandardErrorResponse<openidconnect::core::CoreErrorResponseType>,
32    openidconnect::core::CoreTokenResponse,
33    openidconnect::core::CoreTokenIntrospectionResponse,
34    openidconnect::core::CoreRevocableToken,
35    openidconnect::core::CoreRevocationErrorResponse,
36    openidconnect::EndpointSet,
37    openidconnect::EndpointNotSet,
38    openidconnect::EndpointNotSet,
39    openidconnect::EndpointNotSet,
40    openidconnect::EndpointMaybeSet,
41    openidconnect::EndpointMaybeSet,
42>;
43
44/// A discovered OIDC provider, ready to build login-flow clients from.
45///
46/// This is the single canonical home for provider discovery + client
47/// construction. `analytics-web-srv` caches one of these in a `OnceCell`
48/// and calls `build_client()` for the authorization-code flow.
49pub struct DiscoveredProvider {
50    pub metadata: Arc<CoreProviderMetadata>,
51    pub client_id: ClientId,
52    pub redirect_uri: RedirectUrl,
53}
54
55impl DiscoveredProvider {
56    /// Discover provider metadata for `issuer` and remember the client id /
57    /// redirect uri needed to build clients. Uses the SSRF-hardened HTTP client.
58    pub async fn discover(issuer: &str, client_id: &str, redirect_uri: &str) -> Result<Self> {
59        let issuer_url =
60            IssuerUrl::new(issuer.to_string()).map_err(|e| anyhow!("Invalid issuer URL: {e:?}"))?;
61        let http_client = create_http_client()?;
62        let metadata = CoreProviderMetadata::discover_async(issuer_url, &http_client)
63            .await
64            .map_err(|e| anyhow!("Failed to discover OIDC provider: {e:?}"))?;
65        let redirect_uri = RedirectUrl::new(redirect_uri.to_string())
66            .map_err(|e| anyhow!("Invalid redirect URI: {e:?}"))?;
67        Ok(Self {
68            metadata: Arc::new(metadata),
69            client_id: ClientId::new(client_id.to_string()),
70            redirect_uri,
71        })
72    }
73
74    /// Build a configured login-flow client (public client, PKCE) from the
75    /// discovered metadata.
76    pub fn build_client(&self) -> ConfiguredCoreClient {
77        CoreClient::from_provider_metadata(
78            (*self.metadata).clone(),
79            self.client_id.clone(),
80            None, // public client with PKCE — no secret
81        )
82        .set_redirect_uri(self.redirect_uri.clone())
83    }
84}