Files
ai-video-fullstack/backend/services/openai_realtime/auth.py
Xin Wang 86639692ba feat: implement OpenAI-compatible Realtime API with authentication and management features
- Added support for public Realtime API, including new routes for managing API keys and handling WebRTC connections.
- Introduced RealtimeApiKey model and associated CRUD operations for admin management of API keys.
- Implemented authentication mechanisms for API keys and client secrets.
- Enhanced environment configuration with new secrets for Realtime API.
- Created OpenAIRealtime session management and event processing for real-time interactions.
- Updated schemas and settings to accommodate new features and ensure compatibility with existing systems.
2026-08-11 10:05:55 +08:00

182 lines
5.5 KiB
Python

"""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()