Prefer preferred_username for the displayed player name
CI / Build and push docker image (push) Successful in 1m8s

The free-form "name" claim can change and two accounts can share it;
preferred_username is the account's stable handle. display_name() now
falls back preferred_username -> name -> email -> sub, with tests
pinning the claim priority.
This commit is contained in:
2026-10-06 17:53:27 +08:00
parent 18877b8109
commit d2dae476f0
2 changed files with 41 additions and 4 deletions
@@ -47,8 +47,13 @@ def get_ws_user(ws: WebSocket) -> Optional[OIDCUser]:
def display_name(user: OIDCUser) -> str: def display_name(user: OIDCUser) -> str:
"""Best-effort human-readable name for a user.""" """Best-effort human-readable name for a user.
for key in ("name", "preferred_username", "email"):
``preferred_username`` is preferred: it is the account's stable handle,
while ``name`` is a free-form display name a user may change (and two
accounts can share). ``email`` is a last resort before the ``sub``.
"""
for key in ("preferred_username", "name", "email"):
value = user.get(key) value = user.get(key)
if isinstance(value, str) and value: if isinstance(value, str) and value:
return value return value
@@ -11,11 +11,17 @@ import unittest
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any from typing import Any
from kaya.oidc import OIDCConfig, OIDCProviders from kaya.oidc import OIDCConfig, OIDCProviders, OIDCUser
from kaya.session import InMemorySessionStore, Session, SessionMixin from kaya.session import InMemorySessionStore, Session, SessionMixin
from helpers import async_test from helpers import async_test
from tavolo.platform.auth import current_user, get_ws_user, require_auth, session_user from tavolo.platform.auth import (
current_user,
display_name,
get_ws_user,
require_auth,
session_user,
)
def _providers(*names: str) -> OIDCProviders: def _providers(*names: str) -> OIDCProviders:
@@ -87,6 +93,32 @@ class GetWsUserTest(unittest.TestCase):
self.assertIsNone(get_ws_user(SimpleNamespace())) self.assertIsNone(get_ws_user(SimpleNamespace()))
class DisplayNameTest(unittest.TestCase):
"""The claim priority used for the name shown at the table."""
def test_prefers_preferred_username_over_name(self) -> None:
user = OIDCUser(
{"sub": "alice", "preferred_username": "alice", "name": "Alice"}
)
self.assertEqual("alice", display_name(user))
def test_falls_back_to_name(self) -> None:
user = OIDCUser({"sub": "alice", "name": "Alice"})
self.assertEqual("Alice", display_name(user))
def test_falls_back_to_email(self) -> None:
user = OIDCUser({"sub": "alice", "email": "alice@example.com"})
self.assertEqual("alice@example.com", display_name(user))
def test_falls_back_to_sub(self) -> None:
user = OIDCUser({"sub": "alice"})
self.assertEqual("alice", display_name(user))
def test_ignores_empty_and_non_string_claims(self) -> None:
user = OIDCUser({"sub": "alice", "preferred_username": "", "name": 42})
self.assertEqual("alice", display_name(user))
class CurrentUserTest(unittest.TestCase): class CurrentUserTest(unittest.TestCase):
def test_anonymous_session_has_no_user(self) -> None: def test_anonymous_session_has_no_user(self) -> None:
self.assertIsNone(current_user(_providers("default"), _ctx({}))) self.assertIsNone(current_user(_providers("default"), _ctx({})))