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