diff --git a/lib/streaming_providers/providers/magenta2/provider.py b/lib/streaming_providers/providers/magenta2/provider.py index 1c35151..414588f 100644 --- a/lib/streaming_providers/providers/magenta2/provider.py +++ b/lib/streaming_providers/providers/magenta2/provider.py @@ -233,6 +233,7 @@ class Magenta2Provider(StreamingProvider): # Initialize auth tokens (lazy - populated on first use) self.device_token = None + self._persona_cache = None logger.info("Magenta2 provider initialization completed successfully") @@ -469,15 +470,18 @@ class Magenta2Provider(StreamingProvider): return False def get_persona_token(self, force_refresh: bool = False) -> str: - """ - Get persona token - ONLY authentication entry point - - Raises: - Exception: If persona token cannot be obtained - """ + """Get persona token with simple provider-level caching""" if not self.authenticator.token_flow_manager: raise Exception("TokenFlowManager not initialized") + # Optional: Add simple time-based caching at provider level + if not force_refresh and hasattr(self, '_persona_cache'): + cache_time, cached_token = self._persona_cache + # Cache for 5 minutes at provider level (TokenFlowManager has proper JWT expiry) + if time.time() - cache_time < 300: + return cached_token + + # Get from TokenFlowManager (which has proper JWT expiry caching) persona_result = self.authenticator.token_flow_manager.get_persona_token( force_refresh=force_refresh ) @@ -485,6 +489,12 @@ class Magenta2Provider(StreamingProvider): if not persona_result.success: raise Exception(f"Failed to get persona token: {persona_result.error}") + # Optional: Cache in provider (simple time-based) + if not hasattr(self, '_persona_cache'): + self._persona_cache = (time.time(), persona_result.persona_token) + else: + self._persona_cache = (time.time(), persona_result.persona_token) + return persona_result.persona_token def _ensure_authenticated(self) -> str: diff --git a/lib/streaming_providers/providers/magenta2/token_flow_manager.py b/lib/streaming_providers/providers/magenta2/token_flow_manager.py index 6f819b9..ed3dc29 100644 --- a/lib/streaming_providers/providers/magenta2/token_flow_manager.py +++ b/lib/streaming_providers/providers/magenta2/token_flow_manager.py @@ -14,7 +14,6 @@ from ...base.utils.logger import logger from ...base.auth.session_manager import SessionManager from .sam3_client import Sam3Client from .taa_client import TaaClient -from .token_utils import PersonaTokenComposer @dataclass @@ -87,62 +86,114 @@ class TokenFlowManager: logger.debug(f"TokenFlowManager initialized for {provider_name}" + (f" ({country})" if country else "")) - @staticmethod - def _compose_persona_token(access_token: str) -> Optional[str]: - return PersonaTokenComposer.compose_from_jwt( - jwt_token=access_token, - fallback_account_uri=MAGENTA2_FALLBACK_ACCOUNT_URI - ) - def get_persona_token(self, force_refresh: bool = False) -> PersonaResult: - """ - Get persona token - compose from successful yo_digital token result - """ + """Get persona token with proper JWT expiry caching""" logger.debug("=== GET_PERSONA_TOKEN START ===") - # Get TokenFlowResult from existing method - token_result = self.get_yo_digital_token(force_refresh) - logger.debug(f"token_result.success: {token_result.success}") - logger.debug(f"token_result.access_token present: {bool(token_result.access_token)}") + # Check for cached persona token with proper expiry validation + if not force_refresh: + cached_result = self._get_cached_persona_token() + if cached_result.success: + logger.debug("=== GET_PERSONA_TOKEN SUCCESS (cached) ===") + return cached_result - # If it failed, return the failure - if not token_result.success: + # Get the yo_digital token + token_result = self.get_yo_digital_token(force_refresh) + + if not token_result.success or not token_result.access_token: logger.debug("=== GET_PERSONA_TOKEN FAILED (token_result failed) ===") return PersonaResult( success=False, - error=token_result.error, + error=token_result.error or "No access token" ) - # We have success - extract access_token and compose persona_token - access_token = token_result.access_token + # Compose persona token with expiry information using existing method + from .token_utils import PersonaTokenComposer + composition_result = PersonaTokenComposer.compose_from_jwt( + token_result.access_token, + MAGENTA2_FALLBACK_ACCOUNT_URI + ) - if not access_token: - logger.debug("=== GET_PERSONA_TOKEN FAILED (no access_token) ===") - return PersonaResult( - success=False, - error="Failed to get yo_digital access token" - ) - - logger.debug(f"About to call _compose_persona_token with access_token: {access_token[:50]}...") - - # Compose persona_token - persona_token = self._compose_persona_token(access_token) - - logger.debug(f"_compose_persona_token returned: {persona_token is not None}") - - if not persona_token: + if not composition_result: logger.debug("=== GET_PERSONA_TOKEN FAILED (composition failed) ===") return PersonaResult( success=False, error="Failed to compose persona token" ) + # Cache with the correct expiry (from persona JWT) + self._cache_persona_composition(composition_result) + logger.debug("=== GET_PERSONA_TOKEN SUCCESS ===") return PersonaResult( success=True, - persona_token=persona_token + persona_token=composition_result.persona_token ) + def _get_cached_persona_token(self) -> PersonaResult: + """Check for cached persona token using the actual persona JWT expiry""" + try: + persona_data = self.session_manager.load_scoped_token( + self.provider_name, + 'persona', + self.country + ) + + if (persona_data and + 'persona_token' in persona_data and + 'persona_jwt' in persona_data and + 'expires_at' in persona_data): + + current_time = time.time() + expires_at = persona_data['expires_at'] + + # Check if cached token is still valid using the actual persona JWT expiry + if current_time < (expires_at - 300): # 5-minute buffer + logger.debug(f"Using cached persona token (expires at {time.ctime(expires_at)})") + return PersonaResult( + success=True, + persona_token=persona_data['persona_token'] + ) + else: + logger.debug(f"Cached persona token expired at {time.ctime(expires_at)}") + + except Exception as e: + logger.debug(f"Error checking cached persona token: {e}") + + return PersonaResult(success=False, error="No valid cached token") + + def _cache_persona_composition(self, composition_result) -> None: + """Cache persona composition with proper expiry""" + persona_data = { + 'persona_token': composition_result.persona_token, + 'persona_jwt': composition_result.persona_jwt, # Store for validation + 'expires_at': composition_result.expires_at, + 'composed_at': composition_result.composed_at + } + + success = self.session_manager.save_scoped_token( + self.provider_name, + 'persona', + persona_data, + self.country + ) + + if success: + logger.debug(f"✓ Persona token cached until {time.ctime(composition_result.expires_at)}") + else: + logger.debug("✗ Failed to cache persona token") + + # Keep the existing _compose_persona_token method for backward compatibility + @staticmethod + def _compose_persona_token(access_token: str) -> Optional[str]: + """Backward compatibility method - delegates to new composition""" + from .token_utils import PersonaTokenComposer + result = PersonaTokenComposer.compose_from_jwt( + access_token, + MAGENTA2_FALLBACK_ACCOUNT_URI + ) + return result.persona_token if result else None + def get_yo_digital_token(self, force_refresh: bool = False) -> TokenFlowResult: """ Get yo_digital access token following the complete hierarchy diff --git a/lib/streaming_providers/providers/magenta2/token_utils.py b/lib/streaming_providers/providers/magenta2/token_utils.py index 7498fbe..760eacc 100644 --- a/lib/streaming_providers/providers/magenta2/token_utils.py +++ b/lib/streaming_providers/providers/magenta2/token_utils.py @@ -7,6 +7,7 @@ Consolidates all JWT parsing and persona token composition logic import base64 import json +import time from typing import Dict, Any, Optional from dataclasses import dataclass @@ -181,105 +182,91 @@ class JWTParser: logger.debug(f"Failed to extract raw JWT claims: {e}") return None +@dataclass +class PersonaCompositionResult: + persona_token: str + persona_jwt: str # The actual dc_cts_personaToken JWT + expires_at: float # Expiry from persona JWT + composed_at: float + + def __init__(self, persona_token: str, persona_jwt: str, expires_at: float): + self.persona_token = persona_token + self.persona_jwt = persona_jwt + self.expires_at = expires_at + self.composed_at = time.time() class PersonaTokenComposer: - """Unified persona token composition""" - @staticmethod - def compose_from_jwt(jwt_token: str, - fallback_account_uri: Optional[str] = None) -> Optional[str]: - """ - Compose persona token from JWT access token - - This is the PRIMARY method for persona token composition. - Format: Base64(account_uri + ":" + dc_cts_persona_token) - - Args: - jwt_token: JWT access token containing persona claims - fallback_account_uri: Optional fallback account URI if not in JWT - - Returns: - Base64-encoded persona token or None - """ + def compose_from_jwt(jwt_token: str, fallback_account_uri: str = None) -> Optional[PersonaCompositionResult]: + """Compose persona token and return with expiry information""" try: - # Parse JWT to extract claims claims = JWTParser.parse(jwt_token) if not claims: - logger.error("Failed to parse JWT for persona token composition") return None - # Get persona JWT token (the nested JWT) + # Extract the dc_cts_personaToken (this is the actual persona JWT) persona_jwt = claims.dc_cts_persona_token if not persona_jwt: - logger.error("No dc_cts_persona_token found in JWT claims") + logger.warning("JWT missing dc_cts_personaToken") return None - # Get account URI (prefer JWT, then fallback) + # Get account_uri from claims or use fallback account_uri = claims.account_uri or fallback_account_uri if not account_uri: - logger.error("No account_uri available for persona token composition") + logger.warning("JWT missing account_uri and no fallback provided") return None - # Compose raw token - raw_token = f"{account_uri}:{persona_jwt}" + # Compose the persona token + composed_token = PersonaTokenComposer._compose_token(account_uri, persona_jwt) + if not composed_token: + return None - # Base64 encode - persona_token = base64.b64encode( - raw_token.encode('utf-8') - ).decode('utf-8') + # Extract expiry from the PERSONA JWT (dc_cts_personaToken), not the original JWT + persona_expiry = PersonaTokenComposer._get_jwt_expiry(persona_jwt) + if not persona_expiry: + logger.warning("Could not extract expiry from persona JWT") + return None - logger.info("✓ Persona token composed successfully") - logger.debug(f"Account URI: {account_uri}") - logger.debug(f"Persona token length: {len(persona_token)}") - logger.debug(f"Persona token preview: {persona_token[:50]}...") - - return persona_token + return PersonaCompositionResult( + persona_token=composed_token, + persona_jwt=persona_jwt, + expires_at=persona_expiry + ) except Exception as e: - logger.error(f"Failed to compose persona token from JWT: {e}") + logger.error(f"Error composing persona token: {e}") return None @staticmethod - def compose_from_components(account_uri: str, - dc_cts_persona_token: str) -> Optional[str]: - """ - Compose persona token from explicit components - - Use this when you already have extracted components. - - Args: - account_uri: Account URI (e.g., "http://access.auth.theplatform.com/...") - dc_cts_persona_token: The persona JWT token - - Returns: - Base64-encoded persona token or None - """ + def _get_jwt_expiry(jwt_token: str) -> Optional[float]: + """Extract expiry from any JWT token using existing JWTParser""" try: - if not account_uri or not dc_cts_persona_token: - logger.warning( - f"Cannot compose persona token - " - f"account_uri: {bool(account_uri)}, " - f"dc_cts_persona_token: {bool(dc_cts_persona_token)}" - ) - return None - - # Compose raw token - raw_token = f"{account_uri}:{dc_cts_persona_token}" - - # Base64 encode - persona_token = base64.b64encode( - raw_token.encode('utf-8') - ).decode('utf-8') - - logger.info("✓ Persona token composed from components") - logger.debug(f"Composed token preview: {persona_token[:50]}...") - - return persona_token - + claims = JWTParser.parse(jwt_token) + if claims and claims.raw_claims and 'exp' in claims.raw_claims: + return float(claims.raw_claims['exp']) except Exception as e: - logger.error(f"Failed to compose persona token from components: {e}") + logger.debug(f"Failed to extract expiry from JWT: {e}") + return None + + @staticmethod + def _compose_token(account_uri: str, persona_jwt: str) -> Optional[str]: + """Compose the actual persona token (base64 encoded)""" + try: + import base64 + # Format: account_uri:persona_jwt + token_string = f"{account_uri}:{persona_jwt}" + # Base64 encode + encoded = base64.b64encode(token_string.encode('utf-8')).decode('utf-8') + return encoded + except Exception as e: + logger.error(f"Error composing token: {e}") return None + @staticmethod + def compose_from_components(account_uri: str, dc_cts_persona_token: str) -> Optional[str]: + """Original method - for backward compatibility""" + return PersonaTokenComposer._compose_token(account_uri, dc_cts_persona_token) + @staticmethod def extract_components_from_persona_token(persona_token: str) -> Optional[Dict[str, str]]: """