128 lines
3.5 KiB
Python
Executable File
128 lines
3.5 KiB
Python
Executable File
"""
|
|
Authentication Module
|
|
- API Key for WordPress ↔ Video-Service communication
|
|
- JWT for streaming token validation
|
|
"""
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
from typing import Annotated
|
|
|
|
import jwt
|
|
from fastapi import Depends, Header, HTTPException, Request, status
|
|
from pydantic import BaseModel
|
|
|
|
from app.config import Settings, get_settings
|
|
|
|
|
|
class TokenPayload(BaseModel):
|
|
"""JWT token payload for video streaming."""
|
|
|
|
video_id: str
|
|
buchung_id: int
|
|
ip: str
|
|
exp: datetime
|
|
|
|
|
|
class StreamToken(BaseModel):
|
|
"""Response model for stream token."""
|
|
|
|
token: str
|
|
expires_at: datetime
|
|
stream_url: str
|
|
|
|
|
|
def verify_api_key(
|
|
x_api_key: Annotated[str | None, Header()] = None,
|
|
settings: Settings = Depends(get_settings),
|
|
) -> bool:
|
|
"""Verify API key from WordPress."""
|
|
if not x_api_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Missing API key",
|
|
)
|
|
if x_api_key != settings.api_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid API key",
|
|
)
|
|
return True
|
|
|
|
|
|
def create_stream_token(
|
|
video_id: str,
|
|
buchung_id: int,
|
|
client_ip: str,
|
|
settings: Settings = Depends(get_settings),
|
|
) -> StreamToken:
|
|
"""Create a JWT token for video streaming with IP binding."""
|
|
expiry = datetime.now(UTC) + timedelta(hours=settings.jwt_expiry_hours)
|
|
|
|
payload = {
|
|
"video_id": video_id,
|
|
"buchung_id": buchung_id,
|
|
"ip": client_ip,
|
|
"exp": expiry,
|
|
"iat": datetime.now(UTC),
|
|
}
|
|
|
|
token = jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
|
|
|
|
# Build stream URL using configured base_url (handles production/development)
|
|
stream_url = (
|
|
f"{settings.base_url}/api/v1/stream/{video_id}/master.m3u8?token={token}"
|
|
)
|
|
|
|
return StreamToken(
|
|
token=token,
|
|
expires_at=expiry,
|
|
stream_url=stream_url,
|
|
)
|
|
|
|
|
|
def verify_stream_token(
|
|
token: str,
|
|
video_id: str,
|
|
request: Request,
|
|
settings: Settings = Depends(get_settings),
|
|
) -> TokenPayload:
|
|
"""Verify JWT token for streaming, including IP binding."""
|
|
try:
|
|
payload = jwt.decode(
|
|
token,
|
|
settings.jwt_secret,
|
|
algorithms=[settings.jwt_algorithm],
|
|
)
|
|
except jwt.ExpiredSignatureError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token has expired",
|
|
) from e
|
|
except jwt.InvalidTokenError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=f"Invalid token: {e}",
|
|
) from e
|
|
|
|
# Verify video_id matches
|
|
if payload.get("video_id") != video_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Token not valid for this video",
|
|
)
|
|
|
|
# Verify IP binding (optional in development)
|
|
client_ip = request.client.host if request.client else "unknown"
|
|
if settings.environment == "production" and payload.get("ip") != client_ip:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="IP address mismatch",
|
|
)
|
|
|
|
return TokenPayload(
|
|
video_id=payload["video_id"],
|
|
buchung_id=payload["buchung_id"],
|
|
ip=payload["ip"],
|
|
exp=datetime.fromtimestamp(payload["exp"], tz=UTC),
|
|
)
|