# streaming_providers/providers/rtlplus/auth.py import json import base64 from typing import Dict, Any, Optional from ...base.auth.base_auth import BaseAuthToken, TokenAuthLevel from ...base.auth.base_oauth2_auth import BaseOAuth2Authenticator from ...base.utils.logger import logger from .models import RTLPlusClientCredentials, RTLPlusUserCredentials, RTLPlusAuthToken from .constants import RTLPlusDefaults, RTLPlusConfig from ...base.models.proxy_models import ProxyConfig class RTLPlusAuthenticator(BaseOAuth2Authenticator): def __init__(self, credentials=None, config_dir=None, client_version=None, device_id=None, proxy_config: Optional[ProxyConfig] = None, http_manager=None): # Initialize configuration FIRST config_dict = {} if client_version: config_dict['client_version'] = client_version if device_id: config_dict['device_id'] = device_id self._config = RTLPlusConfig(config_dict) self._client_id = None # Get proxy_config if not provided if proxy_config is None: from ...base.network import ProxyConfigManager proxy_mgr = ProxyConfigManager(config_dir) proxy_config = proxy_mgr.get_proxy_config('rtlplus') # Call parent init FIRST super().__init__( provider_name='rtlplus', credentials=credentials, # Pass None if not provided config_dir=config_dir, proxy_config=proxy_config, http_manager=http_manager ) # NOW set default credentials if needed (after super init) if self.credentials is None: self.credentials = self._get_default_credentials() @property def auth_endpoint(self) -> str: """Override auth_endpoint to use our config""" return self.config.auth_endpoint # Required OAuth2 properties @property def oauth_client_id(self) -> str: return self._get_client_id() @property def oauth_scope(self) -> str: return "openid email" @property def oauth_redirect_uri(self) -> str: return self.config.base_website def _get_auth_headers(self) -> Dict[str, str]: """RTL+-specific authentication headers""" return self.config.get_auth_headers() def _build_auth_payload(self) -> Dict[str, Any]: """Build authentication payload from credentials""" return self.credentials.to_auth_payload() def _get_default_credentials(self): """Get default client credentials for anonymous access""" try: # Try to get dynamic credentials first config_creds = self._get_anonymous_credentials_from_config() if config_creds: return RTLPlusClientCredentials( client_id=config_creds.get('client_id', RTLPlusDefaults.ANONYMOUS_CLIENT_ID), client_secret=config_creds.get('client_secret', RTLPlusDefaults.ANONYMOUS_CLIENT_SECRET) ) except Exception as e: logger.warning(f"Could not get dynamic credentials: {e}") # Fallback to default credentials return RTLPlusClientCredentials() def _create_token_from_response(self, response_data: Dict[str, Any]) -> RTLPlusAuthToken: """Create RTL+-specific token from OAuth2 response""" import time return RTLPlusAuthToken( access_token=response_data['access_token'], token_type=response_data.get('token_type', 'Bearer'), expires_in=response_data.get('expires_in', 86400), issued_at=response_data.get('issued_at', time.time()), refresh_token=response_data.get('refresh_token'), refresh_expires_in=response_data.get('refresh_expires_in', 0), not_before_policy=response_data.get('not-before-policy'), scope=response_data.get('scope', '') ) def get_fallback_credentials(self): """Get fallback credentials (anonymous client credentials)""" return self._get_default_credentials() def _classify_token(self, token: BaseAuthToken) -> TokenAuthLevel: """ Classify RTL+ token authentication level by decoding JWT payload Logic: - CLIENT_CREDENTIALS: isGuest=True AND clientId='anonymous-user' - USER_AUTHENTICATED: Has preferred_username OR email claims - UNKNOWN: Cannot determine or invalid token Args: token: Token to classify Returns: TokenAuthLevel indicating the authentication level """ if not token or not token.access_token: logger.debug("RTL+ Cannot classify: No token or access token") return TokenAuthLevel.UNKNOWN try: # Decode JWT without verification to check the payload parts = token.access_token.split('.') if len(parts) < 2: logger.debug("RTL+ Cannot classify: Invalid token format") return TokenAuthLevel.UNKNOWN # Add padding if needed and decode payload_segment = parts[1] padding = 4 - len(payload_segment) % 4 if padding != 4: payload_segment += '=' * padding payload_json = base64.b64decode(payload_segment) payload = json.loads(payload_json) # Extract relevant claims client_id = payload.get('clientId') is_guest = payload.get('isGuest', False) preferred_username = payload.get('preferred_username') email = payload.get('email') logger.debug(f"RTL+ Token JWT payload: clientId={client_id}, isGuest={is_guest}, " f"has_preferred_username={bool(preferred_username)}, has_email={bool(email)}") # Check for user-authenticated token if preferred_username or email: logger.debug("RTL+ Token classified as USER_AUTHENTICATED (has user claims)") return TokenAuthLevel.USER_AUTHENTICATED # Check for client credentials (anonymous) token if is_guest and client_id == 'anonymous-user': logger.debug("RTL+ Token classified as CLIENT_CREDENTIALS (anonymous)") return TokenAuthLevel.CLIENT_CREDENTIALS # Cannot determine logger.debug("RTL+ Token classified as UNKNOWN (no matching criteria)") return TokenAuthLevel.UNKNOWN except Exception as e: logger.warning(f"RTL+ Error classifying token: {e}") return TokenAuthLevel.UNKNOWN def _perform_oauth_authorization_code_flow(self, username: str, password: str) -> Dict[str, Any]: """ RTL+ specific OAuth2 authorization code flow with PKCE Uses base class generic form login """ return self._perform_generic_form_login( username=username, password=password, form_selector_pattern=r'