Skip to content

Commit 3be6ecd

Browse files
committed
Issue #48: Allowing to use custom user field for verification
* Added 'USER_VERIFICATION_ID_FIELD' setting key * Added tests
1 parent f3ace15 commit 3be6ecd

File tree

8 files changed

+219
-22
lines changed

8 files changed

+219
-22
lines changed

rest_registration/api/views/register.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
from rest_registration.notifications import send_verification_notification
1414
from rest_registration.settings import registration_settings
1515
from rest_registration.utils.responses import get_ok_response
16-
from rest_registration.utils.users import get_user_by_id, get_user_setting
16+
from rest_registration.utils.users import (
17+
get_user_by_verification_id,
18+
get_user_setting,
19+
get_user_verification_id
20+
)
1721
from rest_registration.utils.verification import verify_signer_or_bad_request
1822
from rest_registration.verification import URLParamsSigner
1923

@@ -30,8 +34,8 @@ def get_valid_period(self):
3034

3135
def _calculate_salt(self, data):
3236
if registration_settings.REGISTER_VERIFICATION_ONE_TIME_USE:
33-
user_id = data['user_id']
34-
user = get_user_by_id(user_id, require_verified=False)
37+
user = get_user_by_verification_id(
38+
data['user_id'], require_verified=False)
3539
# Use current user verification flag as a part of the salt.
3640
# If the verification flag gets changed, then assume that
3741
# the change was caused by previous verification and the signature
@@ -77,7 +81,7 @@ def register(request):
7781

7882
if registration_settings.REGISTER_VERIFICATION_ENABLED:
7983
signer = RegisterSigner({
80-
'user_id': user.pk,
84+
'user_id': get_user_verification_id(user),
8185
}, request=request)
8286
template_config = (
8387
registration_settings.REGISTER_VERIFICATION_EMAIL_TEMPLATES)
@@ -117,7 +121,7 @@ def process_verify_registration_data(input_data):
117121
verify_signer_or_bad_request(signer)
118122

119123
verification_flag_field = get_user_setting('VERIFICATION_FLAG_FIELD')
120-
user = get_user_by_id(data['user_id'], require_verified=False)
124+
user = get_user_by_verification_id(data['user_id'], require_verified=False)
121125
setattr(user, verification_flag_field, True)
122126
user.save()
123127

rest_registration/api/views/register_email.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
from rest_registration.notifications import send_verification_notification
1111
from rest_registration.settings import registration_settings
1212
from rest_registration.utils.responses import get_ok_response
13-
from rest_registration.utils.users import get_user_by_id, get_user_setting
13+
from rest_registration.utils.users import (
14+
get_user_by_verification_id,
15+
get_user_setting,
16+
get_user_verification_id
17+
)
1418
from rest_registration.utils.verification import verify_signer_or_bad_request
1519
from rest_registration.verification import URLParamsSigner
1620

@@ -46,7 +50,7 @@ def register_email(request):
4650
registration_settings.REGISTER_EMAIL_VERIFICATION_EMAIL_TEMPLATES)
4751
if registration_settings.REGISTER_EMAIL_VERIFICATION_ENABLED:
4852
signer = RegisterEmailSigner({
49-
'user_id': user.pk,
53+
'user_id': get_user_verification_id(user),
5054
'email': email,
5155
}, request=request)
5256
send_verification_notification(
@@ -88,6 +92,6 @@ def process_verify_email_data(input_data):
8892
verify_signer_or_bad_request(signer)
8993

9094
email_field = get_user_setting('EMAIL_FIELD')
91-
user = get_user_by_id(data['user_id'])
95+
user = get_user_by_verification_id(data['user_id'])
9296
setattr(user, email_field, data['email'])
9397
user.save()

rest_registration/api/views/reset_password.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
from rest_registration.notifications import send_verification_notification
1414
from rest_registration.settings import registration_settings
1515
from rest_registration.utils.responses import get_ok_response
16-
from rest_registration.utils.users import get_user_by_id
16+
from rest_registration.utils.users import (
17+
get_user_by_verification_id,
18+
get_user_verification_id
19+
)
1720
from rest_registration.utils.verification import verify_signer_or_bad_request
1821
from rest_registration.verification import URLParamsSigner
1922

@@ -30,8 +33,8 @@ def get_valid_period(self):
3033

3134
def _calculate_salt(self, data):
3235
if registration_settings.RESET_PASSWORD_VERIFICATION_ONE_TIME_USE:
33-
user_id = data['user_id']
34-
user = get_user_by_id(user_id, require_verified=False)
36+
user = get_user_by_verification_id(
37+
data['user_id'], require_verified=False)
3538
# Use current user password hash as a part of the salt.
3639
# If the password gets changed, then assume that the change
3740
# was caused by previous password reset and the signature
@@ -61,7 +64,7 @@ def send_reset_password_link(request):
6164
if not user:
6265
raise UserNotFound()
6366
signer = ResetPasswordSigner({
64-
'user_id': user.pk,
67+
'user_id': get_user_verification_id(user),
6568
}, request=request)
6669

6770
template_config = (
@@ -100,7 +103,7 @@ def process_reset_password_data(input_data):
100103
signer = ResetPasswordSigner(data)
101104
verify_signer_or_bad_request(signer)
102105

103-
user = get_user_by_id(data['user_id'], require_verified=False)
106+
user = get_user_by_verification_id(data['user_id'], require_verified=False)
104107
try:
105108
validate_password(password, user=user)
106109
except ValidationError as exc:

rest_registration/settings_fields.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ def __new__(cls, name, *, default=None, help=None, import_string=False):
4343
'USER_EMAIL_FIELD',
4444
default='email',
4545
),
46+
Field(
47+
'USER_VERIFICATION_ID_FIELD',
48+
default='pk',
49+
help=dedent("""\
50+
Field used in verification, as part of signed data.
51+
52+
The given field should uniquely identify the user. This means that
53+
using any user field which could change over time
54+
(``email``, ``username``) is NOT recommended.
55+
"""),
56+
),
4657
Field(
4758
'USER_VERIFICATION_FLAG_FIELD',
4859
default='is_active',

rest_registration/utils/users.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,18 @@ def authenticate_by_login_and_password_or_none(login, password):
5656
return user
5757

5858

59-
def get_user_by_id(user_id, default=_RAISE_EXCEPTION, require_verified=True):
59+
def get_user_verification_id(user):
60+
verification_id_field = get_user_setting('VERIFICATION_ID_FIELD')
61+
return getattr(user, verification_id_field)
62+
63+
64+
def get_user_by_verification_id(
65+
user_verification_id, default=_RAISE_EXCEPTION, require_verified=True):
66+
verification_id_field = get_user_setting('VERIFICATION_ID_FIELD')
6067
return get_user_by_lookup_dict({
61-
'pk': user_id,
62-
}, require_verified=require_verified)
68+
verification_id_field: user_verification_id},
69+
default=default,
70+
require_verified=require_verified)
6371

6472

6573
def get_user_by_lookup_dict(

tests/api/test_register.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,45 @@ def test_register_ok(self):
143143
signer = RegisterSigner(verification_data)
144144
signer.verify()
145145

146+
@override_settings(
147+
REST_REGISTRATION=shallow_merge_dicts(
148+
REST_REGISTRATION_WITH_VERIFICATION, {
149+
'USER_VERIFICATION_ID_FIELD': 'username',
150+
},
151+
),
152+
)
153+
def test_register_with_username_as_verification_id_ok(self):
154+
# Using username is not recommended if it can change for a given user.
155+
data = self._get_register_user_data(password='testpassword')
156+
request = self.create_post_request(data)
157+
with self.assert_one_mail_sent() as sent_emails, self.timer() as timer:
158+
response = self.view_func(request)
159+
self.assert_valid_response(response, status.HTTP_201_CREATED)
160+
user_id = response.data['id']
161+
# Check database state.
162+
user = self.user_class.objects.get(id=user_id)
163+
self.assertEqual(user.username, data['username'])
164+
self.assertTrue(user.check_password(data['password']))
165+
self.assertFalse(user.is_active)
166+
# Check verification e-mail.
167+
sent_email = sent_emails[0]
168+
self.assertEqual(sent_email.from_email, VERIFICATION_FROM_EMAIL)
169+
self.assertListEqual(sent_email.to, [data['email']])
170+
url = self.assert_one_url_line_in_text(sent_email.body)
171+
172+
verification_data = self.assert_valid_verification_url(
173+
url,
174+
expected_path=REGISTER_VERIFICATION_URL,
175+
expected_fields={'signature', 'user_id', 'timestamp'},
176+
)
177+
user_verification_id = verification_data['user_id']
178+
self.assertEqual(user_verification_id, user.username)
179+
url_sig_timestamp = int(verification_data['timestamp'])
180+
self.assertGreaterEqual(url_sig_timestamp, timer.start_time)
181+
self.assertLessEqual(url_sig_timestamp, timer.end_time)
182+
signer = RegisterSigner(verification_data)
183+
signer.verify()
184+
146185
@override_settings(
147186
REST_REGISTRATION=shallow_merge_dicts(
148187
REST_REGISTRATION_WITH_VERIFICATION, {
@@ -351,8 +390,10 @@ def prepare_user(self):
351390
self.assertFalse(user.is_active)
352391
return user
353392

354-
def prepare_request(self, user, session=False):
355-
signer = RegisterSigner({'user_id': user.pk})
393+
def prepare_request(self, user, session=False, data_to_sign=None):
394+
if data_to_sign is None:
395+
data_to_sign = {'user_id': user.pk}
396+
signer = RegisterSigner(data_to_sign)
356397
data = signer.get_signed_data()
357398
request = self.create_post_request(data)
358399
if session:
@@ -372,6 +413,22 @@ def test_verify_ok(self):
372413
user.refresh_from_db()
373414
self.assertTrue(user.is_active)
374415

416+
@override_settings(
417+
REST_REGISTRATION=shallow_merge_dicts(
418+
REST_REGISTRATION_WITH_VERIFICATION, {
419+
'USER_VERIFICATION_ID_FIELD': 'username',
420+
},
421+
),
422+
)
423+
def test_verify_with_username_as_verification_id_ok(self):
424+
user = self.prepare_user()
425+
request = self.prepare_request(
426+
user, data_to_sign={'user_id': user.username})
427+
response = self.view_func(request)
428+
self.assert_valid_response(response, status.HTTP_200_OK)
429+
user.refresh_from_db()
430+
self.assertTrue(user.is_active)
431+
375432
@override_settings(REST_REGISTRATION=REST_REGISTRATION_WITH_VERIFICATION)
376433
def test_verify_ok_idempotent(self):
377434
user = self.prepare_user()

tests/api/test_register_email.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rest_framework.test import force_authenticate
77

88
from rest_registration.api.views.register_email import RegisterEmailSigner
9+
from tests.utils import shallow_merge_dicts
910

1011
from .base import APIViewTestCase
1112

@@ -23,7 +24,8 @@ def setUp(self):
2324
super().setUp()
2425
self.email = 'testuser1@example.com'
2526
self.new_email = 'testuser2@example.com'
26-
self.user = self.create_test_user(email=self.email)
27+
self.user = self.create_test_user(
28+
username='testusername', email=self.email)
2729

2830

2931
class RegisterEmailViewTestCase(BaseRegisterEmailViewTestCase):
@@ -69,6 +71,44 @@ def test_ok(self):
6971
signer = RegisterEmailSigner(verification_data)
7072
signer.verify()
7173

74+
@override_settings(
75+
REST_REGISTRATION=shallow_merge_dicts(
76+
REST_REGISTRATION_WITH_EMAIL_VERIFICATION, {
77+
'USER_VERIFICATION_ID_FIELD': 'username',
78+
},
79+
),
80+
)
81+
def test_with_username_as_verification_id_ok(self):
82+
data = {
83+
'email': self.new_email,
84+
}
85+
with self.assert_one_mail_sent() as sent_emails, self.timer() as timer:
86+
response = self._test_authenticated(data)
87+
self.assert_valid_response(response, status.HTTP_200_OK)
88+
# Check database state.
89+
self.user.refresh_from_db()
90+
self.assertEqual(self.user.email, self.email)
91+
# Check verification e-mail.
92+
sent_email = sent_emails[0]
93+
self.assertEqual(
94+
sent_email.from_email,
95+
REST_REGISTRATION_WITH_EMAIL_VERIFICATION['VERIFICATION_FROM_EMAIL'], # noqa: E501
96+
)
97+
self.assertListEqual(sent_email.to, [self.new_email])
98+
url = self.assert_one_url_line_in_text(sent_email.body)
99+
verification_data = self.assert_valid_verification_url(
100+
url,
101+
expected_path=REGISTER_EMAIL_VERIFICATION_URL,
102+
expected_fields={'signature', 'user_id', 'timestamp', 'email'},
103+
)
104+
self.assertEqual(verification_data['email'], self.new_email)
105+
self.assertEqual(verification_data['user_id'], self.user.username)
106+
url_sig_timestamp = int(verification_data['timestamp'])
107+
self.assertGreaterEqual(url_sig_timestamp, timer.start_time)
108+
self.assertLessEqual(url_sig_timestamp, timer.end_time)
109+
signer = RegisterEmailSigner(verification_data)
110+
signer.verify()
111+
72112
@override_settings(
73113
REST_REGISTRATION={
74114
'REGISTER_EMAIL_VERIFICATION_ENABLED': False
@@ -89,9 +129,7 @@ class VerifyEmailViewTestCase(BaseRegisterEmailViewTestCase):
89129
VIEW_NAME = 'verify-email'
90130

91131
@override_settings(
92-
REST_REGISTRATION={
93-
'REGISTER_EMAIL_VERIFICATION_URL': REGISTER_EMAIL_VERIFICATION_URL,
94-
}
132+
REST_REGISTRATION=REST_REGISTRATION_WITH_EMAIL_VERIFICATION,
95133
)
96134
def test_ok(self):
97135
signer = RegisterEmailSigner({
@@ -105,6 +143,25 @@ def test_ok(self):
105143
self.user.refresh_from_db()
106144
self.assertEqual(self.user.email, self.new_email)
107145

146+
@override_settings(
147+
REST_REGISTRATION=shallow_merge_dicts(
148+
REST_REGISTRATION_WITH_EMAIL_VERIFICATION, {
149+
'USER_VERIFICATION_ID_FIELD': 'username',
150+
},
151+
),
152+
)
153+
def test_with_username_as_verification_id_ok(self):
154+
signer = RegisterEmailSigner({
155+
'user_id': self.user.username,
156+
'email': self.new_email,
157+
})
158+
data = signer.get_signed_data()
159+
request = self.create_post_request(data)
160+
response = self.view_func(request)
161+
self.assert_valid_response(response, status.HTTP_200_OK)
162+
self.user.refresh_from_db()
163+
self.assertEqual(self.user.email, self.new_email)
164+
108165
@override_settings(
109166
REST_REGISTRATION={
110167
'REGISTER_EMAIL_VERIFICATION_URL': REGISTER_EMAIL_VERIFICATION_URL,

0 commit comments

Comments
 (0)