aster_forge_external_auth/
registry.rs

1//! Runtime registry for feature-enabled external authentication provider drivers.
2//!
3//! The default registry registers only drivers compiled into the crate through Cargo features.
4//! Applications can create their own registry and call [`ExternalAuthProviderRegistry::add`] to
5//! append product or plugin-provided drivers without replacing built-ins. Tests and advanced
6//! application code can still call [`ExternalAuthProviderRegistry::register`] when intentional
7//! replacement is required.
8
9use std::collections::HashMap;
10use std::sync::{Arc, OnceLock};
11
12use super::driver::{
13    ExternalAuthProviderConfig, ExternalAuthProviderDescriptor, ExternalAuthProviderDriver,
14};
15#[cfg(feature = "github")]
16use super::providers::github::GitHubProviderDriver;
17#[cfg(feature = "google")]
18use super::providers::google::GoogleProviderDriver;
19#[cfg(feature = "microsoft")]
20use super::providers::microsoft::MicrosoftProviderDriver;
21#[cfg(feature = "oauth2")]
22use super::providers::oauth2::OAuth2ProviderDriver;
23#[cfg(feature = "oidc")]
24use super::providers::oidc::OidcProviderDriver;
25#[cfg(feature = "qq")]
26use super::providers::qq::QqProviderDriver;
27use crate::types::ExternalAuthProviderKind;
28use crate::{ExternalAuthError, Result};
29
30/// Registry of external authentication provider drivers keyed by provider kind.
31///
32/// The registry is the shared capability boundary for application services. Callers can use it to
33/// list descriptors for admin surfaces, gate stored provider configs before a login flow starts,
34/// and retrieve the runtime driver for a validated provider. The registry deliberately does not
35/// know how providers are stored in a product database; applications adapt their own rows into
36/// [`ExternalAuthProviderConfig`] immediately before using this type.
37pub struct ExternalAuthProviderRegistry {
38    drivers: HashMap<ExternalAuthProviderKind, Arc<dyn ExternalAuthProviderDriver>>,
39}
40
41impl ExternalAuthProviderRegistry {
42    /// Creates an empty registry with no built-in provider drivers.
43    ///
44    /// This is useful for applications that want a fully explicit provider list, tests that need
45    /// deterministic registration behavior across Cargo feature sets, or plugin hosts that build a
46    /// registry from externally supplied drivers.
47    #[must_use]
48    pub fn empty() -> Self {
49        Self {
50            drivers: HashMap::new(),
51        }
52    }
53
54    /// Creates a registry populated with all feature-enabled built-in drivers.
55    #[must_use]
56    pub fn new() -> Self {
57        #[cfg(not(any(
58            feature = "oidc",
59            feature = "oauth2",
60            feature = "github",
61            feature = "google",
62            feature = "microsoft",
63            feature = "qq"
64        )))]
65        let registry = Self::empty();
66        #[cfg(any(
67            feature = "oidc",
68            feature = "oauth2",
69            feature = "github",
70            feature = "google",
71            feature = "microsoft",
72            feature = "qq"
73        ))]
74        let mut registry = Self::empty();
75        #[cfg(feature = "oidc")]
76        registry.register_builtin(OidcProviderDriver::new());
77        #[cfg(feature = "oauth2")]
78        registry.register_builtin(OAuth2ProviderDriver::new());
79        #[cfg(feature = "github")]
80        registry.register_builtin(GitHubProviderDriver::new());
81        #[cfg(feature = "google")]
82        registry.register_builtin(GoogleProviderDriver::new());
83        #[cfg(feature = "microsoft")]
84        registry.register_builtin(MicrosoftProviderDriver::new());
85        #[cfg(feature = "qq")]
86        registry.register_builtin(QqProviderDriver::new());
87        registry
88    }
89
90    /// Creates a built-in registry and lets an external system append registrations.
91    ///
92    /// This is the intended integration point for application-level extension systems: the caller
93    /// receives a mutable registry, calls [`ExternalAuthProviderRegistry::add`] for each external
94    /// driver it wants to expose, and returns any setup error. Built-in drivers are registered
95    /// before the callback runs, so external systems cannot accidentally replace them through the
96    /// non-replacing add API.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`ExternalAuthError`] when an external registration callback fails.
101    pub fn with_external_registrations<F>(configure: F) -> Result<Self>
102    where
103        F: FnOnce(&mut Self) -> Result<()>,
104    {
105        let mut registry = Self::new();
106        configure(&mut registry)?;
107        Ok(registry)
108    }
109
110    /// Adds a driver if its provider kind is not already registered.
111    ///
112    /// Use this for product-specific or plugin-provided drivers. Duplicate provider kinds return a
113    /// configuration error instead of replacing the existing driver, which keeps built-in behavior
114    /// stable when external systems are enabled.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`ExternalAuthError`] when the provider kind is already present in the builder.
119    pub fn add<D>(&mut self, driver: D) -> Result<()>
120    where
121        D: ExternalAuthProviderDriver + 'static,
122    {
123        self.add_arc(Arc::new(driver))
124    }
125
126    /// Adds an already shared driver if its provider kind is not already registered.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`ExternalAuthError`] when the provider kind is already present in the builder.
131    pub fn add_arc(&mut self, driver: Arc<dyn ExternalAuthProviderDriver>) -> Result<()> {
132        let kind = driver.kind();
133        Self::validate_driver_descriptor(kind, &driver.descriptor())?;
134        if self.drivers.contains_key(&kind) {
135            return Err(ExternalAuthError::config_error(format!(
136                "external auth provider driver '{}' is already registered",
137                kind.as_str()
138            )));
139        }
140        self.drivers.insert(kind, driver);
141        Ok(())
142    }
143
144    /// Registers or replaces a driver for its provider kind.
145    ///
146    /// Use this only when replacement is intentional, such as tests or product-level overrides.
147    /// The driver must still report a descriptor for the same provider kind that it registers.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`ExternalAuthError`] when the provider kind is already registered.
152    pub fn register<D>(&mut self, driver: D) -> Result<()>
153    where
154        D: ExternalAuthProviderDriver + 'static,
155    {
156        self.register_arc(Arc::new(driver))
157    }
158
159    /// Registers or replaces an already shared driver for its provider kind.
160    ///
161    /// # Errors
162    ///
163    /// Returns [`ExternalAuthError`] when the provider kind is already registered.
164    pub fn register_arc(&mut self, driver: Arc<dyn ExternalAuthProviderDriver>) -> Result<()> {
165        let kind = driver.kind();
166        Self::validate_driver_descriptor(kind, &driver.descriptor())?;
167        self.drivers.insert(kind, driver);
168        Ok(())
169    }
170
171    /// Iterates over registered provider kinds.
172    pub fn supported_kinds(&self) -> impl Iterator<Item = ExternalAuthProviderKind> + '_ {
173        self.drivers.keys().copied()
174    }
175
176    /// Returns whether a driver for `kind` is registered.
177    #[must_use]
178    pub fn contains(&self, kind: ExternalAuthProviderKind) -> bool {
179        self.drivers.contains_key(&kind)
180    }
181
182    /// Returns registered provider descriptors sorted by provider kind.
183    #[must_use]
184    pub fn descriptors(&self) -> Vec<ExternalAuthProviderDescriptor> {
185        let mut descriptors = self
186            .drivers
187            .values()
188            .map(|driver| driver.descriptor())
189            .collect::<Vec<_>>();
190        descriptors.sort_by_key(|descriptor| descriptor.kind.as_str());
191        descriptors
192    }
193
194    /// Returns the descriptor for a registered provider kind.
195    ///
196    /// # Errors
197    ///
198    /// Returns [`ExternalAuthError`] when the provider kind is unsupported or unregistered.
199    pub fn descriptor_for(
200        &self,
201        kind: ExternalAuthProviderKind,
202    ) -> Result<ExternalAuthProviderDescriptor> {
203        Ok(self.get_driver(kind)?.descriptor())
204    }
205
206    /// Ensures that a provider kind is enabled in this registry.
207    ///
208    /// This is useful for service-layer guards that need to reject disabled provider kinds without
209    /// constructing a login flow or exposing the underlying driver.
210    ///
211    /// # Errors
212    ///
213    /// Returns [`ExternalAuthError`] when the provider kind is not available.
214    pub fn ensure_provider_supported(&self, kind: ExternalAuthProviderKind) -> Result<()> {
215        if self.contains(kind) {
216            return Ok(());
217        }
218        Err(ExternalAuthError::config_error(format!(
219            "external auth provider driver '{}' is not registered",
220            kind.as_str()
221        )))
222    }
223
224    /// Validates that a product-owned provider config matches a registered driver descriptor.
225    ///
226    /// This catches configuration drift before provider-specific network calls run. The method
227    /// checks that the provider kind is enabled and that the stored protocol matches the driver's
228    /// declared protocol. It returns the descriptor so callers can keep using the capability data
229    /// without another registry lookup.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`ExternalAuthError`] when the provider is unsupported or its config is invalid.
234    pub fn validate_provider_config(
235        &self,
236        provider: &ExternalAuthProviderConfig,
237    ) -> Result<ExternalAuthProviderDescriptor> {
238        let descriptor = self.descriptor_for(provider.provider_kind)?;
239        if provider.protocol != descriptor.protocol {
240            return Err(ExternalAuthError::validation_error(format!(
241                "external auth provider '{}' is configured with protocol '{}' but driver expects '{}'",
242                provider.provider_kind.as_str(),
243                provider.protocol.as_str(),
244                descriptor.protocol.as_str()
245            )));
246        }
247        Ok(descriptor)
248    }
249
250    /// Returns the registered driver for a provider config after validating the config boundary.
251    ///
252    /// Product services should prefer this method when starting authorization, exchanging a
253    /// callback, or testing a provider because it applies the same registry-level gates for every
254    /// flow.
255    ///
256    /// # Errors
257    ///
258    /// Returns [`ExternalAuthError`] when validation fails or no driver is registered.
259    pub fn driver_for_provider(
260        &self,
261        provider: &ExternalAuthProviderConfig,
262    ) -> Result<Arc<dyn ExternalAuthProviderDriver>> {
263        self.validate_provider_config(provider)?;
264        self.get_driver(provider.provider_kind)
265    }
266
267    /// Returns a registered driver by provider kind.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`ExternalAuthError`] when no driver is registered for the provider kind.
272    pub fn get_driver(
273        &self,
274        kind: ExternalAuthProviderKind,
275    ) -> Result<Arc<dyn ExternalAuthProviderDriver>> {
276        self.drivers.get(&kind).cloned().ok_or_else(|| {
277            ExternalAuthError::config_error(format!(
278                "external auth provider driver '{}' is not registered",
279                kind.as_str()
280            ))
281        })
282    }
283
284    /// Returns the OIDC driver from this registry.
285    ///
286    /// # Errors
287    ///
288    /// Returns [`ExternalAuthError`] when the OIDC driver is not enabled or registered.
289    pub fn oidc(&self) -> Result<Arc<dyn ExternalAuthProviderDriver>> {
290        self.get_driver(ExternalAuthProviderKind::Oidc)
291    }
292
293    fn validate_driver_descriptor(
294        kind: ExternalAuthProviderKind,
295        descriptor: &ExternalAuthProviderDescriptor,
296    ) -> Result<()> {
297        if descriptor.kind == kind {
298            return Ok(());
299        }
300        Err(ExternalAuthError::config_error(format!(
301            "external auth provider driver '{}' returned descriptor for '{}'",
302            kind.as_str(),
303            descriptor.kind.as_str()
304        )))
305    }
306
307    #[cfg(any(
308        feature = "github",
309        feature = "google",
310        feature = "microsoft",
311        feature = "oauth2",
312        feature = "oidc",
313        feature = "qq"
314    ))]
315    fn register_builtin<D>(&mut self, driver: D)
316    where
317        D: ExternalAuthProviderDriver + 'static,
318    {
319        let kind = driver.kind();
320        self.drivers.insert(kind, Arc::new(driver));
321    }
322}
323
324impl Default for ExternalAuthProviderRegistry {
325    fn default() -> Self {
326        Self::new()
327    }
328}
329
330/// Returns a process-wide default registry populated with feature-enabled drivers.
331pub fn default_registry() -> &'static ExternalAuthProviderRegistry {
332    static REGISTRY: OnceLock<ExternalAuthProviderRegistry> = OnceLock::new();
333    REGISTRY.get_or_init(ExternalAuthProviderRegistry::new)
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::{
340        ExternalAuthAuthorizationStart, ExternalAuthCallback, ExternalAuthProfile,
341        ExternalAuthProviderConfig, ExternalAuthProviderTestResult,
342    };
343    use async_trait::async_trait;
344
345    #[derive(Default)]
346    struct TestOidcDriver;
347
348    #[async_trait]
349    impl ExternalAuthProviderDriver for TestOidcDriver {
350        fn kind(&self) -> ExternalAuthProviderKind {
351            ExternalAuthProviderKind::Oidc
352        }
353
354        fn descriptor(&self) -> ExternalAuthProviderDescriptor {
355            ExternalAuthProviderDescriptor {
356                kind: ExternalAuthProviderKind::Oidc,
357                protocol: crate::types::ExternalAuthProtocol::Oidc,
358                display_name: "Test OIDC",
359                description: "Test OIDC driver",
360                default_scopes: "openid email profile",
361                issuer_url_required: true,
362                manual_endpoint_configuration_supported: false,
363                authorization_url_required: false,
364                token_url_required: false,
365                userinfo_url_required: false,
366                supports_discovery: true,
367                supports_pkce: true,
368                supports_email_verified_claim: true,
369            }
370        }
371
372        async fn start_authorization(
373            &self,
374            _provider: &ExternalAuthProviderConfig,
375            _redirect_uri: &str,
376        ) -> Result<ExternalAuthAuthorizationStart> {
377            unreachable!("registry tests only inspect driver registration")
378        }
379
380        async fn exchange_callback(
381            &self,
382            _provider: &ExternalAuthProviderConfig,
383            _callback: ExternalAuthCallback,
384        ) -> Result<ExternalAuthProfile> {
385            unreachable!("registry tests only inspect driver registration")
386        }
387
388        async fn test_provider(
389            &self,
390            _provider: &ExternalAuthProviderConfig,
391        ) -> Result<ExternalAuthProviderTestResult> {
392            unreachable!("registry tests only inspect driver registration")
393        }
394    }
395
396    #[derive(Default)]
397    struct MismatchedDescriptorDriver;
398
399    #[async_trait]
400    impl ExternalAuthProviderDriver for MismatchedDescriptorDriver {
401        fn kind(&self) -> ExternalAuthProviderKind {
402            ExternalAuthProviderKind::Oidc
403        }
404
405        fn descriptor(&self) -> ExternalAuthProviderDescriptor {
406            ExternalAuthProviderDescriptor {
407                kind: ExternalAuthProviderKind::GenericOAuth2,
408                protocol: crate::types::ExternalAuthProtocol::OAuth2,
409                display_name: "Mismatched driver",
410                description: "Driver with inconsistent registry metadata",
411                default_scopes: "email",
412                issuer_url_required: false,
413                manual_endpoint_configuration_supported: true,
414                authorization_url_required: true,
415                token_url_required: true,
416                userinfo_url_required: true,
417                supports_discovery: false,
418                supports_pkce: true,
419                supports_email_verified_claim: false,
420            }
421        }
422
423        async fn start_authorization(
424            &self,
425            _provider: &ExternalAuthProviderConfig,
426            _redirect_uri: &str,
427        ) -> Result<ExternalAuthAuthorizationStart> {
428            unreachable!("registry tests only inspect driver registration")
429        }
430
431        async fn exchange_callback(
432            &self,
433            _provider: &ExternalAuthProviderConfig,
434            _callback: ExternalAuthCallback,
435        ) -> Result<ExternalAuthProfile> {
436            unreachable!("registry tests only inspect driver registration")
437        }
438
439        async fn test_provider(
440            &self,
441            _provider: &ExternalAuthProviderConfig,
442        ) -> Result<ExternalAuthProviderTestResult> {
443            unreachable!("registry tests only inspect driver registration")
444        }
445    }
446
447    fn oidc_provider_config() -> ExternalAuthProviderConfig {
448        ExternalAuthProviderConfig {
449            id: 1,
450            key: "test-oidc".to_string(),
451            provider_kind: ExternalAuthProviderKind::Oidc,
452            protocol: crate::types::ExternalAuthProtocol::Oidc,
453            options: crate::types::ExternalAuthProviderOptions::default(),
454            issuer_url: Some("https://issuer.example.com".to_string()),
455            authorization_url: None,
456            token_url: None,
457            userinfo_url: None,
458            client_id: "client-id".to_string(),
459            client_secret: Some("client-secret".to_string()),
460            scopes: "openid email profile".to_string(),
461            subject_claim: None,
462            username_claim: None,
463            display_name_claim: None,
464            email_claim: None,
465            email_verified_claim: None,
466            groups_claim: None,
467            avatar_url_claim: None,
468            outbound_http_user_agent: None,
469        }
470    }
471
472    #[cfg(feature = "oidc")]
473    #[test]
474    fn registry_returns_oidc_driver_by_kind() {
475        let registry = ExternalAuthProviderRegistry::new();
476        let driver = registry
477            .get_driver(ExternalAuthProviderKind::Oidc)
478            .expect("OIDC driver should be registered");
479
480        assert_eq!(driver.kind(), ExternalAuthProviderKind::Oidc);
481    }
482
483    #[test]
484    fn registry_allows_driver_replacement_by_kind() {
485        let mut registry = ExternalAuthProviderRegistry::new();
486        registry
487            .register(TestOidcDriver)
488            .expect("replacement driver should register");
489
490        assert!(registry.contains(ExternalAuthProviderKind::Oidc));
491        #[cfg(feature = "oauth2")]
492        assert!(registry.contains(ExternalAuthProviderKind::GenericOAuth2));
493        #[cfg(feature = "github")]
494        assert!(registry.contains(ExternalAuthProviderKind::GitHub));
495        #[cfg(feature = "google")]
496        assert!(registry.contains(ExternalAuthProviderKind::Google));
497        #[cfg(feature = "microsoft")]
498        assert!(registry.contains(ExternalAuthProviderKind::Microsoft));
499        #[cfg(feature = "qq")]
500        assert!(registry.contains(ExternalAuthProviderKind::Qq));
501    }
502
503    #[test]
504    fn registry_register_rejects_driver_descriptor_kind_mismatch() {
505        let mut registry = ExternalAuthProviderRegistry {
506            drivers: HashMap::new(),
507        };
508
509        let error = registry
510            .register(MismatchedDescriptorDriver)
511            .expect_err("mismatched descriptor should fail");
512
513        assert!(error.to_string().contains(
514            "external auth provider driver 'oidc' returned descriptor for 'generic_oauth2'"
515        ));
516        assert!(!registry.contains(ExternalAuthProviderKind::Oidc));
517    }
518
519    #[test]
520    fn registry_add_rejects_duplicate_kind_without_replacing_existing_driver() {
521        let mut registry = ExternalAuthProviderRegistry {
522            drivers: HashMap::new(),
523        };
524        registry
525            .add(TestOidcDriver)
526            .expect("initial add should work");
527
528        let error = registry
529            .add(TestOidcDriver)
530            .expect_err("duplicate add should fail");
531
532        assert!(
533            error
534                .to_string()
535                .contains("external auth provider driver 'oidc' is already registered")
536        );
537    }
538
539    #[test]
540    fn registry_add_rejects_driver_descriptor_kind_mismatch() {
541        let mut registry = ExternalAuthProviderRegistry {
542            drivers: HashMap::new(),
543        };
544
545        let error = registry
546            .add(MismatchedDescriptorDriver)
547            .expect_err("mismatched descriptor should fail");
548
549        assert!(error.to_string().contains(
550            "external auth provider driver 'oidc' returned descriptor for 'generic_oauth2'"
551        ));
552        assert!(!registry.contains(ExternalAuthProviderKind::Oidc));
553    }
554
555    #[test]
556    fn registry_descriptor_for_returns_registered_descriptor() {
557        let mut registry = ExternalAuthProviderRegistry {
558            drivers: HashMap::new(),
559        };
560        registry
561            .add(TestOidcDriver)
562            .expect("test driver should register");
563
564        let descriptor = registry
565            .descriptor_for(ExternalAuthProviderKind::Oidc)
566            .expect("descriptor should exist");
567
568        assert_eq!(descriptor.kind, ExternalAuthProviderKind::Oidc);
569        assert_eq!(
570            descriptor.protocol,
571            crate::types::ExternalAuthProtocol::Oidc
572        );
573    }
574
575    #[test]
576    fn registry_validate_provider_config_rejects_protocol_mismatch() {
577        let mut registry = ExternalAuthProviderRegistry {
578            drivers: HashMap::new(),
579        };
580        registry
581            .add(TestOidcDriver)
582            .expect("test driver should register");
583        let mut provider = oidc_provider_config();
584        provider.protocol = crate::types::ExternalAuthProtocol::OAuth2;
585
586        let error = registry
587            .validate_provider_config(&provider)
588            .expect_err("protocol mismatch should fail");
589
590        assert!(error.to_string().contains(
591            "external auth provider 'oidc' is configured with protocol 'oauth2' but driver expects 'oidc'"
592        ));
593    }
594
595    #[test]
596    fn registry_driver_for_provider_returns_validated_driver() {
597        let mut registry = ExternalAuthProviderRegistry {
598            drivers: HashMap::new(),
599        };
600        registry
601            .add(TestOidcDriver)
602            .expect("test driver should register");
603        let provider = oidc_provider_config();
604
605        let driver = registry
606            .driver_for_provider(&provider)
607            .expect("valid provider should resolve driver");
608
609        assert_eq!(driver.kind(), ExternalAuthProviderKind::Oidc);
610    }
611
612    #[test]
613    fn registry_with_external_registrations_exposes_configure_hook() {
614        let registry = ExternalAuthProviderRegistry::with_external_registrations(|registry| {
615            if registry.contains(ExternalAuthProviderKind::Oidc) {
616                Ok(())
617            } else {
618                registry.add(TestOidcDriver)
619            }
620        })
621        .expect("external registration hook should add driver");
622
623        assert!(registry.contains(ExternalAuthProviderKind::Oidc));
624    }
625
626    #[test]
627    fn default_registry_is_singleton() {
628        let first = std::ptr::from_ref::<ExternalAuthProviderRegistry>(default_registry());
629        let second = std::ptr::from_ref::<ExternalAuthProviderRegistry>(default_registry());
630
631        assert_eq!(first, second);
632    }
633}