Coverage for src/lexigram/auth/di/sub_providers/oauth2_provider.py: 56%
36 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 12:26 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 12:26 +0800
1# lexigram/auth/providers/oauth2_provider.py
2"""OAuth2 provider - handles OAuth2/OIDC integration only."""
4from __future__ import annotations
6from typing import TYPE_CHECKING, Annotated, Any
8from lexigram.auth.authn.oauth2 import (
9 OAuth2IdentityProvider as OAuth2IdentityProviderConfig,
10)
11from lexigram.auth.authn.oauth2 import OAuth2Manager
12from lexigram.contracts.core import HealthCheckResult, HealthStatus, ProviderPriority
13from lexigram.di.decorators import inject
14from lexigram.di.markers import Inject
15from lexigram.di.provider import Provider
16from lexigram.logging import get_logger
18if TYPE_CHECKING:
19 from lexigram.auth.config import AuthConfig
20 from lexigram.auth.storage.oauth_identity_store import OAuthIdentityStore
21 from lexigram.contracts.core.di import (
22 ContainerRegistrarProtocol,
23 ContainerResolverProtocol,
24 )
26logger = get_logger(__name__)
29@inject
30class OAuth2Provider(Provider):
31 """OAuth2/OIDC integration ONLY."""
33 def __init__(
34 self,
35 config: Annotated[AuthConfig, Inject] | None = None,
36 oauth2_providers: dict[str, dict[str, str]] | None = None,
37 oauth_identity_store: OAuthIdentityStore | None = None,
38 http_client: Any | None = None,
39 **kwargs: Any,
40 ) -> None:
41 super().__init__(name="oauth2", priority=ProviderPriority.SECURITY)
42 self.oauth2_providers = oauth2_providers or (
43 config.oauth2_providers if config else {}
44 )
45 self.oauth_identity_store: OAuthIdentityStore | None = oauth_identity_store
46 self.http_client = http_client
48 @property
49 def identity_resolver(self) -> OAuthIdentityStore | None:
50 """Return the OAuth identity store for resolving external IDs to internal UUIDs.
52 This property exposes the OAuthIdentityStore which implements
53 IdentityResolverProtocol for resolving OAuth external IDs.
54 """
55 return self.oauth_identity_store
57 async def register(self, container: ContainerRegistrarProtocol) -> None:
58 """Register OAuth2 services with the container."""
59 # Convert raw config dicts to OAuth2IdentityProviderConfig instances
60 resolved_providers: dict[str, OAuth2IdentityProviderConfig] = {
61 provider_name: OAuth2IdentityProviderConfig(
62 name=provider_name,
63 client_id=cfg.get("client_id", ""),
64 client_secret=cfg.get("client_secret", ""),
65 authorize_url=cfg.get("authorize_url", ""),
66 access_token_url=cfg.get("access_token_url", ""),
67 userinfo_url=cfg.get("userinfo_url", ""),
68 scope=cfg.get("scope", "openid email profile"),
69 redirect_uri=cfg.get("redirect_uri"),
70 )
71 for provider_name, cfg in self.oauth2_providers.items()
72 }
74 # OAuth2 manager
75 oauth2_manager = OAuth2Manager(
76 providers=resolved_providers,
77 http_client=self.http_client,
78 )
79 container.singleton(OAuth2Manager, lambda: oauth2_manager)
81 # Register OAuthIdentityStore for identity resolution
82 # This enables resolving user IDs from OAuth external IDs
83 from lexigram.auth.storage.oauth_identity_store import (
84 OAuthIdentityStore,
85 )
87 container.singleton(OAuthIdentityStore, lambda: self.oauth_identity_store)
89 # Register individual OAuth2 providers
90 for name, cfg in self.oauth2_providers.items():
91 provider = OAuth2IdentityProviderConfig(
92 name=name,
93 client_id=cfg.get("client_id", ""),
94 client_secret=cfg.get("client_secret", ""),
95 authorize_url=cfg.get("authorize_url", ""),
96 access_token_url=cfg.get("access_token_url", ""),
97 userinfo_url=cfg.get("userinfo_url", ""),
98 scope=cfg.get("scope", "openid email profile"),
99 redirect_uri=cfg.get("redirect_uri"),
100 require_pkce=cfg.get("require_pkce", "true").lower() == "true",
101 )
102 container.singleton(f"oauth2.{name}", lambda p=provider: p)
104 async def boot(self, container: ContainerResolverProtocol) -> None:
105 """Initialize OAuth2 provider."""
106 logger.info("OAuth2Provider started")
108 async def shutdown(self) -> None:
109 """Shutdown OAuth2 provider."""
110 logger.info("OAuth2Provider shutdown")
112 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
113 """Check OAuth2 provider health."""
114 return HealthCheckResult(
115 component=self.name,
116 status=HealthStatus.HEALTHY,
117 details={
118 "service": "oauth2",
119 "providers_count": len(self.oauth2_providers),
120 "identity_store_type": type(self.oauth_identity_store).__name__,
121 },
122 )
125__all__ = [
126 "OAuth2Provider",
127 "logger",
128]