Browse Source
Add address verification step in the "Change Email" flow
Add address verification step in the "Change Email" flow
A similar issue has come up multiple times: the user changes account's email address, enters a bad address by mistake, and gets locked out of their account. This commit adds an extra step in the "Change Email" flow: * In "Account Settings", user clicks on [Change Email] * User gets a prompt for a 6-digit confirmation code, which has been sent to their old address. This is to prevent account takeover when Eve sits down at a computer where Alice is logged in. * The user enters the confirmation code, and a "Change Email" form loads. * The user enters their new email address. * (The new step!) Instead of changing the email right away, we send a special login link to user's specified new address. * (The new step, continued) The user clicks on the login link, their account's email address gets updated, and they get logged in. The additional step makes sure the user can receive email at their new address. If they cannot receive email there, they cannot complete the "Change Email" procedure.pull/658/head
Pēteris Caune
2 years ago
No known key found for this signature in database
GPG Key ID: E28D7679E9A9EDE2
7 changed files with 159 additions and 46 deletions
-
3CHANGELOG.md
-
15hc/accounts/models.py
-
25hc/accounts/tests/test_change_email.py
-
65hc/accounts/tests/test_change_email_verify.py
-
6hc/accounts/urls.py
-
83hc/accounts/views.py
-
8templates/accounts/change_email_instructions.html
@ -0,0 +1,65 @@ |
|||
from unittest.mock import patch |
|||
import time |
|||
|
|||
from django.contrib.auth.hashers import make_password |
|||
from django.core.signing import TimestampSigner |
|||
from hc.test import BaseTestCase |
|||
|
|||
|
|||
class ChangeEmailVerifyTestCase(BaseTestCase): |
|||
def setUp(self): |
|||
super().setUp() |
|||
self.profile.token = make_password("secret-token", "login") |
|||
self.profile.save() |
|||
|
|||
self.checks_url = "/projects/%s/checks/" % self.project.code |
|||
|
|||
def _url(self, expired=False): |
|||
payload = { |
|||
"u": self.alice.username, |
|||
"t": "secret-token", |
|||
"e": "alice+new@example.org", |
|||
} |
|||
|
|||
if expired: |
|||
with patch("django.core.signing.TimestampSigner.timestamp") as mock_ts: |
|||
mock_ts.return_value = "1kHR5c" |
|||
signed = TimestampSigner().sign_object(payload) |
|||
else: |
|||
signed = TimestampSigner().sign_object(payload) |
|||
|
|||
return f"/accounts/change_email/{signed}/" |
|||
|
|||
def test_it_works(self): |
|||
r = self.client.post(self._url()) |
|||
self.assertRedirects(r, self.checks_url) |
|||
|
|||
# Alice's email should have been updated, and password cleared |
|||
self.alice.refresh_from_db() |
|||
self.assertEqual(self.alice.email, "alice+new@example.org") |
|||
self.assertFalse(self.alice.has_usable_password()) |
|||
|
|||
# After login, token should be blank |
|||
self.profile.refresh_from_db() |
|||
self.assertEqual(self.profile.token, "") |
|||
|
|||
def test_it_handles_get(self): |
|||
r = self.client.get(self._url()) |
|||
self.assertContains(r, "You are about to log into") |
|||
|
|||
# Alice's email should have *not* been changed yet |
|||
self.alice.refresh_from_db() |
|||
self.assertEqual(self.alice.email, "alice@example.org") |
|||
|
|||
def test_it_handles_get_with_cookie(self): |
|||
self.client.cookies["auto-login"] = "1" |
|||
r = self.client.get(self._url()) |
|||
self.assertRedirects(r, self.checks_url) |
|||
|
|||
def test_it_handles_expired_link(self): |
|||
r = self.client.post(self._url(expired=True)) |
|||
self.assertContains(r, "The link you just used is incorrect.") |
|||
|
|||
def test_it_handles_bad_payload(self): |
|||
r = self.client.post("/accounts/change_email/bad-payload/") |
|||
self.assertContains(r, "The link you just used is incorrect.") |
Write
Preview
Loading…
Cancel
Save
Reference in new issue