Skip to content

Commit 0a382e9

Browse files
authored
Merge pull request aaronn#78 from aaronn/fix/unique-token-generation
Fix/unique token generation
2 parents e69b14b + 5bec326 commit 0a382e9

File tree

7 files changed

+50
-6
lines changed

7 files changed

+50
-6
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ DEFAULTS = {
320320
# configurable function for sending sms
321321
'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token'
322322
323+
# Token Generation Retry Count
324+
'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3
325+
323326
324327
}
325328
```

drfpasswordless/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
__title__ = 'drfpasswordless'
4-
__version__ = '1.5.6'
4+
__version__ = '1.5.7'
55
__author__ = 'Aaron Ng'
66
__license__ = 'MIT'
77
__copyright__ = 'Copyright 2020 Aaron Ng'

drfpasswordless/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
VERSION = (1, 5, 6)
1+
VERSION = (1, 5, 7)
22

33
__version__ = '.'.join(map(str, VERSION))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 3.0.2 on 2020-11-17 04:10
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('drfpasswordless', '0004_auto_20200125_0853'),
10+
]
11+
12+
operations = [
13+
migrations.AlterUniqueTogether(
14+
name='callbacktoken',
15+
unique_together=set(),
16+
),
17+
]

drfpasswordless/models.py

-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ class Meta:
4747
abstract = True
4848
get_latest_by = 'created_at'
4949
ordering = ['-id']
50-
unique_together = (('key', 'is_active'),)
5150

5251
def __str__(self):
5352
return str(self.key)
@@ -66,4 +65,3 @@ class CallbackToken(AbstractBaseCallbackToken):
6665

6766
class Meta(AbstractBaseCallbackToken.Meta):
6867
verbose_name = 'Callback Token'
69-
unique_together = ['is_active', 'key', 'type']

drfpasswordless/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
'PASSWORDLESS_EMAIL_CALLBACK': 'drfpasswordless.utils.send_email_with_callback_token',
8989
'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token',
9090

91+
# Token Generation Retry Count
92+
'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3
9193
}
9294

9395
# List of settings that may be in string import notation.

drfpasswordless/signals.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from django.contrib.auth import get_user_model
3+
from django.core.exceptions import ValidationError
34
from django.dispatch import receiver
45
from django.db.models import signals
56
from drfpasswordless.models import CallbackToken
@@ -27,18 +28,41 @@ def invalidate_previous_tokens(sender, instance, created, **kwargs):
2728
def check_unique_tokens(sender, instance, **kwargs):
2829
"""
2930
Ensures that mobile and email tokens are unique or tries once more to generate.
31+
Note that here we've decided keys are unique even across auth and validation.
32+
We could consider relaxing this in the future as well by filtering on the instance.type.
3033
"""
3134
if instance._state.adding:
3235
# save is called on a token to create it in the db
3336
# before creating check whether a token with the same key exists
3437
if isinstance(instance, CallbackToken):
38+
unique = False
39+
tries = 0
40+
3541
if CallbackToken.objects.filter(key=instance.key, is_active=True).exists():
36-
instance.key = generate_numeric_token()
42+
# Try N(default=3) times before giving up.
43+
while tries < api_settings.PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS:
44+
tries = tries + 1
45+
new_key = generate_numeric_token()
46+
instance.key = new_key
47+
48+
if not CallbackToken.objects.filter(key=instance.key, is_active=True).exists():
49+
# Leave the loop if we found a valid token that doesn't exist yet.
50+
unique = True
51+
break
52+
53+
if not unique:
54+
# A unique value wasn't found after three tries
55+
raise ValidationError("Couldn't create a unique token even after retrying.")
56+
else:
57+
# A unique value was found immediately.
58+
pass
59+
60+
3761
else:
3862
# save is called on an already existing token to update it. Such as invalidating it.
3963
# in that case there is no need to check for the key. This way we both avoid an unneccessary db hit
4064
# and avoid to change key field of used tokens.
41-
pass
65+
pass
4266

4367

4468

0 commit comments

Comments
 (0)