signup: Prevent unauthorized signup for realms without EmailAuthBackend.

Zulip supports a configuration where account creation is limited solely
by being able to authenticate with a single-sign on authentication
backend, such as Google Authentication, SAML, or LDAP (i.e., the
organization places no restrictions on email address domains or
invitations being required to join, but has disabled the
EmailAuthBackend that is used for email/password authentication).

A bug in the Zulip server meant that Zulip allowed users to create an
account in such organizations by confirming their email address, without
having an account with the SSO authentication backend.

Co-authored-by: Tim Abbott <tabbott@zulip.com>
This commit is contained in:
Mateusz Mandera
2025-04-02 23:18:04 +08:00
committed by Tim Abbott
parent 2429157498
commit c4bb6509dd
3 changed files with 53 additions and 8 deletions

View File

@@ -41,7 +41,12 @@ from zerver.models.realms import (
get_realm,
)
from zerver.models.users import get_user_by_delivery_email, is_cross_realm_bot_email
from zproject.backends import check_password_strength, email_auth_enabled, email_belongs_to_ldap
from zproject.backends import (
check_password_strength,
email_auth_enabled,
email_belongs_to_ldap,
password_auth_enabled,
)
# We don't mark this error for translation, because it's displayed
# only to MIT users.
@@ -248,6 +253,7 @@ class HomepageForm(forms.Form):
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.realm = kwargs.pop("realm", None)
self.from_multiuse_invite = kwargs.pop("from_multiuse_invite", False)
self.require_password_backend = kwargs.pop("require_password_backend", False)
self.invited_as = kwargs.pop("invited_as", None)
super().__init__(*args, **kwargs)
@@ -267,12 +273,17 @@ class HomepageForm(forms.Form):
)
)
if not from_multiuse_invite and realm.invite_required:
raise ValidationError(
_(
"Please request an invite for {email} from the organization administrator."
).format(email=email)
)
if not from_multiuse_invite:
if realm.invite_required:
raise ValidationError(
_(
"Please request an invite for {email} from the organization administrator."
).format(email=email)
)
if self.require_password_backend and not password_auth_enabled(realm):
raise ValidationError(
_("Can't join the organization: password authentication is not enabled.")
)
try:
email_allowed_for_realm(email, realm)

View File

@@ -1028,7 +1028,7 @@ class LoginTest(ZulipTestCase):
# to sending messages, such as getting the welcome bot, looking up
# the alert words for a realm, etc.
with (
self.assert_database_query_count(93),
self.assert_database_query_count(95),
self.assert_memcached_count(18),
self.captureOnCommitCallbacks(execute=True),
):
@@ -3147,6 +3147,39 @@ class UserSignUpTest(ZulipTestCase):
result = self.client_get("/register", subdomain="", follow=True)
self.assert_in_success_response(["Find your Zulip accounts"], result)
@override_settings(
AUTHENTICATION_BACKENDS=(
"zproject.backends.SAMLAuthBackend",
"zproject.backends.ZulipDummyBackend",
)
)
def test_cant_obtain_confirmation_email_when_email_backend_disabled(self) -> None:
"""
When a realm disables EmailAuthBackend while keeping invite_required set to False,
users must not be allowed to generate a confirmation email to themselves by POSTing
it to the registration endpoints - as that would allow them to sign up and obtain
a logged in session in the realm without actually having to go through the
allowed authentication methods.
"""
realm = get_realm("zulip")
self.assertEqual(realm.invite_required, False)
from django.core.mail import outbox
email = "newuser@zulip.com"
original_outbox_length = len(outbox)
result = self.client_post("/register/", {"email": email})
self.assert_not_in_success_response(["check your email"], result)
self.assert_in_success_response(["Sign up with"], result)
self.assertEqual(original_outbox_length, len(outbox))
result = self.client_post("/accounts/home/", {"email": email})
self.assert_not_in_success_response(["check your email"], result)
self.assert_in_success_response(["Sign up with"], result)
self.assertEqual(original_outbox_length, len(outbox))
@override_settings(
AUTHENTICATION_BACKENDS=(
"zproject.backends.ZulipLDAPAuthBackend",

View File

@@ -1069,6 +1069,7 @@ def accounts_home(
form = HomepageForm(
request.POST,
realm=realm,
require_password_backend=True,
from_multiuse_invite=from_multiuse_invite,
invited_as=invited_as,
)