micromegas_auth/multi.rs
1//! Multi-provider authentication that tries multiple auth methods in sequence.
2
3use crate::types::{AuthContext, AuthProvider, ProviderUnavailable};
4use async_trait::async_trait;
5use micromegas_tracing::prelude::*;
6use std::sync::Arc;
7
8/// Multi-provider authentication that tries providers in order until one succeeds.
9///
10/// This provider allows supporting multiple authentication methods simultaneously
11/// and enables adding custom enterprise authentication providers. Providers are
12/// tried in the order they were added via `with_provider()`.
13///
14/// Provider order matters for authentication precedence - the first successful
15/// match wins. Typically, you want faster providers (like API key) before slower
16/// ones (like OIDC JWT validation).
17///
18/// # Example
19///
20/// ```rust,no_run
21/// use micromegas_auth::api_key::{ApiKeyAuthProvider, parse_key_ring};
22/// use micromegas_auth::oidc::{OidcAuthProvider, OidcConfig, OidcIssuer};
23/// use micromegas_auth::multi::MultiAuthProvider;
24/// use std::sync::Arc;
25///
26/// # async fn example() -> anyhow::Result<()> {
27/// // Set up API key provider
28/// let keyring = parse_key_ring(r#"[{"name": "test", "key": "secret"}]"#)?;
29/// let api_key_provider = Arc::new(ApiKeyAuthProvider::new(keyring));
30///
31/// // Set up OIDC provider
32/// let oidc_config = OidcConfig {
33/// issuers: vec![OidcIssuer {
34/// issuer: "https://accounts.google.com".to_string(),
35/// audience: "your-app.apps.googleusercontent.com".to_string(),
36/// }],
37/// jwks_refresh_interval_secs: 3600,
38/// token_cache_size: 1000,
39/// token_cache_ttl_secs: 300,
40/// };
41/// let oidc_provider = Arc::new(OidcAuthProvider::new(oidc_config).await?);
42///
43/// // Create multi-provider with builder pattern
44/// let multi = MultiAuthProvider::new()
45/// .with_provider(api_key_provider)
46/// .with_provider(oidc_provider);
47/// // .with_provider(Arc::new(MyEnterpriseAuthProvider::new())); // Custom provider!
48/// # Ok(())
49/// # }
50/// ```
51pub struct MultiAuthProvider {
52 providers: Vec<Arc<dyn AuthProvider>>,
53}
54
55impl MultiAuthProvider {
56 /// Creates a new empty MultiAuthProvider.
57 #[allow(clippy::new_without_default)]
58 pub fn new() -> Self {
59 Self {
60 providers: Vec::new(),
61 }
62 }
63
64 /// Adds a provider to the authentication chain.
65 ///
66 /// Providers are tried in the order they are added. Returns self for chaining.
67 pub fn with_provider(mut self, provider: Arc<dyn AuthProvider>) -> Self {
68 self.providers.push(provider);
69 self
70 }
71
72 /// Returns true if no providers are configured.
73 pub fn is_empty(&self) -> bool {
74 self.providers.is_empty()
75 }
76}
77
78#[async_trait]
79impl AuthProvider for MultiAuthProvider {
80 async fn validate_request(
81 &self,
82 parts: &dyn crate::types::RequestParts,
83 ) -> anyhow::Result<AuthContext> {
84 // Tracks whether any provider in the chain failed because its store was
85 // unreachable, as opposed to a plain credential rejection. A request that
86 // might have matched a live key had the store been reachable must not be
87 // treated as a confirmed rejection once every provider has been tried.
88 let mut unavailable: Option<anyhow::Error> = None;
89 for provider in &self.providers {
90 match provider.validate_request(parts).await {
91 Ok(auth_ctx) => {
92 return Ok(auth_ctx);
93 }
94 Err(e) => {
95 debug!("partial auth failed: {e:?}");
96 if e.downcast_ref::<ProviderUnavailable>().is_some() {
97 unavailable = Some(e);
98 }
99 }
100 }
101 }
102 if let Some(e) = unavailable {
103 return Err(e);
104 }
105 anyhow::bail!("authentication failed with all providers")
106 }
107}