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 += (
+ 'These sessions differ. '
+ f"Applications that use the shared NetKingdom sign-in, such as OpenBao, "
+ f"will continue as {escape(authelia_uid)}, "
+ f"not {escape(account_uid)}.
"
+ + 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`).