Skip to content

Commit

Permalink
Merge pull request aaronn#78 from aaronn/fix/unique-token-generation
Browse files Browse the repository at this point in the history
Fix/unique token generation
  • Loading branch information
aaronn committed Nov 17, 2020
2 parents e69b14b + 5bec326 commit 0a382e9
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 6 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```
Expand Down
2 changes: 1 addition & 1 deletion drfpasswordless/__init__.py
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion drfpasswordless/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION = (1, 5, 6)
VERSION = (1, 5, 7)

__version__ = '.'.join(map(str, VERSION))
17 changes: 17 additions & 0 deletions drfpasswordless/migrations/0005_auto_20201117_0410.py
Original file line number Diff line number Diff line change
@@ -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(),
),
]
2 changes: 0 additions & 2 deletions drfpasswordless/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -66,4 +65,3 @@ class CallbackToken(AbstractBaseCallbackToken):

class Meta(AbstractBaseCallbackToken.Meta):
verbose_name = 'Callback Token'
unique_together = ['is_active', 'key', 'type']
2 changes: 2 additions & 0 deletions drfpasswordless/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 26 additions & 2 deletions drfpasswordless/signals.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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



Expand Down

0 comments on commit 0a382e9

Please sign in to comment.