from abc import ABC, abstractmethod from typing import Any from authlib.integrations.flask_client import OAuth class OAuthProvider(ABC): """Abstract base class for OAuth providers.""" def __init__(self, oauth: OAuth, client_id: str, client_secret: str): self.oauth = oauth self.client_id = client_id self.client_secret = client_secret self._client = None @property @abstractmethod def name(self) -> str: """Provider name (e.g., 'google', 'github').""" @property @abstractmethod def display_name(self) -> str: """Human-readable provider name (e.g., 'Google', 'GitHub').""" @abstractmethod def get_client_config(self) -> dict[str, Any]: """Return OAuth client configuration.""" @abstractmethod def get_user_info(self, token: dict[str, Any]) -> dict[str, Any]: """Extract user information from OAuth token response.""" def get_client(self): """Get or create OAuth client.""" if self._client is None: config = self.get_client_config() self._client = self.oauth.register( name=self.name, client_id=self.client_id, client_secret=self.client_secret, **config, ) return self._client def get_authorization_url(self, redirect_uri: str) -> str: """Generate authorization URL for OAuth flow.""" client = self.get_client() return client.authorize_redirect(redirect_uri).location def exchange_code_for_token( self, code: str = None, redirect_uri: str = None, ) -> dict[str, Any]: """Exchange authorization code for access token.""" client = self.get_client() token = client.authorize_access_token() return token def normalize_user_data(self, user_info: dict[str, Any]) -> dict[str, Any]: """Normalize user data to common format.""" return { "id": user_info.get("id"), "email": user_info.get("email"), "name": user_info.get("name"), "picture": user_info.get("picture"), "provider": self.name, }