diff --git a/authentik/core/account_selection.py b/authentik/core/account_selection.py index c4c2325a7c..f13020ead1 100644 --- a/authentik/core/account_selection.py +++ b/authentik/core/account_selection.py @@ -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) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 22242e557d..f2d25f6281 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -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.""" diff --git a/authentik/stages/account_selection/stage.py b/authentik/stages/account_selection/stage.py index cc9c783359..b48eff4538 100644 --- a/authentik/stages/account_selection/stage.py +++ b/authentik/stages/account_selection/stage.py @@ -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."""