"""Authentication helpers on top of the kaya-oidc mixin. Scopa has no application roles: every authenticated user may create and join games. Authorization beyond login is game membership, checked against the live game state in Redis. """ from __future__ import annotations from typing import Any, Callable, Mapping, Optional from kaya.core import HttpContext, WebSocket from kaya.oidc import OIDCUser from .app import oidc_mixin def get_ws_user(ws: WebSocket) -> Optional[OIDCUser]: """Return the authenticated user of a WebSocket connection, if any. The session mixin injects ``session`` into the websocket wrapper; the OIDC mixin stores the userinfo there at login. Patched in tests. """ session = getattr(ws, "session", None) if session is None: return None claims = session.get("oidc_user") if not isinstance(claims, Mapping): return None return OIDCUser(claims) def display_name(user: OIDCUser) -> str: """Best-effort human-readable name for a user.""" for key in ("name", "preferred_username", "email"): value = user.get(key) if isinstance(value, str) and value: return value return user.sub def require_auth(handler: Callable[..., Any]) -> Callable[..., Any]: """Decorator: gate a handler on being authenticated (any OIDC user). Responds ``401`` with a JSON error envelope when unauthenticated — unlike kaya's built-in ``OIDCMixin.require_auth`` which redirects to the login page (wrong for a JSON API). """ async def guarded(ctx: HttpContext, *args: Any, **kwargs: Any) -> None: if oidc_mixin.get_user(ctx) is None: await ctx.send_bytes( 401, b'{"error":"unauthenticated"}', {"content-type": ("application/json",)}, ) return await handler(ctx, *args, **kwargs) return guarded