Fix get_manifest

This commit is contained in:
Nirvana
2025-11-19 14:57:57 +01:00
parent fcaa1a7f95
commit 0077b83cc2
5 changed files with 383 additions and 264 deletions
@@ -2,8 +2,6 @@
# -*- coding: utf-8 -*-
import uuid
import time
import base64
import json
from typing import Dict, Optional, Any
from dataclasses import dataclass, field
@@ -18,6 +16,7 @@ from .sam3_client import Sam3Client
from .sso_client import SsoClient
from .taa_client import TaaClient, TaaAuthResult
from .token_flow_manager import TokenFlowManager
from .token_utils import JWTParser, PersonaTokenComposer
from .constants import (
SUPPORTED_COUNTRIES,
@@ -194,66 +193,17 @@ class Magenta2AuthToken(BaseAuthToken):
return base_dict
def compose_persona_token(self) -> Optional[str]:
"""
Compose final persona token from account URI and dc_cts_persona_token
This is the CRITICAL step matching the C++ implementation:
C++: rawToken = accountUri + ":" + dc_cts_personaToken
personaToken = base64_encode(rawToken)
Format: Base64(accountUri + ":" + dc_cts_personaToken)
Example: Base64("urn:theplatform:auth:root:mdeprod:abcd1234-5678-90ef")
"""
if not self.account_uri or not self.dc_cts_persona_token:
logger.warning(
f"Cannot compose persona token - "
f"account_uri: {bool(self.account_uri)}, "
f"dc_cts_persona_token: {bool(self.dc_cts_persona_token)}"
)
return None
try:
# Compose: accountUri + ":" + dc_cts_persona_token
raw_token = f"{self.account_uri}:{self.dc_cts_persona_token}"
# Base64 encode
self.composed_persona_token = base64.b64encode(
raw_token.encode('utf-8')
).decode('utf-8')
logger.info("✓ Persona token composed successfully")
logger.debug(f"Account URI: {self.account_uri}")
logger.debug(f"Composed token preview: {self.composed_persona_token[:50]}...")
return self.composed_persona_token
except Exception as e:
logger.error(f"Failed to compose persona token: {e}")
return None
self.composed_persona_token = PersonaTokenComposer.compose_from_components(
account_uri=self.account_uri,
dc_cts_persona_token=self.dc_cts_persona_token
)
return self.composed_persona_token
def get_jwt_claims(self) -> Optional[Dict[str, Any]]:
"""Extract JWT claims from access token for classification"""
try:
if not self.access_token:
return None
parts = self.access_token.split('.')
if len(parts) != 3:
return None
payload_b64 = parts[1]
padding = len(payload_b64) % 4
if padding:
payload_b64 += '=' * (4 - padding)
payload_json = base64.b64decode(payload_b64).decode('utf-8')
return json.loads(payload_json)
except Exception as e:
logger.debug(f"Failed to extract JWT claims: {e}")
return None
claims = JWTParser.parse(self.access_token)
return claims.raw_claims if claims else None
class Magenta2AuthConfig:
"""Configuration object for Magenta2 authentication"""
@@ -1504,112 +1454,24 @@ class Magenta2Authenticator(BaseOAuth2Authenticator):
logger.error(f"TAA authentication failed: {e}")
raise
def _parse_taa_jwt_complete(self, jwt_token: str) -> Dict[str, Any]:
"""
ENHANCED: Complete JWT parsing extracting ALL required fields
This is critical for persona token composition
"""
try:
parts = jwt_token.split('.')
if len(parts) != 3:
logger.warning("Invalid JWT format")
return {}
# Decode payload
payload_b64 = parts[1]
padding = len(payload_b64) % 4
if padding:
payload_b64 += '=' * (4 - padding)
payload_json = base64.b64decode(payload_b64).decode('utf-8')
claims = json.loads(payload_json)
logger.debug(f"JWT claims found: {list(claims.keys())}")
result = {}
# Enhanced claim mappings - ALL fields from C++ implementation
claim_mappings = {
# Core persona token (most important!)
'dc_cts_persona_token': [
'dc_cts_persona_token',
'personaToken',
'urn:telekom:ott:dc_cts_persona_token'
],
# Account URI (needed for composition!)
'account_uri': [
'dc_cts_account_uri',
'accountUri',
'urn:telekom:ott:dc_cts_account_uri',
'mpxAccountUri'
],
# IDs
'persona_id': [
'dc_cts_personaId',
'personaId',
'urn:telekom:ott:dc_cts_personaId'
],
'account_id': [
'dc_cts_accountId',
'accountId',
'urn:telekom:ott:dc_cts_accountId'
],
'consumer_id': [
'dc_cts_consumerId',
'consumerId',
'urn:telekom:ott:dc_cts_consumerId'
],
'tv_account_id': [
'dc_tvAccountId',
'tvAccountId',
'urn:telekom:ott:dc_tvAccountId'
],
# Account token
'account_token': [
'dc_cts_account_token',
'accountToken',
'urn:telekom:ott:dc_cts_account_token'
],
}
# Extract all claims
for target_key, source_keys in claim_mappings.items():
for source_key in source_keys:
if source_key in claims:
result[target_key] = claims[source_key]
logger.debug(f"Extracted {target_key} from {source_key}")
break
# Extract token expiration
if 'exp' in claims:
result['token_exp'] = claims['exp']
logger.debug(f"Token expires at: {claims['exp']}")
# CRITICAL CHECK: Verify we have the essential fields
if 'dc_cts_persona_token' not in result:
logger.error("CRITICAL: dc_cts_persona_token not found in JWT!")
logger.debug(f"Available claims: {list(claims.keys())}")
if 'account_uri' not in result:
logger.warning("account_uri not found in JWT - will try to construct from MPX account PID")
# Try to construct from MPX account PID if available
if self._mpx_account_pid:
result['account_uri'] = f"urn:theplatform:auth:root:{self._mpx_account_pid}"
logger.info(f"✓ Constructed account_uri from MPX PID: {result['account_uri']}")
elif 'mpxAccountPid' in claims:
result['account_uri'] = f"urn:theplatform:auth:root:{claims['mpxAccountPid']}"
logger.info(f"✓ Constructed account_uri from JWT mpxAccountPid: {result['account_uri']}")
return result
except Exception as e:
logger.error(f"Failed to parse TAA JWT completely: {e}")
@staticmethod
def _parse_taa_jwt_complete(jwt_token: str) -> Dict[str, Any]:
claims = JWTParser.parse(jwt_token)
if not claims:
return {}
# Return as dict for backward compatibility
return {
'dc_cts_persona_token': claims.dc_cts_persona_token,
'account_uri': claims.account_uri,
'persona_id': claims.persona_id,
'account_id': claims.account_id,
'consumer_id': claims.consumer_id,
'tv_account_id': claims.tv_account_id,
'account_token': claims.account_token,
'token_exp': claims.token_exp
}
def _classify_token(self, token: BaseAuthToken) -> TokenAuthLevel:
"""
Classify Magenta2 token based on JWT claims and structure
@@ -128,6 +128,8 @@ MAGENTA2_FALLBACK_ENDPOINTS = {
'ENTITLEMENT': 'https://entitlement.p7s1.io/api/user/entitlement-token',
}
MAGENTA2_FALLBACK_ACCOUNT_URI = 'http://access.auth.theplatform.com/data/Account/2709353023'
# ============================================================================
# Application Configuration
# ============================================================================
@@ -6,15 +6,15 @@ yo_digital → taa → tvhubs → line_auth/remote_login
"""
import time
import base64
import json
from typing import Optional, Dict, Any
from dataclasses import dataclass
from .constants import MAGENTA2_FALLBACK_ACCOUNT_URI
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
@@ -88,91 +88,11 @@ class TokenFlowManager:
(f" ({country})" if country else ""))
@staticmethod
def _extract_jwt_claims(jwt_token: str) -> Dict[str, Any]:
"""Extract claims from JWT token with correct claim names"""
try:
parts = jwt_token.split('.')
if len(parts) != 3:
logger.warning("Invalid JWT format")
return {}
# Decode payload
payload_b64 = parts[1]
padding = len(payload_b64) % 4
if padding:
payload_b64 += '=' * (4 - padding)
payload_json = base64.b64decode(payload_b64).decode('utf-8')
claims = json.loads(payload_json)
logger.debug(f"JWT claims extracted: {list(claims.keys())}")
return claims
except Exception as e:
logger.error(f"Failed to extract JWT claims: {e}")
return {}
def _compose_persona_token(self, access_token: str) -> Optional[str]:
"""
Compose persona token from access_token JWT claims
Format: Base64(account_uri + ":" + persona_jwt_token)
"""
try:
# Extract claims from JWT
claims = self._extract_jwt_claims(access_token)
# Get persona JWT token (this is the nested JWT)
persona_jwt_token = claims.get('dc_cts_personaToken')
if not persona_jwt_token:
logger.error("No persona JWT token found in JWT claims")
return None
# Get account URI
account_uri = None
# 1. Try provider_config first
if hasattr(self, 'provider_config') and self.provider_config:
account_uri = self.provider_config.get_mpx_account_uri()
if account_uri:
logger.debug("Using account URI from provider_config")
# 2. Try to extract from JWT claims
if not account_uri:
account_uri = claims.get('dc_cts_account_uri') or claims.get('accountUri')
if account_uri:
logger.debug("Using account URI from JWT claims")
# 3. Final fallback - use the same format as your valid example
if not account_uri:
account_uri = "http://access.auth.theplatform.com/data/Account/2709353023"
logger.debug("Using fallback account URI")
if not account_uri:
logger.error("No account URI available from any source")
return None
logger.debug(f"Using account URI: {account_uri}")
logger.debug(f"Persona JWT token length: {len(persona_jwt_token)}")
logger.debug(f"Persona JWT token preview: {persona_jwt_token[:50]}...")
# CRITICAL: Compose raw token as account_uri + ":" + persona_jwt_token
raw_token = f"{account_uri}:{persona_jwt_token}"
logger.debug(f"Raw token length before encoding: {len(raw_token)}")
# Base64 encode
persona_token = base64.b64encode(raw_token.encode('utf-8')).decode('utf-8')
logger.info("✓ Persona token composed successfully")
logger.debug(f"Final persona token length: {len(persona_token)}")
logger.debug(f"Final persona token preview: {persona_token[:50]}...")
return persona_token
except Exception as e:
logger.error(f"Failed to compose persona token: {e}")
return None
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:
"""
@@ -0,0 +1,348 @@
# streaming_providers/providers/magenta2/token_utils.py
# -*- coding: utf-8 -*-
"""
Unified JWT and Persona Token utilities for Magenta2
Consolidates all JWT parsing and persona token composition logic
"""
import base64
import json
from typing import Dict, Any, Optional
from dataclasses import dataclass
from ...base.utils.logger import logger
@dataclass
class JWTClaims:
"""Structured JWT claims for Magenta2 tokens"""
# Raw claims
raw_claims: Dict[str, Any]
# Persona token components
dc_cts_persona_token: Optional[str] = None
account_uri: Optional[str] = None
# User identifiers
persona_id: Optional[str] = None
account_id: Optional[str] = None
consumer_id: Optional[str] = None
tv_account_id: Optional[str] = None
# Token metadata
account_token: Optional[str] = None
token_exp: Optional[int] = None
client_id: Optional[str] = None
# SSO fields
sso_user_id: Optional[str] = None
sso_display_name: Optional[str] = None
def has_persona_components(self) -> bool:
"""Check if claims contain components needed for persona token"""
return bool(self.dc_cts_persona_token and self.account_uri)
def is_user_token(self) -> bool:
"""Check if this is a user-authenticated token"""
return bool(
self.persona_id or
self.account_id or
self.consumer_id or
self.tv_account_id
)
class JWTParser:
"""Unified JWT parsing utilities"""
# Comprehensive claim mappings for all Magenta2 token types
CLAIM_MAPPINGS = {
'dc_cts_persona_token': [
'dc_cts_persona_token',
'dc_cts_personaToken',
'personaToken',
'urn:telekom:ott:dc_cts_persona_token'
],
'account_uri': [
'dc_cts_account_uri',
'accountUri',
'urn:telekom:ott:dc_cts_account_uri',
'mpxAccountUri'
],
'persona_id': [
'dc_cts_personaId',
'personaId',
'urn:telekom:ott:dc_cts_personaId'
],
'account_id': [
'dc_cts_accountId',
'accountId',
'urn:telekom:ott:dc_cts_accountId'
],
'consumer_id': [
'dc_cts_consumerId',
'consumerId',
'urn:telekom:ott:dc_cts_consumerId'
],
'tv_account_id': [
'dc_tvAccountId',
'tvAccountId',
'urn:telekom:ott:dc_tvAccountId'
],
'account_token': [
'dc_cts_account_token',
'accountToken',
'urn:telekom:ott:dc_cts_account_token'
],
}
@staticmethod
def parse(jwt_token: str) -> Optional[JWTClaims]:
"""
Parse JWT token and extract all Magenta2-specific claims
Args:
jwt_token: JWT token string
Returns:
JWTClaims object or None if parsing fails
"""
try:
# Decode JWT
parts = jwt_token.split('.')
if len(parts) != 3:
logger.warning("Invalid JWT format - expected 3 parts")
return None
# Decode payload with padding
payload_b64 = parts[1]
padding = len(payload_b64) % 4
if padding:
payload_b64 += '=' * (4 - padding)
payload_json = base64.b64decode(payload_b64).decode('utf-8')
raw_claims = json.loads(payload_json)
logger.debug(f"JWT parsed successfully - claims: {list(raw_claims.keys())}")
# Extract structured claims
claims = JWTClaims(raw_claims=raw_claims)
# Map all known claims
for target_key, source_keys in JWTParser.CLAIM_MAPPINGS.items():
for source_key in source_keys:
if source_key in raw_claims:
setattr(claims, target_key, raw_claims[source_key])
logger.debug(f"Mapped {target_key} from {source_key}")
break
# Extract standard JWT fields
claims.token_exp = raw_claims.get('exp')
claims.client_id = raw_claims.get('client_id', raw_claims.get('clientId'))
# Log critical missing fields
if not claims.dc_cts_persona_token:
logger.warning("JWT missing dc_cts_persona_token")
if not claims.account_uri:
logger.warning("JWT missing account_uri")
return claims
except Exception as e:
logger.error(f"Failed to parse JWT token: {e}")
return None
@staticmethod
def extract_raw_claims(jwt_token: str) -> Optional[Dict[str, Any]]:
"""
Extract raw claims dictionary without mapping
Useful for debugging or custom claim extraction
Args:
jwt_token: JWT token string
Returns:
Dictionary of raw claims or None
"""
try:
parts = jwt_token.split('.')
if len(parts) != 3:
return None
payload_b64 = parts[1]
padding = len(payload_b64) % 4
if padding:
payload_b64 += '=' * (4 - padding)
payload_json = base64.b64decode(payload_b64).decode('utf-8')
return json.loads(payload_json)
except Exception as e:
logger.debug(f"Failed to extract raw JWT claims: {e}")
return None
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
"""
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)
persona_jwt = claims.dc_cts_persona_token
if not persona_jwt:
logger.error("No dc_cts_persona_token found in JWT claims")
return None
# Get account URI (prefer JWT, then fallback)
account_uri = claims.account_uri or fallback_account_uri
if not account_uri:
logger.error("No account_uri available for persona token composition")
return None
# Compose raw token
raw_token = f"{account_uri}:{persona_jwt}"
# Base64 encode
persona_token = base64.b64encode(
raw_token.encode('utf-8')
).decode('utf-8')
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
except Exception as e:
logger.error(f"Failed to compose persona token from JWT: {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
"""
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
except Exception as e:
logger.error(f"Failed to compose persona token from components: {e}")
return None
@staticmethod
def extract_components_from_persona_token(persona_token: str) -> Optional[Dict[str, str]]:
"""
Extract components from an existing persona token
Useful for debugging or token analysis
Args:
persona_token: Base64-encoded persona token
Returns:
Dictionary with 'account_uri' and 'persona_jwt' or None
"""
try:
# Decode base64
decoded = base64.b64decode(persona_token).decode('utf-8')
# Find the last colon (after the account URI which may contain colons)
last_colon_index = decoded.rfind(':')
if last_colon_index == -1:
logger.error("No colon found in decoded persona token")
return None
account_uri = decoded[:last_colon_index]
persona_jwt = decoded[last_colon_index + 1:]
# Verify persona_jwt looks like a JWT
if not persona_jwt.startswith('eyJ'):
logger.warning(f"Extracted token doesn't look like a JWT: {persona_jwt[:20]}...")
return {
'account_uri': account_uri,
'persona_jwt': persona_jwt
}
except Exception as e:
logger.error(f"Failed to extract components from persona token: {e}")
return None
class TokenValidator:
"""Token validation utilities"""
@staticmethod
def is_jwt_token(token: str) -> bool:
"""Check if string is a valid JWT token format"""
try:
parts = token.split('.')
return len(parts) == 3 and parts[0].startswith('eyJ')
except:
return False
@staticmethod
def is_persona_token(token: str) -> bool:
"""Check if string is a valid persona token format"""
try:
# Should be base64 encoded
decoded = base64.b64decode(token).decode('utf-8')
# Should contain a colon and the part after should look like a JWT
last_colon = decoded.rfind(':')
if last_colon == -1:
return False
persona_jwt = decoded[last_colon + 1:]
return persona_jwt.startswith('eyJ')
except:
return False
+1 -14
View File
@@ -68,20 +68,7 @@
</category>
<category id="magenta2" label="Magenta TV 2.0 (DE)">
<setting id="magenta2_username" type="text" label="Username" default="">
<level>0</level>
<constraints>
<allowempty>false</allowempty>
</constraints>
</setting>
<setting id="magenta2_password" type="text" label="Password" default="" option="hidden">
<level>0</level>
<constraints>
<allowempty>false</allowempty>
</constraints>
</setting>
<setting id="enable_magenta2" type="bool" label="Enable Magenta TV 2.0 (DE)" default="false" />
<setting id="magenta2_proxy_enabled" type="bool" label="Enable Proxy" default="false" />
<setting id="magenta2_proxy_host" type="text" label="Proxy Host" visible="eq(-1,true)" />
<setting id="magenta2_proxy_port" type="number" label="Proxy Port" visible="eq(-2,true)" />