Clarify account session controls and add authenticator recovery guidance
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
61dc76739f
commit
a9ff77d21d
8 changed files with 395 additions and 26 deletions
77
tests/test_account_clarity.py
Normal file
77
tests/test_account_clarity.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
"""Role-independent browser session state and recovery acceptance checks."""
|
||||
import unittest
|
||||
import test_portal_navigation
|
||||
from test_web import invoke
|
||||
from user_engine.web import PortalApplication
|
||||
|
||||
class AccountClarityTests(unittest.TestCase):
|
||||
setUp = test_portal_navigation.PortalNavigationTests.setUp
|
||||
|
||||
def test_anonymous_and_expired_sessions_have_login_without_logout(self):
|
||||
self.oidc.sessions['operator'].expires_at = 0
|
||||
for cookie in [None, 'ue_session=unknown', 'ue_session=operator']:
|
||||
for path in ['/', '/access-recovery', '/logged-out', '/security']:
|
||||
with self.subTest(path=path, cookie=cookie):
|
||||
response, body = invoke(self.app, path, cookie=cookie)
|
||||
self.assertEqual('200 OK', response['status'])
|
||||
self.assertIn(b'href="/login">Sign in', body)
|
||||
self.assertNotIn(b'href="/logout"', body)
|
||||
self.assertNotIn(b'action="/logout"', body)
|
||||
self.assertNotIn(b'Signed in to this portal as', body)
|
||||
self.assertNotIn(b'You have logged out', body)
|
||||
self.assertEqual('no-store', response['headers']['Cache-Control'])
|
||||
|
||||
def test_authenticated_roles_have_logout_without_login(self):
|
||||
for who in ['operator', 'member']:
|
||||
for path in ['/', '/access-recovery', '/security', '/onboarding']:
|
||||
with self.subTest(path=path, who=who):
|
||||
response, body = invoke(self.app, path, cookie='ue_session='+who)
|
||||
self.assertEqual('200 OK', response['status'])
|
||||
self.assertIn(b'Signed in to this portal as', body)
|
||||
self.assertIn(b'href="/logout"', body)
|
||||
self.assertNotIn(b'href="/login"', body)
|
||||
self.assertNotIn(b'Verify my current identity', body)
|
||||
response, _ = invoke(self.app, '/register', cookie='ue_session='+who)
|
||||
self.assertEqual('/onboarding', response['headers']['Location'])
|
||||
|
||||
def test_wrong_shared_identity_recovery_does_not_claim_a_known_session(self):
|
||||
_, body = invoke(self.app, '/access-recovery')
|
||||
self.assertIn(b'<details><summary>Wrong account', body)
|
||||
self.assertIn(b'https://kc.example/account/logout', body)
|
||||
self.assertIn(b'Use another account', body)
|
||||
self.assertNotIn(b'Sign out of NetKingdom', body)
|
||||
|
||||
def test_browser_denial_is_recoverable_while_api_remains_json(self):
|
||||
for cookie in [None, 'ue_session=member']:
|
||||
response, body = invoke(self.app, '/platform', cookie=cookie, headers={'HTTP_ACCEPT':'text/html'})
|
||||
self.assertEqual('403 Forbidden', response['status'])
|
||||
self.assertIn('text/html', response['headers']['Content-Type'])
|
||||
self.assertIn(b'href="/access-recovery"', body)
|
||||
self.assertIn(b'corr_test', body)
|
||||
self.assertNotIn(b'href="/platform"', body)
|
||||
response, body = invoke(self.app, '/api/v1/me', headers={'HTTP_ACCEPT':'text/html'})
|
||||
self.assertEqual('403 Forbidden', response['status'])
|
||||
self.assertIn('application/json', response['headers']['Content-Type'])
|
||||
self.assertIn(b'"access_denied"', body)
|
||||
|
||||
def test_otp_help_is_available_without_portal_login_and_never_claims_activation(self):
|
||||
response, body = invoke(self.app, '/security', query='enabled=true&username=forged&next=https://evil.example')
|
||||
self.assertEqual('200 OK', response['status'])
|
||||
self.assertIn(b'Authenticator setup is temporarily unavailable', body)
|
||||
self.assertIn(b'cannot currently confirm', body)
|
||||
self.assertNotIn(b'Manage authenticator app</a>', body)
|
||||
for value in [b'forged', b'evil.example', b'<img', b'name="otp"']:
|
||||
self.assertNotIn(value, body)
|
||||
self.app.mfa_management_url='https://authenticator.example/account'
|
||||
_, body = invoke(self.app, '/security')
|
||||
self.assertIn(b'href="https://authenticator.example/account">Manage authenticator app', body)
|
||||
self.assertIn(b'Wait for the sign-in service to confirm activation', body)
|
||||
self.assertIn(b'Opening the setup page does not activate', body)
|
||||
self.assertNotIn(b'Authenticator setup is temporarily unavailable', body)
|
||||
|
||||
def test_provider_handoff_rejects_unsafe_configuration(self):
|
||||
for target in ['http://provider.example', '//provider.example', 'https://u:p@provider.example',
|
||||
'https://provider.example/?next=evil', 'https://provider.example/#secret', 'https://bad host/']:
|
||||
with self.subTest(target=target), self.assertRaises(ValueError):
|
||||
PortalApplication(self.app.service, trusted_proxy_secret=self.app.trusted_proxy_secret,
|
||||
login_url=self.app.login_url, mfa_management_url=target)
|
||||
|
|
@ -39,8 +39,7 @@ class PortalNavigationTests(unittest.TestCase):
|
|||
response, body = self.get(path)
|
||||
self.assertEqual('200 OK', response['status'], path)
|
||||
self.assertIn(b'href="/platform"', body)
|
||||
self.assertIn(b'action="/logout"', body)
|
||||
self.assertIn(b'value="operator-csrf"', body)
|
||||
self.assertIn(b'href="/logout"', body)
|
||||
_, body = self.get('/onboarding')
|
||||
self.assertIn(b'no personal tenant memberships', body)
|
||||
self.assertIn(b'platform operator role', body)
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class PortalApplicationTests(unittest.TestCase):
|
|||
self.assertEqual("ok", json.loads(payload)["status"])
|
||||
home, html = invoke(self.app, "/")
|
||||
self.assertEqual("200 OK", home["status"])
|
||||
self.assertIn(b"Sign in with KeyCape", html)
|
||||
self.assertIn(b'href="/login">Sign in', html)
|
||||
self.assertIn(b'name="viewport"', html)
|
||||
self.assertIn(b"focus-visible", html)
|
||||
self.assertIn(b"<main>", html)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue