Prove registration collision safety
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

This commit is contained in:
tegwick 2026-08-10 18:37:41 +02:00
parent b80de5a1f4
commit a8f190b7b7
2 changed files with 43 additions and 0 deletions

View file

@ -354,6 +354,42 @@ class PortalApplicationTests(unittest.TestCase):
) )
self.assertEqual("403 Forbidden", replay["status"]) self.assertEqual("403 Forbidden", replay["status"])
def test_public_registration_duplicate_inputs_never_take_over_identity(self):
verifier = FakeRegistrationVerification()
self.app.registration_verification = verifier
self.app.provisioning = FakeProvisioning()
self.app.registration_clients = frozenset({"coulomb-social"})
self.app.registration_tenants = frozenset({"tenant:coulomb"})
self.app.registration_oidc_issuer = "https://kc.example"
self.app.registration_password_setup_origins = frozenset({"https://kc.example"})
def register(username, email):
invoke(self.app, "/api/v1/public/registrations", method="POST", body={
"username": username, "email": email,
"client_id": "coulomb-social", "tenant": "tenant:coulomb",
})
registration_id = verifier.requested.registration_id
verifier.registration_id = registration_id
result, _ = invoke(
self.app, "/api/v1/public/registrations/verify", method="POST",
body={"handle": "x" * 32},
)
return result, self.app.service.store.registration_session(registration_id)
first, first_session = register("first.person", "shared@example.test")
second, second_session = register("second.person", "shared@example.test")
collision, collision_session = register("first.person", "other@example.test")
self.assertEqual("303 See Other", first["status"])
self.assertEqual("303 See Other", second["status"])
self.assertNotEqual(first_session.user_id, second_session.user_id)
self.assertEqual("409 Conflict", collision["status"])
linked = self.app.service.store.find_identity(
"https://kc.example", "first.person"
)
self.assertEqual(first_session.user_id, linked.user_id)
self.assertNotEqual(collision_session.user_id, linked.user_id)
def test_public_registration_rejects_unregistered_client(self): def test_public_registration_rejects_unregistered_client(self):
self.app.registration_verification = FakeRegistrationVerification() self.app.registration_verification = FakeRegistrationVerification()
self.app.registration_clients = frozenset({"coulomb-social"}) self.app.registration_clients = frozenset({"coulomb-social"})

View file

@ -197,3 +197,10 @@ verification expiry/replay redaction, peer rate limiting, identity-link
collision, cross-tenant denial, lifecycle deletion, audit/outbox redaction, collision, cross-tenant denial, lifecycle deletion, audit/outbox redaction,
and request correlation. Deployed consumer conformance remains gated on public and request correlation. Deployed consumer conformance remains gated on public
runtime credentials and activation. runtime credentials and activation.
Duplicate-input conformance now also proves that a verified mailbox reused
with a different username creates a distinct local user instead of implicitly
linking to an existing account. Reusing an existing provider username fails at
the identity-link uniqueness boundary, leaves the original link unchanged, and
never transfers that identity to the later registration. The full suite passes
131 tests with three environment-dependent skips.