USER-WP-0039: show account-site and Authelia uid on the account page (NK-WP-0051-T03)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 58s
Account journey acceptance / journeys (push) Successful in 37s

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 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1041694@bnt-lap001
Assistant-Session: 7af0bb68-ac4a-4ce3-8639-24ddd84c0b52
This commit is contained in:
tegwick 2026-10-14 23:42:21 +02:00
parent 71feaf391b
commit 80f5870ebb
3 changed files with 159 additions and 10 deletions

View file

@ -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.</p></sect
"<p>You are not signed in.</p>"
f"{level}</section>"
)
name = escape(actor.preferred_username or actor.subject)
verification = "Verified by your identity provider" if actor.assurance else "Verification pending"
return (
'<section aria-labelledby="login-state"><h2 id="login-state">Identity</h2>'
f"<p>Signed in as <strong>{name}</strong>.</p>"
"<p>This session is for the account site. An application can keep its own session.</p>"
+ self._session_uids(actor)
+ "<p>This session is for the account site. An application can keep its own session.</p>"
f"{level}"
f"<p>{escape(verification)}</p>"
'<p><a href="/security">Password and one-time code help</a></p></section>'
)
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 = (
'<dl class="session-uids">'
"<dt>Account site session <code>uid</code></dt>"
f'<dd data-session="account-site"><strong>{escape(account_uid)}</strong></dd>'
"<dt>Authelia session <code>uid</code> (shared NetKingdom sign-in)</dt>"
+ (
f'<dd data-session="authelia"><strong>{escape(authelia_uid)}</strong></dd>'
if authelia_uid
else '<dd data-session="authelia">not signed in / unknown</dd>'
)
+ "</dl>"
)
if authelia_uid and authelia_uid != account_uid:
lines += (
'<p role="alert" class="session-uid-mismatch"><strong>These sessions differ.</strong> '
f"Applications that use the shared NetKingdom sign-in, such as OpenBao, "
f"will continue as <strong>{escape(authelia_uid)}</strong>, "
f"not <strong>{escape(account_uid)}</strong>.</p>"
+ 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.</p></sect
if "netkingdom_name" in cache:
return cache["netkingdom_name"]
name: str | None = None
if environ is not None and self.identity_lookup is not None:
cookie_header = (
str(environ.get("HTTP_COOKIE", "")) if environ is not None
else cache.get("cookie_header")
)
if cookie_header is not None and self.identity_lookup is not None:
try:
found = self.identity_lookup.username(str(environ.get("HTTP_COOKIE", "")))
found = self.identity_lookup.username(cookie_header)
except Exception:
found = None
if is_identity_name(found):

View file

@ -55,11 +55,62 @@ class AccountIdentityDisclosureTests(unittest.TestCase):
_, body = invoke(
self.app, "/", cookie="ue_session=member; authelia_session=super-secret-session"
)
self.assertIn(b"Signed in as", body)
self.assertIn(b"sample.user", body)
self.assertNotIn(b"platform-root", body)
# The account-site session stays; the other Authelia uid is only shown.
self.assertIn(b"Signed in as <strong>sample.user</strong>", body)
self.assertNotIn(b"Signed in as <strong>platform-root</strong>", 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'<dd data-session="account-site"><strong>sample.user</strong></dd>', body)
self.assertIn(b'<dd data-session="authelia"><strong>sample.user</strong></dd>', body)
self.assertIn(b"Account site session <code>uid</code>", body)
self.assertIn(b"Authelia session <code>uid</code>", 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'<dd data-session="account-site"><strong>sample.user</strong></dd>', body)
self.assertIn(b'<dd data-session="authelia"><strong>bernd.worsch-3</strong></dd>', 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"),
("<script>x</script>", "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'<dd data-session="authelia">not signed in / unknown</dd>', body)
self.assertNotIn(b"session-uid-mismatch", body)
self.assertNotIn(b"<script>x", body)
def test_without_lookup_authelia_uid_is_unknown(self):
self.app.identity_lookup = None
_, body = invoke(self.app, "/", cookie="ue_session=member; authelia_session=opaque")
self.assertIn(b'<dd data-session="authelia">not signed in / unknown</dd>', body)
self.assertNotIn(b"session-uid-mismatch", body)
self.assertNotIn(b"opaque", body)
def test_lookup_failure_or_unsafe_name_stays_signed_out(self):
for result in [TimeoutError("slow"), "<script>alert(1)</script>", "platform root"]:

View file

@ -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`).