diff --git a/src/user_engine/identity_state.py b/src/user_engine/identity_state.py
index 8bb7c2b..8dec749 100644
--- a/src/user_engine/identity_state.py
+++ b/src/user_engine/identity_state.py
@@ -17,7 +17,9 @@ from urllib.request import HTTPRedirectHandler, Request, build_opener
from user_engine.oidc import cookie_value
_IDENTITY_NAME = re.compile(r"^[A-Za-z0-9._@+-]{1,200}$")
-_SESSION_TOKEN = re.compile(r"^[A-Za-z0-9._~+/=-]{1,4096}$")
+# RFC 6265 cookie-octet: printable ASCII except space, '"', ',', ';' and '\'.
+# Authelia session ids use characters such as '%', '^' and '!'.
+_SESSION_TOKEN = re.compile(r"^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]{1,4096}$")
_PUBLIC_HOST = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+")
_CLUSTER_HOST = ".svc.cluster.local"
diff --git a/src/user_engine/web.py b/src/user_engine/web.py
index 1366ced..7b338fa 100644
--- a/src/user_engine/web.py
+++ b/src/user_engine/web.py
@@ -68,6 +68,19 @@ def _jsonable(value: Any) -> Any:
return value
+def _assurance_text(level: object) -> str:
+ """Name the account-site session's level from its KeyCape token.
+
+ Authelia's own authentication_level is not shown: KeyCape checks the
+ one-time code, so Authelia reports level 1 even for an aal2 sign-in.
+ """
+ if level == "aal2":
+ return ' · aal2 one-time code verified'
+ if level == "aal1":
+ return ' · aal1 password only'
+ return ""
+
+
class PortalApplication:
"""Small, auditable HTTP adapter over :class:`UserEngineService`."""
@@ -2343,10 +2356,13 @@ Use the login name they provide; it may differ from your display name.
uiduid (shared NetKingdom sign-in)uid", body)
self.assertIn(b"Authelia session uid", body)
@@ -84,7 +84,7 @@ class AccountIdentityDisclosureTests(unittest.TestCase):
for path in ["/", "/onboarding"]:
with self.subTest(path=path):
body = self._uid_lines(path, "bernd.worsch-3")
- self.assertIn(b'