fix: validate form in login() (#7435)
* fix: validate form in login() * refactor: custom LoginView subclass for logins Preserves old behavior, but avoids some hacks. * test: reverse with strings, not view refs * chore: remove unused imports * fix: restore logout() call
This commit is contained in:
parent
96902bf3b8
commit
3c13db45fd
|
@ -50,8 +50,6 @@ from ietf.utils.test_utils import TestCase, login_testing_unauthorized
|
||||||
from ietf.utils.timezone import date_today
|
from ietf.utils.timezone import date_today
|
||||||
|
|
||||||
|
|
||||||
import ietf.ietfauth.views
|
|
||||||
|
|
||||||
if os.path.exists(settings.HTPASSWD_COMMAND):
|
if os.path.exists(settings.HTPASSWD_COMMAND):
|
||||||
skip_htpasswd_command = False
|
skip_htpasswd_command = False
|
||||||
skip_message = ""
|
skip_message = ""
|
||||||
|
@ -83,30 +81,30 @@ class IetfAuthTests(TestCase):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
def test_index(self):
|
def test_index(self):
|
||||||
self.assertEqual(self.client.get(urlreverse(ietf.ietfauth.views.index)).status_code, 200)
|
self.assertEqual(self.client.get(urlreverse("ietf.ietfauth.views.index")).status_code, 200)
|
||||||
|
|
||||||
def test_login_and_logout(self):
|
def test_login_and_logout(self):
|
||||||
PersonFactory(user__username='plain')
|
PersonFactory(user__username='plain')
|
||||||
|
|
||||||
# try logging in without a next
|
# try logging in without a next
|
||||||
r = self.client.get(urlreverse(ietf.ietfauth.views.login))
|
r = self.client.get(urlreverse("ietf.ietfauth.views.login"))
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
r = self.client.post(urlreverse(ietf.ietfauth.views.login), {"username":"plain", "password":"plain+password"})
|
r = self.client.post(urlreverse("ietf.ietfauth.views.login"), {"username":"plain", "password":"plain+password"})
|
||||||
self.assertEqual(r.status_code, 302)
|
self.assertEqual(r.status_code, 302)
|
||||||
self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.profile))
|
self.assertEqual(urlsplit(r["Location"])[2], urlreverse("ietf.ietfauth.views.profile"))
|
||||||
|
|
||||||
# try logging out
|
# try logging out
|
||||||
r = self.client.post(urlreverse('django.contrib.auth.views.logout'), {})
|
r = self.client.post(urlreverse('django.contrib.auth.views.logout'), {})
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertNotContains(r, "accounts/logout")
|
self.assertNotContains(r, "accounts/logout")
|
||||||
|
|
||||||
r = self.client.get(urlreverse(ietf.ietfauth.views.profile))
|
r = self.client.get(urlreverse("ietf.ietfauth.views.profile"))
|
||||||
self.assertEqual(r.status_code, 302)
|
self.assertEqual(r.status_code, 302)
|
||||||
self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.login))
|
self.assertEqual(urlsplit(r["Location"])[2], urlreverse("ietf.ietfauth.views.login"))
|
||||||
|
|
||||||
# try logging in with a next
|
# try logging in with a next
|
||||||
r = self.client.post(urlreverse(ietf.ietfauth.views.login) + "?next=/foobar", {"username":"plain", "password":"plain+password"})
|
r = self.client.post(urlreverse("ietf.ietfauth.views.login") + "?next=/foobar", {"username":"plain", "password":"plain+password"})
|
||||||
self.assertEqual(r.status_code, 302)
|
self.assertEqual(r.status_code, 302)
|
||||||
self.assertEqual(urlsplit(r["Location"])[2], "/foobar")
|
self.assertEqual(urlsplit(r["Location"])[2], "/foobar")
|
||||||
|
|
||||||
|
@ -137,19 +135,19 @@ class IetfAuthTests(TestCase):
|
||||||
# try with a trivial next
|
# try with a trivial next
|
||||||
_test_login("/")
|
_test_login("/")
|
||||||
# try with a next that requires login
|
# try with a next that requires login
|
||||||
_test_login(urlreverse(ietf.ietfauth.views.profile))
|
_test_login(urlreverse("ietf.ietfauth.views.profile"))
|
||||||
|
|
||||||
def test_login_with_different_email(self):
|
def test_login_with_different_email(self):
|
||||||
person = PersonFactory(user__username='plain')
|
person = PersonFactory(user__username='plain')
|
||||||
email = EmailFactory(person=person)
|
email = EmailFactory(person=person)
|
||||||
|
|
||||||
# try logging in without a next
|
# try logging in without a next
|
||||||
r = self.client.get(urlreverse(ietf.ietfauth.views.login))
|
r = self.client.get(urlreverse("ietf.ietfauth.views.login"))
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
r = self.client.post(urlreverse(ietf.ietfauth.views.login), {"username":email, "password":"plain+password"})
|
r = self.client.post(urlreverse("ietf.ietfauth.views.login"), {"username":email, "password":"plain+password"})
|
||||||
self.assertEqual(r.status_code, 302)
|
self.assertEqual(r.status_code, 302)
|
||||||
self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.profile))
|
self.assertEqual(urlsplit(r["Location"])[2], urlreverse("ietf.ietfauth.views.profile"))
|
||||||
|
|
||||||
def extract_confirm_url(self, confirm_email):
|
def extract_confirm_url(self, confirm_email):
|
||||||
# dig out confirm_email link
|
# dig out confirm_email link
|
||||||
|
@ -176,7 +174,7 @@ class IetfAuthTests(TestCase):
|
||||||
# For the lowered barrier to account creation period, we are disabling this kind of failure
|
# For the lowered barrier to account creation period, we are disabling this kind of failure
|
||||||
# def test_create_account_failure(self):
|
# def test_create_account_failure(self):
|
||||||
|
|
||||||
# url = urlreverse(ietf.ietfauth.views.create_account)
|
# url = urlreverse("ietf.ietfauth.views.create_account")
|
||||||
|
|
||||||
# # get
|
# # get
|
||||||
# r = self.client.get(url)
|
# r = self.client.get(url)
|
||||||
|
@ -195,7 +193,7 @@ class IetfAuthTests(TestCase):
|
||||||
self.assertTrue("Additional Assistance Required" in r)
|
self.assertTrue("Additional Assistance Required" in r)
|
||||||
|
|
||||||
def register(self, email):
|
def register(self, email):
|
||||||
url = urlreverse(ietf.ietfauth.views.create_account)
|
url = urlreverse("ietf.ietfauth.views.create_account")
|
||||||
|
|
||||||
# register email
|
# register email
|
||||||
empty_outbox()
|
empty_outbox()
|
||||||
|
@ -240,7 +238,7 @@ class IetfAuthTests(TestCase):
|
||||||
note = get_payload_text(outbox[-1])
|
note = get_payload_text(outbox[-1])
|
||||||
self.assertIn(email, note)
|
self.assertIn(email, note)
|
||||||
self.assertIn("A datatracker account for that email already exists", note)
|
self.assertIn("A datatracker account for that email already exists", note)
|
||||||
self.assertIn(urlreverse(ietf.ietfauth.views.password_reset), note)
|
self.assertIn(urlreverse("ietf.ietfauth.views.password_reset"), note)
|
||||||
|
|
||||||
def test_ietfauth_profile(self):
|
def test_ietfauth_profile(self):
|
||||||
EmailFactory(person__user__username='plain')
|
EmailFactory(person__user__username='plain')
|
||||||
|
@ -249,7 +247,7 @@ class IetfAuthTests(TestCase):
|
||||||
username = "plain"
|
username = "plain"
|
||||||
email_address = Email.objects.filter(person__user__username=username).first().address
|
email_address = Email.objects.filter(person__user__username=username).first().address
|
||||||
|
|
||||||
url = urlreverse(ietf.ietfauth.views.profile)
|
url = urlreverse("ietf.ietfauth.views.profile")
|
||||||
login_testing_unauthorized(self, username, url)
|
login_testing_unauthorized(self, username, url)
|
||||||
|
|
||||||
|
|
||||||
|
@ -400,7 +398,7 @@ class IetfAuthTests(TestCase):
|
||||||
def test_email_case_insensitive_protection(self):
|
def test_email_case_insensitive_protection(self):
|
||||||
EmailFactory(address="TestAddress@example.net")
|
EmailFactory(address="TestAddress@example.net")
|
||||||
person = PersonFactory()
|
person = PersonFactory()
|
||||||
url = urlreverse(ietf.ietfauth.views.profile)
|
url = urlreverse("ietf.ietfauth.views.profile")
|
||||||
login_testing_unauthorized(self, person.user.username, url)
|
login_testing_unauthorized(self, person.user.username, url)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
@ -441,7 +439,7 @@ class IetfAuthTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_reset_password(self):
|
def test_reset_password(self):
|
||||||
url = urlreverse(ietf.ietfauth.views.password_reset)
|
url = urlreverse("ietf.ietfauth.views.password_reset")
|
||||||
email = 'someone@example.com'
|
email = 'someone@example.com'
|
||||||
password = 'foobar'
|
password = 'foobar'
|
||||||
|
|
||||||
|
@ -507,7 +505,7 @@ class IetfAuthTests(TestCase):
|
||||||
self.assertEqual(len(outbox), 1)
|
self.assertEqual(len(outbox), 1)
|
||||||
confirm_url = self.extract_confirm_url(outbox[-1])
|
confirm_url = self.extract_confirm_url(outbox[-1])
|
||||||
|
|
||||||
r = self.client.post(urlreverse(ietf.ietfauth.views.login), {'username': email, 'password': password})
|
r = self.client.post(urlreverse("ietf.ietfauth.views.login"), {'username': email, 'password': password})
|
||||||
|
|
||||||
r = self.client.get(confirm_url)
|
r = self.client.get(confirm_url)
|
||||||
self.assertEqual(r.status_code, 404)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
@ -589,7 +587,7 @@ class IetfAuthTests(TestCase):
|
||||||
availability="unavailable",
|
availability="unavailable",
|
||||||
)
|
)
|
||||||
|
|
||||||
url = urlreverse(ietf.ietfauth.views.review_overview)
|
url = urlreverse("ietf.ietfauth.views.review_overview")
|
||||||
|
|
||||||
login_testing_unauthorized(self, reviewer.user.username, url)
|
login_testing_unauthorized(self, reviewer.user.username, url)
|
||||||
|
|
||||||
|
@ -633,10 +631,9 @@ class IetfAuthTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_change_password(self):
|
def test_change_password(self):
|
||||||
|
chpw_url = urlreverse("ietf.ietfauth.views.change_password")
|
||||||
chpw_url = urlreverse(ietf.ietfauth.views.change_password)
|
prof_url = urlreverse("ietf.ietfauth.views.profile")
|
||||||
prof_url = urlreverse(ietf.ietfauth.views.profile)
|
login_url = urlreverse("ietf.ietfauth.views.login")
|
||||||
login_url = urlreverse(ietf.ietfauth.views.login)
|
|
||||||
redir_url = '%s?next=%s' % (login_url, chpw_url)
|
redir_url = '%s?next=%s' % (login_url, chpw_url)
|
||||||
|
|
||||||
# get without logging in
|
# get without logging in
|
||||||
|
@ -681,9 +678,9 @@ class IetfAuthTests(TestCase):
|
||||||
|
|
||||||
def test_change_username(self):
|
def test_change_username(self):
|
||||||
|
|
||||||
chun_url = urlreverse(ietf.ietfauth.views.change_username)
|
chun_url = urlreverse("ietf.ietfauth.views.change_username")
|
||||||
prof_url = urlreverse(ietf.ietfauth.views.profile)
|
prof_url = urlreverse("ietf.ietfauth.views.profile")
|
||||||
login_url = urlreverse(ietf.ietfauth.views.login)
|
login_url = urlreverse("ietf.ietfauth.views.login")
|
||||||
redir_url = '%s?next=%s' % (login_url, chun_url)
|
redir_url = '%s?next=%s' % (login_url, chun_url)
|
||||||
|
|
||||||
# get without logging in
|
# get without logging in
|
||||||
|
|
|
@ -14,7 +14,7 @@ urlpatterns = [
|
||||||
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', views.confirm_new_email),
|
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', views.confirm_new_email),
|
||||||
url(r'^create/$', views.create_account),
|
url(r'^create/$', views.create_account),
|
||||||
url(r'^create/confirm/(?P<auth>[^/]+)/$', views.confirm_account),
|
url(r'^create/confirm/(?P<auth>[^/]+)/$', views.confirm_account),
|
||||||
url(r'^login/$', views.login),
|
url(r'^login/$', views.AnyEmailLoginView.as_view(), name="ietf.ietfauth.views.login"),
|
||||||
url(r'^logout/$', LogoutView.as_view(), name="django.contrib.auth.views.logout"),
|
url(r'^logout/$', LogoutView.as_view(), name="django.contrib.auth.views.logout"),
|
||||||
url(r'^password/$', views.change_password),
|
url(r'^password/$', views.change_password),
|
||||||
url(r'^profile/$', views.profile),
|
url(r'^profile/$', views.profile),
|
||||||
|
|
|
@ -45,7 +45,7 @@ import django.core.signing
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import update_session_auth_hash, logout, authenticate
|
from django.contrib.auth import logout, update_session_auth_hash
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
from django.contrib.auth.hashers import identify_hasher
|
from django.contrib.auth.hashers import identify_hasher
|
||||||
|
@ -752,53 +752,57 @@ def change_username(request):
|
||||||
return render(request, 'registration/change_username.html', {'form': form})
|
return render(request, 'registration/change_username.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
class AnyEmailAuthenticationForm(AuthenticationForm):
|
||||||
def login(request, extra_context=None):
|
"""AuthenticationForm that allows any email address as the username
|
||||||
"""
|
|
||||||
This login function is a wrapper around django's login() for the purpose
|
Also performs a check for a cleared password field and provides a helpful error message
|
||||||
of providing a notification if the user's password has been cleared. The
|
if that applies to the user attempting to log in.
|
||||||
warning will be triggered if the password field has been set to something
|
|
||||||
which is not recognized as a valid password hash.
|
|
||||||
"""
|
"""
|
||||||
|
_unauthenticated_user = None
|
||||||
|
|
||||||
if request.method == "POST":
|
def clean_username(self):
|
||||||
form = AuthenticationForm(request, data=request.POST)
|
username = self.cleaned_data.get("username", None)
|
||||||
username = form.data.get('username')
|
if username is None:
|
||||||
user = User.objects.filter(username__iexact=username).first() # Consider _never_ actually looking for the User username and only looking at Email
|
raise self.get_invalid_login_error()
|
||||||
if not user:
|
user = User.objects.filter(username__iexact=username).first()
|
||||||
# try to find user ID from the email address
|
if user is None:
|
||||||
email = Email.objects.filter(address=username).first()
|
email = Email.objects.filter(address=username).first()
|
||||||
if email and email.person and email.person.user:
|
if email and email.person:
|
||||||
u2 = email.person.user
|
user = email.person.user # might be None
|
||||||
# be conservative, only accept this if login is valid
|
if user is None:
|
||||||
if u2:
|
raise self.get_invalid_login_error()
|
||||||
pw = form.data.get('password')
|
self._unauthenticated_user = user # remember this for the clean() method
|
||||||
au = authenticate(request, username=u2.username, password=pw)
|
return user.username
|
||||||
if au:
|
|
||||||
# kludge to change the querydict
|
def clean(self):
|
||||||
q2 = request.POST.copy()
|
if self._unauthenticated_user is not None:
|
||||||
q2['username'] = u2.username
|
try:
|
||||||
request.POST = q2
|
identify_hasher(self._unauthenticated_user.password)
|
||||||
user = u2
|
|
||||||
#
|
|
||||||
if user:
|
|
||||||
try:
|
|
||||||
identify_hasher(user.password)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
extra_context = {"alert":
|
self.add_error(
|
||||||
"Note: Your password has been cleared because "
|
"password",
|
||||||
"of possible password leakage. "
|
'Your password has been cleared because of possible password leakage. '
|
||||||
"Please use the password reset link below "
|
'Please use the "Forgot your password?" button below to set a new password '
|
||||||
"to set a new password for your account.",
|
'for your account.',
|
||||||
}
|
)
|
||||||
response = LoginView.as_view(extra_context=extra_context)(request)
|
return super().clean()
|
||||||
if isinstance(response, HttpResponseRedirect) and user and user.is_authenticated:
|
|
||||||
try:
|
|
||||||
user.person
|
class AnyEmailLoginView(LoginView):
|
||||||
except Person.DoesNotExist:
|
"""LoginView that allows any email address as the username
|
||||||
logout(request)
|
|
||||||
response = render(request, 'registration/missing_person.html')
|
Redirects to the missing_person page instead of logging in if the user does not have a Person
|
||||||
return response
|
"""
|
||||||
|
form_class = AnyEmailAuthenticationForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Security check complete. Log the user in if they have a Person."""
|
||||||
|
user = form.get_user() # user has authenticated at this point
|
||||||
|
if not hasattr(user, "person"):
|
||||||
|
logout(self.request) # should not be logged in yet, but just in case...
|
||||||
|
return render(self.request, "registration/missing_person.html")
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@person_required
|
@person_required
|
||||||
|
|
Loading…
Reference in a new issue