Files
ai-video-fullstack/backend/services/openai_realtime/auth.py
T

182 lines
5.5 KiB
Python
Raw Normal View History

"""Authentication for the public OpenAI-compatible Realtime API."""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import secrets
import time
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any
from uuid import uuid4
import settings
from db.models import RealtimeApiKey
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
API_KEY_PREFIX = "sk-rt-"
CLIENT_SECRET_PREFIX = "ek-rt-"
class RealtimeAuthError(ValueError):
"""Raised when a public Realtime credential cannot be accepted."""
@dataclass(frozen=True)
class RealtimeCredential:
api_key_id: str
assistant_id: str | None = None
session: dict[str, Any] | None = None
safety_identifier_hash: str | None = None
ephemeral: bool = False
def _b64encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _b64decode(value: str) -> bytes:
padding = "=" * (-len(value) % 4)
return base64.urlsafe_b64decode(f"{value}{padding}".encode("ascii"))
def _peppered_digest(value: str) -> str:
return hmac.new(
settings.REALTIME_TOKEN_SECRET.encode("utf-8"),
value.encode("utf-8"),
hashlib.sha256,
).hexdigest()
def _token_signature(encoded_payload: str) -> str:
digest = hmac.new(
settings.REALTIME_TOKEN_SECRET.encode("utf-8"),
encoded_payload.encode("ascii"),
hashlib.sha256,
).digest()
return _b64encode(digest)
def hash_safety_identifier(value: str | None) -> str | None:
normalized = (value or "").strip()
if not normalized:
return None
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def create_api_key_value() -> tuple[str, str, str, str]:
key_id = f"rtkey_{uuid4().hex}"
random_secret = secrets.token_urlsafe(32)
value = f"{API_KEY_PREFIX}{key_id[6:18]}.{random_secret}"
return key_id, value, value[:24], _peppered_digest(value)
def create_client_secret(
*,
api_key_id: str,
assistant_id: str,
session: dict[str, Any],
safety_identifier_hash: str | None,
) -> tuple[str, int]:
now = int(time.time())
expires_at = now + settings.REALTIME_CLIENT_SECRET_TTL_SECONDS
payload = {
"sub": api_key_id,
"assistant_id": assistant_id,
"session": session,
"safety_identifier_hash": safety_identifier_hash,
"iat": now,
"exp": expires_at,
"jti": f"rtcs_{uuid4().hex}",
}
encoded = _b64encode(
json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode(
"utf-8"
)
)
return f"{CLIENT_SECRET_PREFIX}{encoded}.{_token_signature(encoded)}", expires_at
async def _active_api_key(
session: AsyncSession,
*,
key_id: str | None = None,
key_prefix: str | None = None,
) -> RealtimeApiKey | None:
if key_id:
row = await session.get(RealtimeApiKey, key_id)
elif key_prefix:
row = (
await session.execute(
select(RealtimeApiKey).where(
RealtimeApiKey.key_prefix == key_prefix
)
)
).scalar_one_or_none()
else:
return None
now = datetime.now(UTC)
if row is None or row.revoked_at is not None:
return None
if row.expires_at is not None and row.expires_at <= now:
return None
return row
async def authenticate_bearer(
session: AsyncSession,
token: str,
) -> RealtimeCredential:
if token.startswith(API_KEY_PREFIX):
prefix = token[:24]
row = await _active_api_key(session, key_prefix=prefix)
if row is None or not hmac.compare_digest(
row.key_hash,
_peppered_digest(token),
):
raise RealtimeAuthError("Invalid or expired API key")
row.last_used_at = datetime.now(UTC)
await session.commit()
return RealtimeCredential(api_key_id=row.id)
if not token.startswith(CLIENT_SECRET_PREFIX):
raise RealtimeAuthError("Unsupported Realtime credential")
compact = token[len(CLIENT_SECRET_PREFIX) :]
try:
encoded, signature = compact.rsplit(".", 1)
except ValueError as exc:
raise RealtimeAuthError("Invalid client secret") from exc
if not hmac.compare_digest(_token_signature(encoded), signature):
raise RealtimeAuthError("Invalid client secret")
try:
payload = json.loads(_b64decode(encoded))
except (ValueError, json.JSONDecodeError) as exc:
raise RealtimeAuthError("Invalid client secret") from exc
if int(payload.get("exp", 0)) < int(time.time()):
raise RealtimeAuthError("Client secret expired")
api_key_id = str(payload.get("sub") or "")
row = await _active_api_key(session, key_id=api_key_id)
if row is None:
raise RealtimeAuthError("Parent API key is no longer active")
row.last_used_at = datetime.now(UTC)
await session.commit()
return RealtimeCredential(
api_key_id=api_key_id,
assistant_id=str(payload.get("assistant_id") or "") or None,
session=(payload.get("session") if isinstance(payload.get("session"), dict) else None),
safety_identifier_hash=str(payload.get("safety_identifier_hash") or "") or None,
ephemeral=True,
)
def bearer_from_authorization(value: str | None) -> str:
scheme, _, token = (value or "").partition(" ")
if scheme.lower() != "bearer" or not token.strip():
raise RealtimeAuthError("Missing Bearer credential")
return token.strip()