From 80f5870ebb7996dee89e467f948d0fd8b545c58d Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 14 Oct 2026 23:42:21 +0200 Subject: [PATCH] USER-WP-0039: show account-site and Authelia uid on the account page (NK-WP-0051-T03) The Identity section now names both session uids on separate lines and marks a mismatch, using the existing server-side Authelia /api/state lookup. Unknown Authelia uid reads 'not signed in / unknown'. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 1041694@bnt-lap001 Assistant-Session: 7af0bb68-ac4a-4ce3-8639-24ddd84c0b52 --- src/user_engine/web.py | 49 +++++++++++++-- tests/test_account_identity_disclosure.py | 59 ++++++++++++++++-- ...-WP-0039-sign-in-diagnosis-session-uids.md | 61 +++++++++++++++++++ 3 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 workplans/USER-WP-0039-sign-in-diagnosis-session-uids.md diff --git a/src/user_engine/web.py b/src/user_engine/web.py index d12ef55..1366ced 100644 --- a/src/user_engine/web.py +++ b/src/user_engine/web.py @@ -142,7 +142,10 @@ class PortalApplication: def __call__(self, environ: Mapping[str, Any], start_response: StartResponse) -> Iterable[bytes]: correlation_id = environ.get("HTTP_X_REQUEST_ID") or f"corr_{secrets.token_hex(12)}" navigation_token = _ACCOUNT_NAVIGATION.set("") - cache_token = _REQUEST_CACHE.set({}) + # The cookie header stays in the per-request cache only so the + # Authelia uid can be looked up once while rendering; it is never + # rendered or logged. + cache_token = _REQUEST_CACHE.set({"cookie_header": str(environ.get("HTTP_COOKIE", ""))}) browser_token = _BROWSER_REQUEST.set( "text/html" in str(environ.get("HTTP_ACCEPT", "")) and not str(environ.get("PATH_INFO", "/")).startswith("/api/") @@ -2321,17 +2324,47 @@ Use the login name they provide; it may differ from your display name.

You are not signed in.

" f"{level}" ) - name = escape(actor.preferred_username or actor.subject) verification = "Verified by your identity provider" if actor.assurance else "Verification pending" return ( '

Identity

' - f"

Signed in as {name}.

" - "

This session is for the account site. An application can keep its own session.

" + + self._session_uids(actor) + + "

This session is for the account site. An application can keep its own session.

" f"{level}" f"

{escape(verification)}

" '

Password and one-time code help

' ) + def _session_uids(self, actor: Any) -> str: + """Name the account-site uid and the Authelia uid on separate lines. + + NK-WP-0051-T03 (contract: net-kingdom docs/sign-in-diagnosis.md). + Only the two uids are shown; the Authelia uid comes from the existing + server-side /api/state lookup and is never guessed. + """ + account_uid = actor.preferred_username or actor.subject + authelia_uid = self._netkingdom_name() + lines = ( + '
' + "
Account site session uid
" + f'
{escape(account_uid)}
' + "
Authelia session uid (shared NetKingdom sign-in)
" + + ( + f'
{escape(authelia_uid)}
' + if authelia_uid + else '
not signed in / unknown
' + ) + + "
" + ) + if authelia_uid and authelia_uid != account_uid: + lines += ( + '" + + self._identity_switch_help() + ) + return lines + @staticmethod def _is_exception_account(actor: Any, memberships: tuple[Any, ...]) -> bool: if {"tenant-admin", "platform-operator", "platform-root"}.intersection(actor.roles): @@ -2526,9 +2559,13 @@ Use the login name they provide; it may differ from your display name.

sample.user", body) + self.assertNotIn(b"Signed in as platform-root", body) self.assertNotIn(b'href="/login"', body) - self.assertEqual([], self.app.identity_lookup.headers) + self.assertNotIn(b"super-secret-session", body) + self.assertEqual(1, len(self.app.identity_lookup.headers)) + + # NK-WP-0051-T03: two labelled uid lines and a visible mismatch marker. + def _uid_lines(self, path, lookup_result, cookie="ue_session=member; authelia_session=super-secret-session"): + self.app.identity_lookup = RecordingLookup(lookup_result) + response, body = invoke(self.app, path, cookie=cookie) + self.assertEqual("200 OK", response["status"]) + return body + + def test_signed_in_page_names_both_session_uids(self): + for path in ["/", "/onboarding"]: + with self.subTest(path=path): + body = self._uid_lines(path, "sample.user") + self.assertIn(b'
sample.user
', body) + self.assertIn(b'
sample.user
', body) + self.assertIn(b"Account site session uid", body) + self.assertIn(b"Authelia session uid", body) + self.assertNotIn(b"session-uid-mismatch", body) + self.assertEqual(1, len(self.app.identity_lookup.headers)) + + def test_differing_authelia_uid_is_marked(self): + for path in ["/", "/onboarding"]: + with self.subTest(path=path): + body = self._uid_lines(path, "bernd.worsch-3") + self.assertIn(b'
sample.user
', body) + self.assertIn(b'
bernd.worsch-3
', body) + self.assertIn(b'role="alert" class="session-uid-mismatch"', body) + self.assertIn(b"These sessions differ.", body) + self.assertIn(b"https://kc.example/account/logout", body) + self.assertNotIn(b"super-secret-session", body) + + def test_unknown_authelia_uid_is_not_guessed(self): + cases = [ + (None, "ue_session=member; authelia_session=opaque"), + (TimeoutError("slow"), "ue_session=member; authelia_session=opaque"), + ("", "ue_session=member; authelia_session=opaque"), + ("sample.user", "ue_session=member"), + ] + for result, cookie in cases: + with self.subTest(result=result, cookie=cookie): + body = self._uid_lines("/", result if cookie.count("authelia") else None, cookie) + self.assertIn(b'
not signed in / unknown
', body) + self.assertNotIn(b"session-uid-mismatch", body) + self.assertNotIn(b"", "platform root"]: diff --git a/workplans/USER-WP-0039-sign-in-diagnosis-session-uids.md b/workplans/USER-WP-0039-sign-in-diagnosis-session-uids.md new file mode 100644 index 0000000..8940966 --- /dev/null +++ b/workplans/USER-WP-0039-sign-in-diagnosis-session-uids.md @@ -0,0 +1,61 @@ +--- +id: USER-WP-0039 +type: workplan +title: "Sign-in diagnosis: account-site and Authelia uid on the account page" +domain: communication +repo: user-engine +status: active +owner: claude +topic_slug: communication +created: "2026-10-14" +updated: "2026-10-14" +related: + - NK-WP-0051-T03 + - USER-WP-0036 +--- + +# Sign-in diagnosis: two session uids + +Owner side of NetKingdom NK-WP-0051-T03. Contract: +`net-kingdom/docs/sign-in-diagnosis.md` (only source of field names and the +never-show list). + +Problem: `users.coulomb.social` stayed signed in as `bernd.worsch-3` while +`bao login` reused a different Authelia session, and nothing showed the +mismatch. + +Approach: reuse the existing server-side Authelia `/api/state` lookup +(`identity_state.AutheliaIdentityState`, already configured in +rapp-user-engine via `USER_ENGINE_IDENTITY_STATE_URL` / +`USER_ENGINE_IDENTITY_STATE_HOST`). The account site already receives the +`authelia_session` cookie because Authelia's cookie domain is +`coulomb.social`; no ingress, forward-auth, or cookie-domain change is +needed. The account-site session is unchanged. + +## Show both session uids and mark a mismatch + +```task +id: USER-WP-0039-T01 +status: done +priority: high +``` + +The Identity section on `/` and `/onboarding` shows two labelled lines: +account-site session `uid` and Authelia session `uid`. When they differ a +`role="alert"` marker names both and links the shared-sign-in logout. When +the Authelia uid is unavailable it reads "not signed in / unknown". Only +the two uids are rendered; no email, token, cookie, or OTP. Tests in +`tests/test_account_identity_disclosure.py`. + +## Deploy and record evidence for NK-WP-0051 + +```task +id: USER-WP-0039-T02 +status: todo +priority: high +``` + +Build and roll the image through rapp-user-engine (no manifest change +needed), check the two lines on `users.coulomb.social` with a mismatched +session, and report the image digest to net-kingdom for the contract's +evidence table (`user-engine change`).