diff --git a/config.py b/config.py index 2c5104664..fc60d2441 100644 --- a/config.py +++ b/config.py @@ -494,3 +494,6 @@ class DefaultConfig(ImmutableConfig): # Defines a secret for enabling the health-check endpoint's debug information. ENABLE_HEALTH_DEBUG_SECRET = None + + # The lifetime for a user recovery token before it becomes invalid. + USER_RECOVERY_TOKEN_LIFETIME = '30m' diff --git a/data/model/test/test_user.py b/data/model/test/test_user.py index f877806e6..47dd7c0ec 100644 --- a/data/model/test/test_user.py +++ b/data/model/test/test_user.py @@ -1,9 +1,30 @@ +from datetime import datetime + +import pytest + from mock import patch -from data.model.user import create_user_noverify +from data.database import EmailConfirmation +from data.model.user import create_user_noverify, validate_reset_code +from util.timedeltastring import convert_to_timedelta from test.fixtures import * def test_create_user_with_expiration(initialized_db): with patch('data.model.config.app_config', {'DEFAULT_TAG_EXPIRATION': '1h'}): user = create_user_noverify('foobar', 'foo@example.com', email_required=False) assert user.removed_tag_expiration_s == 60 * 60 + +@pytest.mark.parametrize('token_lifetime, time_since', [ + ('1m', '2m'), + ('2m', '1m'), + ('1h', '1m'), +]) +def test_validation_code(token_lifetime, time_since, initialized_db): + user = create_user_noverify('foobar', 'foo@example.com', email_required=False) + created = datetime.now() - convert_to_timedelta(time_since) + confirmation = EmailConfirmation.create(user=user, pw_reset=True, created=created) + + with patch('data.model.config.app_config', {'USER_RECOVERY_TOKEN_LIFETIME': token_lifetime}): + result = validate_reset_code(confirmation.code) + expect_success = convert_to_timedelta(token_lifetime) >= convert_to_timedelta(time_since) + assert expect_success == (result is not None) diff --git a/data/model/user.py b/data/model/user.py index 134e48f8c..fdd17e445 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -507,19 +507,26 @@ def create_reset_password_email_code(email): def validate_reset_code(code): + # Find the reset code. try: code = EmailConfirmation.get(EmailConfirmation.code == code, EmailConfirmation.pw_reset == True) except EmailConfirmation.DoesNotExist: return None + # Make sure the code is not expired. + max_lifetime_duration = convert_to_timedelta(config.app_config['USER_RECOVERY_TOKEN_LIFETIME']) + if code.created + max_lifetime_duration < datetime.now(): + code.delete_instance() + return None + + # Verify the user and return the code. user = code.user if not user.verified: user.verified = True user.save() code.delete_instance() - return user