Files
video-service/app/api/auth.py

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),
)