core: Share account selection user serialization

Centralize the browser-local account display shape so the API and account selection stage do not duplicate it.

Agent-thread: https://sdko.org/internal/threads/019e9417-19b5-7e60-9be4-3f2c423366dd
A7k-product: product
A7k-product-repo: 3
Co-authored-by: Agent <agent@svc.sdko.net>
This commit is contained in:
Dominic R
2026-06-04 15:39:30 -04:00
parent 99a0db2a1e
commit dd95e75250
3 changed files with 38 additions and 30 deletions
+30
View File
@@ -26,6 +26,7 @@ from authentik.flows.planner import (
FlowPlanner,
)
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER
from authentik.lib.avatars import get_avatar
from authentik.lib.utils.urls import is_url_absolute
from authentik.policies.engine import PolicyEngine
from authentik.root.middleware import SessionMiddleware
@@ -60,6 +61,35 @@ def _coerce_known_account(raw_account: object) -> KnownAccount | None:
return KnownAccount(uid=uid, session_key=session_key)
def user_matches_hint(user: User, hint: str) -> bool:
"""Check whether an account matches the supplied login hint."""
return hint in {user.uuid.hex, user.email, user.username}
def serialize_account_selection_user(
request: HttpRequest,
user: User,
hint: str | None = None,
) -> dict[str, object]:
"""Serialize a browser-local account for account selection surfaces."""
is_current = (
request.user.is_authenticated
and not isinstance(request.user, AnonymousUser)
and user.pk == request.user.pk
)
data = {
"uid": user.uuid.hex,
"username": user.username,
"name": user.name,
"email": user.email,
"avatar": get_avatar(user, request),
"is_current": is_current,
}
if hint is not None:
data["is_hint"] = bool(hint and user_matches_hint(user, hint))
return data
def get_known_accounts(request: HttpRequest) -> list[KnownAccount]:
"""Return remembered accounts from the signed browser cookie."""
raw_accounts = request.COOKIES.get(COOKIE_NAME_KNOWN_ACCOUNTS)
+5 -9
View File
@@ -65,7 +65,10 @@ from authentik.api.search.fields import (
from authentik.api.validation import validate
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand
from authentik.core.account_selection import get_known_account_users
from authentik.core.account_selection import (
get_known_account_users,
serialize_account_selection_user,
)
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
JSONDictField,
@@ -811,14 +814,7 @@ class UserViewSet(
def _serialize_account_selection_user(self, user: User) -> dict[str, Any]:
"""Serialize an account remembered by this browser."""
return {
"uid": user.uuid.hex,
"username": user.username,
"name": user.name,
"email": user.email,
"avatar": get_avatar(user, self.request),
"is_current": user.pk == self.request.user.pk,
}
return serialize_account_selection_user(self.request, user)
def _get_account_selection_users(self) -> list[User]:
"""Return active known accounts, including the current session user first."""
+3 -21
View File
@@ -2,7 +2,6 @@
from urllib.parse import urlencode
from django.contrib.auth.models import AnonymousUser
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
@@ -19,9 +18,11 @@ from authentik.core.account_selection import (
get_known_account_session,
get_known_account_users,
get_live_account_session,
serialize_account_selection_user,
set_account_selection_context,
set_session_cookie,
start_fresh_session,
user_matches_hint,
)
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Application, AuthenticatedSession, User
@@ -29,7 +30,6 @@ from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_REDIRECT
from authentik.flows.stage import ChallengeStageView, StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.lib.avatars import get_avatar
LOGGER = get_logger()
COMPONENT = "ak-stage-account-selection"
@@ -47,11 +47,6 @@ class AccountSelectionChallengeUser(PassiveSerializer):
is_hint = BooleanField()
def user_matches_hint(user: User, hint: str) -> bool:
"""Check whether an account matches the supplied login hint."""
return hint in {user.uuid.hex, user.email, user.username}
class AccountSelectionChallenge(Challenge):
"""Challenge for selecting a browser-local account."""
@@ -101,20 +96,7 @@ class AccountSelectionStageView(ChallengeStageView):
def serialize_account(self, user: User, hint: str = "") -> dict[str, object]:
"""Serialize a selectable account."""
is_current = (
self.request.user.is_authenticated
and not isinstance(self.request.user, AnonymousUser)
and user.pk == self.request.user.pk
)
return {
"uid": user.uuid.hex,
"username": user.username,
"name": user.name,
"email": user.email,
"avatar": get_avatar(user, self.request),
"is_current": is_current,
"is_hint": bool(hint and user_matches_hint(user, hint)),
}
return serialize_account_selection_user(self.request, user, hint)
def get_challenge(self) -> AccountSelectionChallenge:
"""Show the current account and live remembered accounts for this browser."""