mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-02 22:12:29 +02:00
345 lines
11 KiB
Python
345 lines
11 KiB
Python
# 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
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, Optional
|
|
|
|
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
|
|
|
|
|
|
@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:
|
|
@staticmethod
|
|
def compose_from_jwt(
|
|
jwt_token: str, fallback_account_uri: str = None
|
|
) -> Optional[PersonaCompositionResult]:
|
|
"""Compose persona token and return with expiry information"""
|
|
logger.debug(f"🟢 PersonaTokenComposer.compose_from_jwt START")
|
|
try:
|
|
logger.debug(f"🟢 Input JWT token length: {len(jwt_token) if jwt_token else 0}")
|
|
|
|
claims = JWTParser.parse(jwt_token)
|
|
if not claims:
|
|
logger.error("🔴 JWTParser.parse returned None")
|
|
return None
|
|
|
|
# Extract the dc_cts_personaToken (this is the actual persona JWT)
|
|
persona_jwt = claims.dc_cts_persona_token
|
|
if not persona_jwt:
|
|
logger.warning("🔴 JWT missing dc_cts_personaToken")
|
|
return None
|
|
|
|
# Get account_uri from claims or use fallback
|
|
account_uri = claims.account_uri or fallback_account_uri
|
|
if not account_uri:
|
|
logger.warning("🔴 JWT missing account_uri and no fallback provided")
|
|
return None
|
|
|
|
# Compose the persona token
|
|
composed_token = PersonaTokenComposer._compose_token(account_uri, persona_jwt)
|
|
if not composed_token:
|
|
logger.error("🔴 _compose_token returned None")
|
|
return None
|
|
|
|
# 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.debug(f"🟢 PersonaTokenComposer.compose_from_jwt SUCCESS")
|
|
return PersonaCompositionResult(
|
|
persona_token=composed_token,
|
|
persona_jwt=persona_jwt,
|
|
expires_at=persona_expiry,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"🔴 Error in PersonaTokenComposer.compose_from_jwt: {e}")
|
|
import traceback
|
|
|
|
logger.error(f"🔴 FULL TRACEBACK: {traceback.format_exc()}")
|
|
return None
|
|
|
|
@staticmethod
|
|
def _get_jwt_expiry(jwt_token: str) -> Optional[float]:
|
|
"""Extract expiry from any JWT token using existing JWTParser"""
|
|
try:
|
|
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.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]]:
|
|
"""
|
|
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
|