Skip to content

Commit

Permalink
Issue #44: register verification one-time use
Browse files Browse the repository at this point in the history
* Added REGISTER_VERIFICATION_ONE_TIME_USE setting
* Added warning check in case
  REGISTER_VERIFICATION_AOUT_LOGIN is enabled
  • Loading branch information
apragacz committed Apr 25, 2019
1 parent a7877d3 commit 7373571
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 13 deletions.
18 changes: 18 additions & 0 deletions rest_registration/api/views/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ def get_base_url(self):
def get_valid_period(self):
return registration_settings.REGISTER_VERIFICATION_PERIOD

def _calculate_salt(self, data):
if registration_settings.REGISTER_VERIFICATION_ONE_TIME_USE:
user_id = data['user_id']
user = get_user_by_id(user_id, require_verified=False)
# Use current user verification flag as a part of the salt.
# If the verification flag gets changed, then assume that
# the change was caused by previous verification and the signature
# is not valid anymore because changed user verification flag
# implies changed salt used when verifying the input data.
verification_flag_field = get_user_setting(
'VERIFICATION_FLAG_FIELD')
verification_flag = getattr(user, verification_flag_field)
salt = '{self.SALT_BASE}:{verification_flag}'.format(
self=self, verification_flag=verification_flag)
else:
salt = self.SALT_BASE
return salt


@api_view_serializer_class_getter(
lambda: registration_settings.REGISTER_SERIALIZER_CLASS)
Expand Down
22 changes: 22 additions & 0 deletions rest_registration/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class ErrorCode(object):
NO_AUTH_INSTALLED = 'E008'


class WarningCode(object):
REGISTER_VERIFICATION_MULTIPLE_AUTO_LOGIN = 'W001'


@register()
@simple_check(
'django.contrib.auth is not in INSTALLED_APPS',
Expand Down Expand Up @@ -108,6 +112,23 @@ def token_auth_installed_check():
)


@register()
@simple_check(
'REGISTER_VERIFICATION_AUTO_LOGIN is enabled,'
' but REGISTER_VERIFICATION_ONE_TIME_USE is not enabled.'
' This can allow multiple logins using the verification url.',
WarningCode.REGISTER_VERIFICATION_MULTIPLE_AUTO_LOGIN,
)
def register_verification_one_time_auto_login_check():
return implies(
all([
registration_settings.REGISTER_VERIFICATION_ENABLED,
registration_settings.REGISTER_VERIFICATION_AUTO_LOGIN,
]),
registration_settings.REGISTER_VERIFICATION_ONE_TIME_USE,
)


@register()
@simple_check(
'REGISTER_VERIFICATION_EMAIL_TEMPLATES is invalid',
Expand Down Expand Up @@ -159,6 +180,7 @@ def implies(premise, conclusion):
verification_from_check,
token_auth_config_check,
token_auth_installed_check,
register_verification_one_time_auto_login_check,
valid_register_verification_email_template_config_check,
valid_reset_password_verification_email_template_config_check,
valid_register_email_verification_email_template_config_check,
Expand Down
13 changes: 7 additions & 6 deletions rest_registration/decorators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import functools
import types

from django.core.checks import Error
from django.core.checks import Error, Warning


def api_view_serializer_class_getter(serializer_class_getter):
Expand Down Expand Up @@ -35,29 +35,30 @@ def api_view_serializer_class(serializer_class):
return api_view_serializer_class_getter(lambda: serializer_class)


def simple_check(error_message, error_code, obj=None):
def simple_check(error_message, error_code, obj=None, warning=False):
message_cls = Warning if warning else Error

def decorator(predicate):

@functools.wraps(predicate)
def check_fun(app_configs, **kwargs):
from rest_registration.apps import RestRegistrationConfig

errors = []
messages = []
if not predicate():
err_id = '{RestRegistrationConfig.name}.{error_code}'.format(
RestRegistrationConfig=RestRegistrationConfig,
error_code=error_code,
)
errors.append(
Error(
messages.append(
message_cls(
error_message,
obj=obj,
hint=None,
id=err_id,
)
)
return errors
return messages

return check_fun

Expand Down
1 change: 1 addition & 0 deletions rest_registration/settings_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def __new__(cls, name, *, default=None, help=None, import_string=False):
to create the activation link for newly registered user.
"""),
),
Field('REGISTER_VERIFICATION_ONE_TIME_USE', default=False),
Field(
'REGISTER_VERIFICATION_EMAIL_TEMPLATES',
default={
Expand Down
54 changes: 48 additions & 6 deletions tests/api/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,24 +355,64 @@ def _get_register_user_data(
class VerifyRegistrationViewTestCase(APIViewTestCase):
VIEW_NAME = 'verify-registration'

def create_verify_and_user(self, session=False):
def prepare_user(self):
user = self.create_test_user(is_active=False)
self.assertFalse(user.is_active)
return user

def prepare_request(self, user, session=False):
signer = RegisterSigner({'user_id': user.pk})
data = signer.get_signed_data()
request = self.create_post_request(data)
if session:
self.add_session_to_request(request)
response = self.view_func(request)
return user, response
return request

def prepare_user_and_request(self, session=False):
user = self.prepare_user()
request = self.prepare_request(user, session=session)
return user, request

@override_settings(REST_REGISTRATION=REST_REGISTRATION_WITH_VERIFICATION)
def test_verify_ok(self):
user, response = self.create_verify_and_user()
user, request = self.prepare_user_and_request()
response = self.view_func(request)
self.assert_valid_response(response, status.HTTP_200_OK)
user.refresh_from_db()
self.assertTrue(user.is_active)

@override_settings(REST_REGISTRATION=REST_REGISTRATION_WITH_VERIFICATION)
def test_verify_ok_idempotent(self):
user = self.prepare_user()
request1 = self.prepare_request(user)
request2 = self.prepare_request(user)

self.view_func(request1)

response = self.view_func(request2)
self.assert_valid_response(response, status.HTTP_200_OK)
user.refresh_from_db()
self.assertTrue(user.is_active)

@override_settings(
REST_REGISTRATION=shallow_merge_dicts(
REST_REGISTRATION_WITH_VERIFICATION, {
'REGISTER_VERIFICATION_ONE_TIME_USE': True,
},
),
)
def test_verify_one_time_use(self):
user = self.prepare_user()
request1 = self.prepare_request(user)
request2 = self.prepare_request(user)

self.view_func(request1)

response = self.view_func(request2)
self.assert_valid_response(response, status.HTTP_400_BAD_REQUEST)
user.refresh_from_db()
self.assertTrue(user.is_active)

@override_settings(
REST_REGISTRATION=shallow_merge_dicts(
REST_REGISTRATION_WITH_VERIFICATION, {
Expand All @@ -382,7 +422,8 @@ def test_verify_ok(self):
)
def test_verify_ok_login(self):
with patch('django.contrib.auth.login') as login_mock:
user, response = self.create_verify_and_user()
user, request = self.prepare_user_and_request()
response = self.view_func(request)
login_mock.assert_called_once_with(mock.ANY, user)
self.assert_valid_response(response, status.HTTP_200_OK)
user.refresh_from_db()
Expand Down Expand Up @@ -425,7 +466,8 @@ def test_verify_expired(self):
}
)
def test_verify_disabled(self):
user, response = self.create_verify_and_user()
user, request = self.prepare_user_and_request()
response = self.view_func(request)

self.assert_invalid_response(response, status.HTTP_404_NOT_FOUND)
user.refresh_from_db()
Expand Down
17 changes: 16 additions & 1 deletion tests/test_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.test.utils import override_settings

from rest_registration.apps import RestRegistrationConfig
from rest_registration.checks import __ALL_CHECKS__, ErrorCode
from rest_registration.checks import __ALL_CHECKS__, ErrorCode, WarningCode
from rest_registration.settings import DEFAULTS


Expand Down Expand Up @@ -63,6 +63,21 @@ def test_checks_preferred_setup_missing_sender_email(self):
ErrorCode.NO_VER_FROM_EMAIL,
])

@override_settings(
REST_REGISTRATION={
'REGISTER_VERIFICATION_URL': '/verify-account/',
'REGISTER_VERIFICATION_AUTO_LOGIN': True,
'REGISTER_EMAIL_VERIFICATION_ENABLED': False,
'RESET_PASSWORD_VERIFICATION_ENABLED': False,
'VERIFICATION_FROM_EMAIL': 'jon.doe@example.com',
},
)
def test_checks_multiple_time_auto_login(self):
errors = simulate_checks()
self.assert_error_codes_match(errors, [
WarningCode.REGISTER_VERIFICATION_MULTIPLE_AUTO_LOGIN,
])

@override_settings(
REST_REGISTRATION={
'REGISTER_VERIFICATION_ENABLED': False,
Expand Down

0 comments on commit 7373571

Please sign in to comment.