mirror of https://github.com/zulip/zulip.git
auth: Include user-input email in some error messages in the login form.
Fixes #13126.
This commit is contained in:
parent
fb3864ea3c
commit
c5806d9728
|
@ -92,7 +92,7 @@ page can be easily identified in it's respective JavaScript file. -->
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if is_deactivated %}
|
{% if deactivated_account_error %}
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
{{ deactivated_account_error }}
|
{{ deactivated_account_error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -49,12 +49,12 @@ MIT_VALIDATION_ERROR = (
|
||||||
+ '<a href="mailto:support@zulip.com">contact us</a>.'
|
+ '<a href="mailto:support@zulip.com">contact us</a>.'
|
||||||
)
|
)
|
||||||
WRONG_SUBDOMAIN_ERROR = (
|
WRONG_SUBDOMAIN_ERROR = (
|
||||||
"Your Zulip account is not a member of the "
|
"Your Zulip account {username} is not a member of the "
|
||||||
+ "organization associated with this subdomain. "
|
+ "organization associated with this subdomain. "
|
||||||
+ "Please contact your organization administrator with any questions."
|
+ "Please contact your organization administrator with any questions."
|
||||||
)
|
)
|
||||||
DEACTIVATED_ACCOUNT_ERROR = (
|
DEACTIVATED_ACCOUNT_ERROR = (
|
||||||
"Your account is no longer active. "
|
"Your account {username} is no longer active. "
|
||||||
+ "Please contact your organization administrator to reactivate it."
|
+ "Please contact your organization administrator to reactivate it."
|
||||||
)
|
)
|
||||||
PASSWORD_RESET_NEEDED_ERROR = (
|
PASSWORD_RESET_NEEDED_ERROR = (
|
||||||
|
@ -432,13 +432,15 @@ class OurAuthenticationForm(AuthenticationForm):
|
||||||
# We exclude mirror dummy accounts here. They should be treated as the
|
# We exclude mirror dummy accounts here. They should be treated as the
|
||||||
# user never having had an account, so we let them fall through to the
|
# user never having had an account, so we let them fall through to the
|
||||||
# normal invalid_login case below.
|
# normal invalid_login case below.
|
||||||
raise ValidationError(mark_safe(DEACTIVATED_ACCOUNT_ERROR))
|
error_message = DEACTIVATED_ACCOUNT_ERROR.format(username=username)
|
||||||
|
raise ValidationError(mark_safe(error_message))
|
||||||
|
|
||||||
if return_data.get("invalid_subdomain"):
|
if return_data.get("invalid_subdomain"):
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"User %s attempted password login to wrong subdomain %s", username, subdomain
|
"User %s attempted password login to wrong subdomain %s", username, subdomain
|
||||||
)
|
)
|
||||||
raise ValidationError(mark_safe(WRONG_SUBDOMAIN_ERROR))
|
error_message = WRONG_SUBDOMAIN_ERROR.format(username=username)
|
||||||
|
raise ValidationError(mark_safe(error_message))
|
||||||
|
|
||||||
if self.user_cache is None:
|
if self.user_cache is None:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
|
|
|
@ -168,7 +168,10 @@ class AuthBackendTest(ZulipTestCase):
|
||||||
if isinstance(backend, SocialAuthMixin):
|
if isinstance(backend, SocialAuthMixin):
|
||||||
# Returns a redirect to login page with an error.
|
# Returns a redirect to login page with an error.
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertEqual(result.url, user_profile.realm.uri + "/login/?is_deactivated=true")
|
self.assertEqual(
|
||||||
|
result.url,
|
||||||
|
f"{user_profile.realm.uri}/login/?is_deactivated={user_profile.delivery_email}",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Just takes you back to the login page treating as
|
# Just takes you back to the login page treating as
|
||||||
# invalid auth; this is correct because the form will
|
# invalid auth; this is correct because the form will
|
||||||
|
@ -1098,7 +1101,10 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase):
|
||||||
account_data_dict, expect_choose_email_screen=True, subdomain="zulip"
|
account_data_dict, expect_choose_email_screen=True, subdomain="zulip"
|
||||||
)
|
)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertEqual(result.url, user_profile.realm.uri + "/login/?is_deactivated=true")
|
self.assertEqual(
|
||||||
|
result.url,
|
||||||
|
f"{user_profile.realm.uri}/login/?is_deactivated={user_profile.delivery_email}",
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.output,
|
m.output,
|
||||||
[
|
[
|
||||||
|
@ -1107,7 +1113,11 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase):
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
# TODO: verify whether we provide a clear error message
|
|
||||||
|
result = self.client_get(result.url)
|
||||||
|
self.assert_in_success_response(
|
||||||
|
[f"Your account {user_profile.delivery_email} is no longer active."], result
|
||||||
|
)
|
||||||
|
|
||||||
def test_social_auth_invalid_realm(self) -> None:
|
def test_social_auth_invalid_realm(self) -> None:
|
||||||
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
||||||
|
|
|
@ -1218,8 +1218,10 @@ class InactiveUserTest(ZulipTestCase):
|
||||||
user_profile = self.example_user("hamlet")
|
user_profile = self.example_user("hamlet")
|
||||||
do_deactivate_user(user_profile, acting_user=None)
|
do_deactivate_user(user_profile, acting_user=None)
|
||||||
|
|
||||||
result = self.login_with_return(self.example_email("hamlet"))
|
result = self.login_with_return(user_profile.delivery_email)
|
||||||
self.assert_in_response("Your account is no longer active.", result)
|
self.assert_in_response(
|
||||||
|
"Your account {} is no longer active.".format(user_profile.delivery_email), result
|
||||||
|
)
|
||||||
|
|
||||||
def test_login_deactivated_mirror_dummy(self) -> None:
|
def test_login_deactivated_mirror_dummy(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -1260,7 +1262,10 @@ class InactiveUserTest(ZulipTestCase):
|
||||||
form = OurAuthenticationForm(request, payload)
|
form = OurAuthenticationForm(request, payload)
|
||||||
with self.settings(AUTHENTICATION_BACKENDS=("zproject.backends.EmailAuthBackend",)):
|
with self.settings(AUTHENTICATION_BACKENDS=("zproject.backends.EmailAuthBackend",)):
|
||||||
self.assertFalse(form.is_valid())
|
self.assertFalse(form.is_valid())
|
||||||
self.assertIn("Your account is no longer active", str(form.errors))
|
self.assertIn(
|
||||||
|
"Your account {} is no longer active".format(user_profile.delivery_email),
|
||||||
|
str(form.errors),
|
||||||
|
)
|
||||||
|
|
||||||
def test_webhook_deactivated_user(self) -> None:
|
def test_webhook_deactivated_user(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -735,9 +735,11 @@ class LoginTest(ZulipTestCase):
|
||||||
def test_login_deactivated_user(self) -> None:
|
def test_login_deactivated_user(self) -> None:
|
||||||
user_profile = self.example_user("hamlet")
|
user_profile = self.example_user("hamlet")
|
||||||
do_deactivate_user(user_profile, acting_user=None)
|
do_deactivate_user(user_profile, acting_user=None)
|
||||||
result = self.login_with_return(self.example_email("hamlet"), "xxx")
|
result = self.login_with_return(user_profile.delivery_email, "xxx")
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
self.assert_in_response("Your account is no longer active.", result)
|
self.assert_in_response(
|
||||||
|
"Your account {} is no longer active.".format(user_profile.delivery_email), result
|
||||||
|
)
|
||||||
self.assert_logged_in_user_id(None)
|
self.assert_logged_in_user_id(None)
|
||||||
|
|
||||||
def test_login_bad_password(self) -> None:
|
def test_login_bad_password(self) -> None:
|
||||||
|
@ -809,8 +811,9 @@ class LoginTest(ZulipTestCase):
|
||||||
self.assert_logged_in_user_id(None)
|
self.assert_logged_in_user_id(None)
|
||||||
|
|
||||||
def test_login_wrong_subdomain(self) -> None:
|
def test_login_wrong_subdomain(self) -> None:
|
||||||
|
email = self.mit_email("sipbtest")
|
||||||
with self.assertLogs(level="WARNING") as m:
|
with self.assertLogs(level="WARNING") as m:
|
||||||
result = self.login_with_return(self.mit_email("sipbtest"), "xxx")
|
result = self.login_with_return(email, "xxx")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.output,
|
m.output,
|
||||||
[
|
[
|
||||||
|
@ -818,11 +821,11 @@ class LoginTest(ZulipTestCase):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
self.assert_in_response(
|
expected_error = (
|
||||||
"Your Zulip account is not a member of the "
|
f"Your Zulip account {email} is not a member of the "
|
||||||
"organization associated with this subdomain.",
|
+ "organization associated with this subdomain."
|
||||||
result,
|
|
||||||
)
|
)
|
||||||
|
self.assert_in_response(expected_error, result)
|
||||||
self.assert_logged_in_user_id(None)
|
self.assert_logged_in_user_id(None)
|
||||||
|
|
||||||
def test_login_invalid_subdomain(self) -> None:
|
def test_login_invalid_subdomain(self) -> None:
|
||||||
|
@ -5311,6 +5314,12 @@ class TestLoginPage(ZulipTestCase):
|
||||||
self.assertEqual(result.status_code, 400)
|
self.assertEqual(result.status_code, 400)
|
||||||
self.assert_in_response("Authentication subdomain", result)
|
self.assert_in_response("Authentication subdomain", result)
|
||||||
|
|
||||||
|
def test_login_page_is_deactivated_validation(self) -> None:
|
||||||
|
with patch("zerver.views.auth.logging.info") as mock_info:
|
||||||
|
result = self.client_get("/login/?is_deactivated=invalid_email")
|
||||||
|
mock_info.assert_called_once()
|
||||||
|
self.assert_not_in_success_response(["invalid_email"], result)
|
||||||
|
|
||||||
|
|
||||||
class TestFindMyTeam(ZulipTestCase):
|
class TestFindMyTeam(ZulipTestCase):
|
||||||
def test_template(self) -> None:
|
def test_template(self) -> None:
|
||||||
|
|
|
@ -12,15 +12,19 @@ from django.contrib.auth import authenticate
|
||||||
from django.contrib.auth.views import LoginView as DjangoLoginView
|
from django.contrib.auth.views import LoginView as DjangoLoginView
|
||||||
from django.contrib.auth.views import PasswordResetView as DjangoPasswordResetView
|
from django.contrib.auth.views import PasswordResetView as DjangoPasswordResetView
|
||||||
from django.contrib.auth.views import logout_then_login as django_logout_then_login
|
from django.contrib.auth.views import logout_then_login as django_logout_then_login
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import validate_email
|
||||||
from django.forms import Form
|
from django.forms import Form
|
||||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, HttpResponseServerError
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, HttpResponseServerError
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template.response import SimpleTemplateResponse
|
from django.template.response import SimpleTemplateResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.http import url_has_allowed_host_and_scheme
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_safe
|
from django.views.decorators.http import require_safe
|
||||||
|
from jinja2.utils import Markup as mark_safe
|
||||||
from social_django.utils import load_backend, load_strategy
|
from social_django.utils import load_backend, load_strategy
|
||||||
from two_factor.forms import BackupTokenForm
|
from two_factor.forms import BackupTokenForm
|
||||||
from two_factor.views import LoginView as BaseTwoFactorLoginView
|
from two_factor.views import LoginView as BaseTwoFactorLoginView
|
||||||
|
@ -670,13 +674,22 @@ def redirect_to_deactivation_notice() -> HttpResponse:
|
||||||
|
|
||||||
|
|
||||||
def update_login_page_context(request: HttpRequest, context: Dict[str, Any]) -> None:
|
def update_login_page_context(request: HttpRequest, context: Dict[str, Any]) -> None:
|
||||||
for key in ("email", "already_registered", "is_deactivated"):
|
for key in ("email", "already_registered"):
|
||||||
try:
|
try:
|
||||||
context[key] = request.GET[key]
|
context[key] = request.GET[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
context["deactivated_account_error"] = DEACTIVATED_ACCOUNT_ERROR
|
deactivated_email = request.GET.get("is_deactivated")
|
||||||
|
if deactivated_email is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
validate_email(deactivated_email)
|
||||||
|
context["deactivated_account_error"] = mark_safe(
|
||||||
|
DEACTIVATED_ACCOUNT_ERROR.format(username=escape(deactivated_email))
|
||||||
|
)
|
||||||
|
except ValidationError:
|
||||||
|
logging.info("Invalid email in is_deactivated param to login page: %s", deactivated_email)
|
||||||
|
|
||||||
|
|
||||||
class TwoFactorLoginView(BaseTwoFactorLoginView):
|
class TwoFactorLoginView(BaseTwoFactorLoginView):
|
||||||
|
|
|
@ -73,6 +73,7 @@ from zerver.lib.rate_limiter import RateLimitedObject
|
||||||
from zerver.lib.redis_utils import get_dict_from_redis, get_redis_client, put_dict_in_redis
|
from zerver.lib.redis_utils import get_dict_from_redis, get_redis_client, put_dict_in_redis
|
||||||
from zerver.lib.request import RequestNotes
|
from zerver.lib.request import RequestNotes
|
||||||
from zerver.lib.subdomains import get_subdomain
|
from zerver.lib.subdomains import get_subdomain
|
||||||
|
from zerver.lib.url_encoding import add_query_to_redirect_url
|
||||||
from zerver.lib.users import check_full_name, validate_user_custom_profile_field
|
from zerver.lib.users import check_full_name, validate_user_custom_profile_field
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
CustomProfileField,
|
CustomProfileField,
|
||||||
|
@ -1346,11 +1347,11 @@ def redirect_to_login(realm: Realm) -> HttpResponseRedirect:
|
||||||
return HttpResponseRedirect(redirect_url)
|
return HttpResponseRedirect(redirect_url)
|
||||||
|
|
||||||
|
|
||||||
def redirect_deactivated_user_to_login(realm: Realm) -> HttpResponseRedirect:
|
def redirect_deactivated_user_to_login(realm: Realm, email: str) -> HttpResponseRedirect:
|
||||||
# Specifying the template name makes sure that the user is not redirected to dev_login in case of
|
# Specifying the template name makes sure that the user is not redirected to dev_login in case of
|
||||||
# a deactivated account on a test server.
|
# a deactivated account on a test server.
|
||||||
login_url = reverse("login_page", kwargs={"template_name": "zerver/login.html"})
|
login_url = reverse("login_page", kwargs={"template_name": "zerver/login.html"})
|
||||||
redirect_url = realm.uri + login_url + "?is_deactivated=true"
|
redirect_url = add_query_to_redirect_url(realm.uri + login_url, f"is_deactivated={email}")
|
||||||
return HttpResponseRedirect(redirect_url)
|
return HttpResponseRedirect(redirect_url)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1565,7 +1566,7 @@ def social_auth_finish(
|
||||||
return_data["inactive_user_id"],
|
return_data["inactive_user_id"],
|
||||||
return_data["realm_string_id"],
|
return_data["realm_string_id"],
|
||||||
)
|
)
|
||||||
return redirect_deactivated_user_to_login(realm)
|
return redirect_deactivated_user_to_login(realm, return_data["validated_email"])
|
||||||
|
|
||||||
if auth_backend_disabled or inactive_realm or no_verified_email or email_not_associated:
|
if auth_backend_disabled or inactive_realm or no_verified_email or email_not_associated:
|
||||||
# Redirect to login page. We can't send to registration
|
# Redirect to login page. We can't send to registration
|
||||||
|
|
Loading…
Reference in New Issue