diff --git a/README.md b/README.md index 2203a2c..6fe49a7 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,9 @@ DEFAULTS = { # configurable function for sending sms 'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token' + # Token Generation Retry Count + 'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3 + } ``` diff --git a/drfpasswordless/__init__.py b/drfpasswordless/__init__.py index 6f0ab97..e9802b2 100644 --- a/drfpasswordless/__init__.py +++ b/drfpasswordless/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- __title__ = 'drfpasswordless' -__version__ = '1.5.6' +__version__ = '1.5.7' __author__ = 'Aaron Ng' __license__ = 'MIT' __copyright__ = 'Copyright 2020 Aaron Ng' diff --git a/drfpasswordless/__version__.py b/drfpasswordless/__version__.py index 6897abd..e3cced8 100644 --- a/drfpasswordless/__version__.py +++ b/drfpasswordless/__version__.py @@ -1,3 +1,3 @@ -VERSION = (1, 5, 6) +VERSION = (1, 5, 7) __version__ = '.'.join(map(str, VERSION)) diff --git a/drfpasswordless/migrations/0005_auto_20201117_0410.py b/drfpasswordless/migrations/0005_auto_20201117_0410.py new file mode 100644 index 0000000..7057588 --- /dev/null +++ b/drfpasswordless/migrations/0005_auto_20201117_0410.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.2 on 2020-11-17 04:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('drfpasswordless', '0004_auto_20200125_0853'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='callbacktoken', + unique_together=set(), + ), + ] diff --git a/drfpasswordless/models.py b/drfpasswordless/models.py index fd3557e..d09f3fc 100644 --- a/drfpasswordless/models.py +++ b/drfpasswordless/models.py @@ -47,7 +47,6 @@ class Meta: abstract = True get_latest_by = 'created_at' ordering = ['-id'] - unique_together = (('key', 'is_active'),) def __str__(self): return str(self.key) @@ -66,4 +65,3 @@ class CallbackToken(AbstractBaseCallbackToken): class Meta(AbstractBaseCallbackToken.Meta): verbose_name = 'Callback Token' - unique_together = ['is_active', 'key', 'type'] diff --git a/drfpasswordless/settings.py b/drfpasswordless/settings.py index f7f6e0b..5b93197 100644 --- a/drfpasswordless/settings.py +++ b/drfpasswordless/settings.py @@ -88,6 +88,8 @@ 'PASSWORDLESS_EMAIL_CALLBACK': 'drfpasswordless.utils.send_email_with_callback_token', 'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token', + # Token Generation Retry Count + 'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3 } # List of settings that may be in string import notation. diff --git a/drfpasswordless/signals.py b/drfpasswordless/signals.py index ac5e063..33adf6d 100644 --- a/drfpasswordless/signals.py +++ b/drfpasswordless/signals.py @@ -1,5 +1,6 @@ import logging from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError from django.dispatch import receiver from django.db.models import signals from drfpasswordless.models import CallbackToken @@ -27,18 +28,41 @@ def invalidate_previous_tokens(sender, instance, created, **kwargs): def check_unique_tokens(sender, instance, **kwargs): """ Ensures that mobile and email tokens are unique or tries once more to generate. + Note that here we've decided keys are unique even across auth and validation. + We could consider relaxing this in the future as well by filtering on the instance.type. """ if instance._state.adding: # save is called on a token to create it in the db # before creating check whether a token with the same key exists if isinstance(instance, CallbackToken): + unique = False + tries = 0 + if CallbackToken.objects.filter(key=instance.key, is_active=True).exists(): - instance.key = generate_numeric_token() + # Try N(default=3) times before giving up. + while tries < api_settings.PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS: + tries = tries + 1 + new_key = generate_numeric_token() + instance.key = new_key + + if not CallbackToken.objects.filter(key=instance.key, is_active=True).exists(): + # Leave the loop if we found a valid token that doesn't exist yet. + unique = True + break + + if not unique: + # A unique value wasn't found after three tries + raise ValidationError("Couldn't create a unique token even after retrying.") + else: + # A unique value was found immediately. + pass + + else: # save is called on an already existing token to update it. Such as invalidating it. # in that case there is no need to check for the key. This way we both avoid an unneccessary db hit # and avoid to change key field of used tokens. - pass + pass