diff --git a/README.md b/README.md
index 79ba22c..ef2fc61 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,7 @@ curl -X POST -d "mobile=+15552143912" localhost:8000/auth/mobile/
TokenAuthentication scheme.
```bash
-curl -X POST -d "token=815381" localhost:8000/callback/auth/
+curl -X POST -d "email=aaron@email.com&token=815381" localhost:8000/auth/token/
> HTTP/1.0 200 OK
> {"token":"76be2d9ecfaf5fa4226d722bzdd8a4fff207ed0e”}
@@ -149,19 +149,11 @@ You’ll also need to set up an SMTP server to send emails (`See Django
Docs `__), but for
development you can set up a dummy development smtp server to test
emails. Sent emails will print to the console. `Read more
-here. `__
+here. `__
```python
# Settings.py
-…
-EMAIL_HOST = 'localhost'
-EMAIL_PORT = 1025
-```
-
-Then run the following:
-
-```bash
-python -m smtpd -n -c DebuggingServer localhost:1025
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
```
Configuring Mobile
@@ -210,11 +202,11 @@ This is off by default but can be turned on with
enabled they look for the User model fields ``email_verified`` or
``mobile_verified``.
-You can also use ``/validate/email/`` or ``/validate/mobile/`` which will
+You can also use ``auth/verify/email/`` or ``/auth/verify/mobile/`` which will
automatically send a token to the endpoint attached to the current
``request.user``'s email or mobile if available.
-You can then send that token to ``/callback/verify/`` which will double-check
+You can then send that token to ``/auth/verify/`` which will double-check
that the endpoint belongs to the request.user and mark the alias as verified.
Registration
@@ -239,6 +231,12 @@ DEFAULTS = {
# Allowed auth types, can be EMAIL, MOBILE, or both.
'PASSWORDLESS_AUTH_TYPES': ['EMAIL'],
+ # URL Prefix for Authentication Endpoints
+ 'PASSWORDLESS_AUTH_PREFIX': 'auth',
+
+ # URL Prefix for Verification Endpoints
+ 'PASSWORDLESS_VERIFY_PREFIX': 'auth',
+
# Amount of time that tokens last, in seconds
'PASSWORDLESS_TOKEN_EXPIRE_TIME': 15 * 60,
@@ -338,7 +336,7 @@ License
The MIT License (MIT)
-Copyright (c) 2018 Aaron Ng
+Copyright (c) 2020 Aaron Ng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/drfpasswordless/__init__.py b/drfpasswordless/__init__.py
index ecdfc2a..7c8b987 100644
--- a/drfpasswordless/__init__.py
+++ b/drfpasswordless/__init__.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
__title__ = 'drfpasswordless'
-__version__ = '1.4.0'
+__version__ = '1.5.0'
__author__ = 'Aaron Ng'
__license__ = 'MIT'
__copyright__ = 'Copyright 2020 Aaron Ng'
diff --git a/drfpasswordless/__version__.py b/drfpasswordless/__version__.py
index d9e5b37..d7ecfd8 100644
--- a/drfpasswordless/__version__.py
+++ b/drfpasswordless/__version__.py
@@ -1,3 +1,3 @@
-VERSION = (1, 4, 0)
+VERSION = (1, 5, 0)
__version__ = '.'.join(map(str, VERSION))
diff --git a/drfpasswordless/admin.py b/drfpasswordless/admin.py
index d983d07..268d699 100644
--- a/drfpasswordless/admin.py
+++ b/drfpasswordless/admin.py
@@ -19,8 +19,8 @@ def link_to_user(self, obj):
class AbstractCallbackTokenInline(admin.StackedInline):
max_num = 0
extra = 0
- readonly_fields = ('created_at', 'key', 'is_active')
- fields = ('created_at', 'user', 'key', 'is_active')
+ readonly_fields = ('created_at', 'key', 'type', 'is_active')
+ fields = ('created_at', 'user', 'key', 'type', 'is_active')
class CallbackInline(AbstractCallbackTokenInline):
@@ -28,7 +28,7 @@ class CallbackInline(AbstractCallbackTokenInline):
class AbstractCallbackTokenAdmin(UserLinkMixin, admin.ModelAdmin):
- readonly_fields = ('created_at', 'user', 'key')
- list_display = ('created_at', UserLinkMixin.LINK_TO_USER_FIELD, 'key', 'is_active')
- fields = ('created_at', 'user', 'key', 'is_active')
+ readonly_fields = ('created_at', 'user', 'key', 'type',)
+ list_display = ('created_at', UserLinkMixin.LINK_TO_USER_FIELD, 'key', 'type', 'is_active')
+ fields = ('created_at', 'user', 'key', 'type', 'is_active')
extra = 0
diff --git a/drfpasswordless/migrations/0003_callbacktoken_type.py b/drfpasswordless/migrations/0003_callbacktoken_type.py
new file mode 100644
index 0000000..2160ae2
--- /dev/null
+++ b/drfpasswordless/migrations/0003_callbacktoken_type.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.2 on 2020-01-22 08:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('drfpasswordless', '0002_auto_20200122_0424'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='callbacktoken',
+ name='type',
+ field=models.CharField(choices=[('AUTH', 'Auth'), ('VERIFY', 'Verify')], default='VERIFY', max_length=20),
+ preserve_default=False,
+ ),
+ ]
diff --git a/drfpasswordless/models.py b/drfpasswordless/models.py
index f657b16..bdac4ba 100644
--- a/drfpasswordless/models.py
+++ b/drfpasswordless/models.py
@@ -33,6 +33,7 @@ class AbstractBaseCallbackToken(models.Model):
When a new token is created, older ones of the same type are invalidated
via the pre_save signal in signals.py.
"""
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name=None, on_delete=models.CASCADE)
@@ -56,7 +57,12 @@ class CallbackToken(AbstractBaseCallbackToken):
"""
Generates a random six digit number to be returned.
"""
+ TOKEN_TYPE_AUTH = 'AUTH'
+ TOKEN_TYPE_VERIFY = 'VERIFY'
+ TOKEN_TYPES = ((TOKEN_TYPE_AUTH, 'Auth'), (TOKEN_TYPE_VERIFY, 'Verify'))
+
key = models.CharField(default=generate_numeric_token, max_length=6, unique=True)
+ type = models.CharField(max_length=20, choices=TOKEN_TYPES)
class Meta(AbstractBaseCallbackToken.Meta):
verbose_name = 'Callback Token'
diff --git a/drfpasswordless/serializers.py b/drfpasswordless/serializers.py
index eb6e9c1..f59623c 100644
--- a/drfpasswordless/serializers.py
+++ b/drfpasswordless/serializers.py
@@ -4,6 +4,7 @@
from django.core.exceptions import PermissionDenied
from django.core.validators import RegexValidator
from rest_framework import serializers
+from rest_framework.exceptions import ValidationError
from drfpasswordless.models import CallbackToken
from drfpasswordless.settings import api_settings
from drfpasswordless.utils import authenticate_by_token, verify_user_alias, validate_token_age
@@ -168,21 +169,48 @@ class AbstractBaseCallbackTokenSerializer(serializers.Serializer):
Abstract class inspired by DRF's own token serializer.
Returns a user if valid, None or a message if not.
"""
+ phone_regex = RegexValidator(regex=r'^\+?1?\d{9,15}$',
+ message="Mobile number must be entered in the format:"
+ " '+999999999'. Up to 15 digits allowed.")
+
+ email = serializers.EmailField(required=False) # Needs to be required=false to require both.
+ mobile = serializers.CharField(required=False, validators=[phone_regex], max_length=15)
token = TokenField(min_length=6, max_length=6, validators=[token_age_validator])
+ def validate_alias(self, attrs):
+ email = attrs.get('email', None)
+ mobile = attrs.get('mobile', None)
+
+ if email and mobile:
+ raise serializers.ValidationError()
+
+ if not email and not mobile:
+ raise serializers.ValidationError()
+
+ if email:
+ return 'email', email
+ elif mobile:
+ return 'mobile', mobile
+
+ return None
+
class CallbackTokenAuthSerializer(AbstractBaseCallbackTokenSerializer):
def validate(self, attrs):
- callback_token = attrs.get('token', None)
-
- token = CallbackToken.objects.get(key=callback_token, is_active=True)
+ # Check Aliases
+ try:
+ alias_type, alias = self.validate_alias(attrs)
+ callback_token = attrs.get('token', None)
+ user = User.objects.get(**{alias_type: alias})
+ token = CallbackToken.objects.get(**{'user': user,
+ 'key': callback_token,
+ 'type': CallbackToken.TOKEN_TYPE_AUTH,
+ 'is_active': True})
- if token:
- # Check the token type for our uni-auth method.
- # authenticates and checks the expiry of the callback token.
- user = authenticate_by_token(token)
- if user:
+ if token.user == user:
+ # Check the token type for our uni-auth method.
+ # authenticates and checks the expiry of the callback token.
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
@@ -203,8 +231,11 @@ def validate(self, attrs):
else:
msg = _('Invalid Token')
raise serializers.ValidationError(msg)
- else:
- msg = _('Missing authentication token.')
+ except User.DoesNotExist:
+ msg = _('Invalid alias parameters provided.')
+ raise serializers.ValidationError(msg)
+ except ValidationError:
+ msg = _('Invalid alias parameters provided.')
raise serializers.ValidationError(msg)
@@ -216,15 +247,17 @@ class CallbackTokenVerificationSerializer(AbstractBaseCallbackTokenSerializer):
def validate(self, attrs):
try:
+ alias_type, alias = self.validate_alias(attrs)
user_id = self.context.get("user_id")
+ user = User.objects.get(**{'id': user_id, alias_type: alias})
callback_token = attrs.get('token', None)
- token = CallbackToken.objects.get(key=callback_token, is_active=True)
- user = User.objects.get(pk=user_id)
+ token = CallbackToken.objects.get(**{'user': user,
+ 'key': callback_token,
+ 'type': CallbackToken.TOKEN_TYPE_VERIFY,
+ 'is_active': True})
if token.user == user:
- # Check that the token.user is the request.user
-
# Mark this alias as verified
success = verify_user_alias(user, token)
if success is False:
@@ -237,11 +270,11 @@ def validate(self, attrs):
logger.debug("drfpasswordless: User token mismatch when verifying alias.")
except CallbackToken.DoesNotExist:
- msg = _('Missing authentication token.')
+ msg = _('We could not verify this alias.')
logger.debug("drfpasswordless: Tried to validate alias with bad token.")
pass
except User.DoesNotExist:
- msg = _('Missing user.')
+ msg = _('We could not verify this alias.')
logger.debug("drfpasswordless: Tried to validate alias with bad user.")
pass
except PermissionDenied:
diff --git a/drfpasswordless/services.py b/drfpasswordless/services.py
index 42ca867..f165237 100644
--- a/drfpasswordless/services.py
+++ b/drfpasswordless/services.py
@@ -7,8 +7,8 @@
class TokenService(object):
@staticmethod
- def send_token(user, alias_type, **message_payload):
- token = create_callback_token_for_user(user, alias_type)
+ def send_token(user, alias_type, token_type, **message_payload):
+ token = create_callback_token_for_user(user, alias_type, token_type)
send_action = None
if alias_type == 'email':
send_action = send_email_with_callback_token
diff --git a/drfpasswordless/settings.py b/drfpasswordless/settings.py
index 24af854..cc33c27 100644
--- a/drfpasswordless/settings.py
+++ b/drfpasswordless/settings.py
@@ -8,6 +8,12 @@
# Allowed auth types, can be EMAIL, MOBILE, or both.
'PASSWORDLESS_AUTH_TYPES': ['EMAIL'],
+ # URL Prefix for Authentication Endpoints
+ 'PASSWORDLESS_AUTH_PREFIX': 'auth/',
+
+ # URL Prefix for Verification Endpoints
+ 'PASSWORDLESS_VERIFY_PREFIX': 'auth/verify/',
+
# Amount of time that tokens last, in seconds
'PASSWORDLESS_TOKEN_EXPIRE_TIME': 15 * 60,
diff --git a/drfpasswordless/signals.py b/drfpasswordless/signals.py
index 7e02fd1..de68979 100644
--- a/drfpasswordless/signals.py
+++ b/drfpasswordless/signals.py
@@ -10,20 +10,13 @@
logger = logging.getLogger(__name__)
-@receiver(signals.pre_save, sender=CallbackToken)
-def invalidate_previous_tokens(sender, instance, **kwargs):
+@receiver(signals.post_save, sender=CallbackToken)
+def invalidate_previous_tokens(sender, instance, created, **kwargs):
"""
- Invalidates all previously issued tokens as a post_save signal.
+ Invalidates all previously issued tokens of that type when a new one is created, used, or anything like that.
"""
- active_tokens = None
if isinstance(instance, CallbackToken):
- active_tokens = CallbackToken.objects.active().filter(user=instance.user).exclude(id=instance.id)
-
- # Invalidate tokens
- if active_tokens:
- for token in active_tokens:
- token.is_active = False
- token.save()
+ CallbackToken.objects.active().filter(user=instance.user, type=instance.type).exclude(id=instance.id).update(is_active=False)
@receiver(signals.pre_save, sender=CallbackToken)
@@ -72,7 +65,7 @@ def update_alias_verification(sender, instance, **kwargs):
message_payload = {'email_subject': email_subject,
'email_plaintext': email_plaintext,
'email_html': email_html}
- success = TokenService.send_token(instance, 'email', **message_payload)
+ success = TokenService.send_token(instance, 'email', CallbackToken.TOKEN_TYPE_VERIFY, **message_payload)
if success:
logger.info('drfpasswordless: Successfully sent email on updated address: %s'
@@ -104,7 +97,7 @@ def update_alias_verification(sender, instance, **kwargs):
if api_settings.PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN is True:
mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE
message_payload = {'mobile_message': mobile_message}
- success = TokenService.send_token(instance, 'mobile', **message_payload)
+ success = TokenService.send_token(instance, 'mobile', CallbackToken.TOKEN_TYPE_VERIFY, **message_payload)
if success:
logger.info('drfpasswordless: Successfully sent SMS on updated mobile: %s'
diff --git a/drfpasswordless/urls.py b/drfpasswordless/urls.py
index 1fa8f69..08189d7 100644
--- a/drfpasswordless/urls.py
+++ b/drfpasswordless/urls.py
@@ -1,3 +1,4 @@
+from drfpasswordless.settings import api_settings
from django.urls import path
from drfpasswordless.views import (
ObtainEmailCallbackToken,
@@ -8,11 +9,13 @@
ObtainMobileVerificationCallbackToken,
)
+app_name = 'drfpasswordless'
+
urlpatterns = [
- path('callback/auth/', ObtainAuthTokenFromCallbackToken.as_view(), name='auth_callback'),
- path('auth/email/', ObtainEmailCallbackToken.as_view(), name='auth_email'),
- path('auth/mobile/', ObtainMobileCallbackToken.as_view(), name='auth_mobile'),
- path('callback/verify/', VerifyAliasFromCallbackToken.as_view(), name='verify_callback'),
- path('verify/email/', ObtainEmailVerificationCallbackToken.as_view(), name='verify_email'),
- path('verify/mobile/', ObtainMobileVerificationCallbackToken.as_view(), name='verify_mobile'),
+ path(api_settings.PASSWORDLESS_AUTH_PREFIX + 'email/', ObtainEmailCallbackToken.as_view(), name='auth_email'),
+ path(api_settings.PASSWORDLESS_AUTH_PREFIX + 'mobile/', ObtainMobileCallbackToken.as_view(), name='auth_mobile'),
+ path(api_settings.PASSWORDLESS_AUTH_PREFIX + 'token/', ObtainAuthTokenFromCallbackToken.as_view(), name='auth_token'),
+ path(api_settings.PASSWORDLESS_VERIFY_PREFIX + 'email/', ObtainEmailVerificationCallbackToken.as_view(), name='verify_email'),
+ path(api_settings.PASSWORDLESS_VERIFY_PREFIX + 'mobile/', ObtainMobileVerificationCallbackToken.as_view(), name='verify_mobile'),
+ path(api_settings.PASSWORDLESS_VERIFY_PREFIX, VerifyAliasFromCallbackToken.as_view(), name='verify_token'),
]
diff --git a/drfpasswordless/utils.py b/drfpasswordless/utils.py
index d1e73ed..2bdc106 100644
--- a/drfpasswordless/utils.py
+++ b/drfpasswordless/utils.py
@@ -16,7 +16,7 @@
def authenticate_by_token(callback_token):
try:
- token = CallbackToken.objects.get(key=callback_token, is_active=True)
+ token = CallbackToken.objects.get(key=callback_token, is_active=True, type=CallbackToken.TOKEN_TYPE_AUTH)
# Returning a user designates a successful authentication.
token.user = User.objects.get(pk=token.user.pk)
@@ -35,20 +35,22 @@ def authenticate_by_token(callback_token):
return None
-def create_callback_token_for_user(user, token_type):
+def create_callback_token_for_user(user, alias_type, token_type):
token = None
- token_type = token_type.upper()
+ alias_type_u = alias_type.upper()
- if token_type == 'EMAIL':
+ if alias_type_u == 'EMAIL':
token = CallbackToken.objects.create(user=user,
- to_alias_type=token_type,
- to_alias=getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME))
+ to_alias_type=alias_type_u,
+ to_alias=getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME),
+ type=token_type)
- elif token_type == 'MOBILE':
+ elif alias_type_u == 'MOBILE':
token = CallbackToken.objects.create(user=user,
- to_alias_type=token_type,
- to_alias=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME))
+ to_alias_type=alias_type_u,
+ to_alias=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME),
+ type=token_type)
if token is not None:
return token
diff --git a/drfpasswordless/views.py b/drfpasswordless/views.py
index fa38396..500cfbb 100644
--- a/drfpasswordless/views.py
+++ b/drfpasswordless/views.py
@@ -4,6 +4,7 @@
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.views import APIView
+from drfpasswordless.models import CallbackToken
from drfpasswordless.settings import api_settings
from drfpasswordless.serializers import (
EmailAuthSerializer,
@@ -37,6 +38,11 @@ def alias_type(self):
# Alias Type
raise NotImplementedError
+ @property
+ def token_type(self):
+ # Token Type
+ raise NotImplementedError
+
def post(self, request, *args, **kwargs):
if self.alias_type.upper() not in api_settings.PASSWORDLESS_AUTH_TYPES:
# Only allow auth types allowed in settings.
@@ -47,7 +53,7 @@ def post(self, request, *args, **kwargs):
# Validate -
user = serializer.validated_data['user']
# Create and send callback token
- success = TokenService.send_token(user, self.alias_type, **self.message_payload)
+ success = TokenService.send_token(user, self.alias_type, self.token_type, **self.message_payload)
# Respond With Success Or Failure of Sent
if success:
@@ -68,6 +74,7 @@ class ObtainEmailCallbackToken(AbstractBaseObtainCallbackToken):
failure_response = "Unable to email you a login code. Try again later."
alias_type = 'email'
+ token_type = CallbackToken.TOKEN_TYPE_AUTH
email_subject = api_settings.PASSWORDLESS_EMAIL_SUBJECT
email_plaintext = api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE
@@ -84,6 +91,7 @@ class ObtainMobileCallbackToken(AbstractBaseObtainCallbackToken):
failure_response = "Unable to send you a login code. Try again later."
alias_type = 'mobile'
+ token_type = CallbackToken.TOKEN_TYPE_AUTH
mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE
message_payload = {'mobile_message': mobile_message}
@@ -96,6 +104,7 @@ class ObtainEmailVerificationCallbackToken(AbstractBaseObtainCallbackToken):
failure_response = "Unable to email you a verification code. Try again later."
alias_type = 'email'
+ token_type = CallbackToken.TOKEN_TYPE_VERIFY
email_subject = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_SUBJECT
email_plaintext = api_settings.PASSWORDLESS_EMAIL_VERIFICATION_PLAINTEXT_MESSAGE
@@ -114,6 +123,7 @@ class ObtainMobileVerificationCallbackToken(AbstractBaseObtainCallbackToken):
failure_response = "Unable to send you a verification code. Try again later."
alias_type = 'mobile'
+ token_type = CallbackToken.TOKEN_TYPE_VERIFY
mobile_message = api_settings.PASSWORDLESS_MOBILE_MESSAGE
message_payload = {'mobile_message': mobile_message}
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index ecda58f..ca3d44a 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -3,6 +3,7 @@
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
+from django.urls import reverse
from drfpasswordless.settings import api_settings, DEFAULTS
from drfpasswordless.utils import CallbackToken
@@ -15,7 +16,7 @@ def setUp(self):
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = 'noreply@example.com'
self.email_field_name = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
- self.url = '/auth/email/'
+ self.url = reverse('drfpasswordless:auth_email')
def test_email_signup_failed(self):
email = 'failedemail182+'
@@ -75,8 +76,8 @@ def setUp(self):
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = 'noreply@example.com'
self.email = 'aaron@example.com'
- self.url = '/auth/email/'
- self.challenge_url = '/callback/auth/'
+ self.url = reverse('drfpasswordless:auth_email')
+ self.challenge_url = reverse('drfpasswordless:auth_token')
self.email_field_name = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
self.user = User.objects.create(**{self.email_field_name: self.email})
@@ -87,7 +88,33 @@ def test_email_auth_failed(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Token sent to alias
- challenge_data = {'token': '123456'} # Send an arbitrary token instead
+ challenge_data = {'email': self.email, 'token': '123456'} # Send an arbitrary token instead
+
+ # Try to auth with the callback token
+ challenge_response = self.client.post(self.challenge_url, challenge_data)
+ self.assertEqual(challenge_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_email_auth_missing_alias(self):
+ data = {'email': self.email}
+ response = self.client.post(self.url, data)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ # Token sent to alias
+ callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
+ challenge_data = {'token': callback_token} # Missing Alias
+
+ # Try to auth with the callback token
+ challenge_response = self.client.post(self.challenge_url, challenge_data)
+ self.assertEqual(challenge_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_email_auth_bad_alias(self):
+ data = {'email': self.email}
+ response = self.client.post(self.url, data)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ # Token sent to alias
+ callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
+ challenge_data = {'email': 'abcde@example.com', 'token': callback_token} # Bad Alias
# Try to auth with the callback token
challenge_response = self.client.post(self.challenge_url, challenge_data)
@@ -100,7 +127,7 @@ def test_email_auth_expired(self):
# Token sent to alias
callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
- challenge_data = {'token': callback_token}
+ challenge_data = {'email': self.email, 'token': callback_token}
data = {'email': self.email}
response = self.client.post(self.url, data)
@@ -108,7 +135,7 @@ def test_email_auth_expired(self):
# Second token sent to alias
second_callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
- second_challenge_data = {'token': second_callback_token}
+ second_challenge_data = {'email': self.email, 'token': second_callback_token}
# Try to auth with the old callback token
challenge_response = self.client.post(self.challenge_url, challenge_data)
@@ -129,7 +156,7 @@ def test_email_auth_success(self):
# Token sent to alias
callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
- challenge_data = {'token': callback_token}
+ challenge_data = {'email': self.email, 'token': callback_token}
# Try to auth with the callback token
challenge_response = self.client.post(self.challenge_url, challenge_data)
@@ -156,7 +183,7 @@ def setUp(self):
api_settings.PASSWORDLESS_TEST_SUPPRESSION = True
api_settings.PASSWORDLESS_AUTH_TYPES = ['MOBILE']
api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER = '+15550000000'
- self.url = '/auth/mobile/'
+ self.url = reverse('drfpasswordless:auth_mobile')
self.mobile_field_name = api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME
@@ -227,8 +254,8 @@ def setUp(self):
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = 'noreply@example.com'
self.email = 'aaron@example.com'
- self.url = '/auth/email/'
- self.challenge_url = '/callback/auth/'
+ self.url = reverse('drfpasswordless:auth_email')
+ self.challenge_url = reverse('drfpasswordless:auth_token')
self.email_field_name = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
self.user = User.objects.create(**{self.email_field_name: self.email})
@@ -241,7 +268,7 @@ def test_token_creation_gets_overridden(self):
# Token sent to alias
callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
- challenge_data = {'token': callback_token}
+ challenge_data = {'email': self.email, 'token': callback_token}
# Try to auth with the callback token
challenge_response = self.client.post(self.challenge_url, challenge_data)
@@ -268,8 +295,8 @@ def setUp(self):
api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER = '+15550000000'
self.mobile = '+15551234567'
- self.url = '/auth/mobile/'
- self.challenge_url = '/callback/auth/'
+ self.url = reverse('drfpasswordless:auth_mobile')
+ self.challenge_url = reverse('drfpasswordless:auth_token')
self.mobile_field_name = api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME
@@ -281,7 +308,7 @@ def test_mobile_auth_failed(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Token sent to alias
- challenge_data = {'token': '123456'} # Send an arbitrary token instead
+ challenge_data = {'mobile': self.mobile, 'token': '123456'} # Send an arbitrary token instead
# Try to auth with the callback token
challenge_response = self.client.post(self.challenge_url, challenge_data)
@@ -294,7 +321,7 @@ def test_mobile_auth_expired(self):
# Token sent to alias
first_callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
- first_challenge_data = {'token': first_callback_token}
+ first_challenge_data = {'mobile': self.mobile, 'token': first_callback_token}
data = {'mobile': self.mobile}
second_response = self.client.post(self.url, data)
@@ -302,7 +329,7 @@ def test_mobile_auth_expired(self):
# Second token sent to alias
second_callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
- second_challenge_data = {'token': second_callback_token}
+ second_challenge_data = {'mobile': self.mobile, 'token': second_callback_token}
# Try to auth with the old callback token
challenge_response = self.client.post(self.challenge_url, first_challenge_data)
@@ -323,7 +350,7 @@ def test_mobile_auth_success(self):
# Token sent to alias
callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
- challenge_data = {'token': callback_token}
+ challenge_data = {'mobile': self.mobile, 'token': callback_token}
# Try to auth with the callback token
challenge_response = self.client.post(self.challenge_url, challenge_data)
diff --git a/tests/test_settings.py b/tests/test_settings.py
index f62524f..a6799d3 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -3,6 +3,7 @@
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
+from django.urls import reverse
from drfpasswordless.settings import api_settings, DEFAULTS
from drfpasswordless.utils import CallbackToken
@@ -19,11 +20,11 @@ def setUp(self):
api_settings.PASSWORDLESS_TEST_SUPPRESSION = True
self.email = 'aaron@example.com'
- self.email_url = '/auth/email/'
+ self.email_url = reverse('drfpasswordless:auth_email')
self.email_data = {'email': self.email}
self.mobile = '+15551234567'
- self.mobile_url = '/auth/mobile/'
+ self.mobile_url = reverse('drfpasswordless:auth_mobile')
self.mobile_data = {'mobile': self.mobile}
def test_email_auth_disabled(self):
@@ -89,8 +90,8 @@ def setUp(self):
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = 'noreply@example.com'
api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED = True
- self.url = '/auth/email/'
- self.callback_url = '/callback/auth/'
+ self.url = reverse('drfpasswordless:auth_email')
+ self.callback_url = reverse('drfpasswordless:auth_token')
self.email_field_name = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
self.email_verified_field_name = api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME
@@ -107,7 +108,7 @@ def test_email_unverified_to_verified_and_back(self):
# Verify a token exists for the user, sign in and check verified again
callback = CallbackToken.objects.filter(user=user, is_active=True).first()
- callback_data = {'token': callback}
+ callback_data = {'email': email, 'token': callback}
callback_response = self.client.post(self.callback_url, callback_data)
self.assertEqual(callback_response.status_code, status.HTTP_200_OK)
@@ -139,8 +140,8 @@ def setUp(self):
api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER = '+15550000000'
api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED = True
- self.url = '/auth/mobile/'
- self.callback_url = '/callback/auth/'
+ self.url = reverse('drfpasswordless:auth_mobile')
+ self.callback_url = reverse('drfpasswordless:auth_token')
self.mobile_field_name = api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME
self.mobile_verified_field_name = api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME
@@ -157,7 +158,7 @@ def test_mobile_unverified_to_verified_and_back(self):
# Verify a token exists for the user, sign in and check verified again
callback = CallbackToken.objects.filter(user=user, is_active=True).first()
- callback_data = {'token': callback}
+ callback_data = {'mobile': mobile, 'token': callback}
callback_response = self.client.post(self.callback_url, callback_data)
self.assertEqual(callback_response.status_code, status.HTTP_200_OK)
diff --git a/tests/test_verification.py b/tests/test_verification.py
index 37873ed..43a846d 100644
--- a/tests/test_verification.py
+++ b/tests/test_verification.py
@@ -2,6 +2,7 @@
from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
+from django.urls import reverse
from drfpasswordless.settings import api_settings, DEFAULTS
from drfpasswordless.utils import CallbackToken
@@ -15,10 +16,10 @@ def setUp(self):
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = 'noreply@example.com'
api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED = True
- self.url = '/auth/email/'
- self.callback_url = '/callback/auth/'
- self.verify_url = '/verify/email/'
- self.callback_verify = '/callback/verify/'
+ self.url = reverse('drfpasswordless:auth_email')
+ self.callback_url = reverse('drfpasswordless:auth_token')
+ self.verify_url = reverse('drfpasswordless:verify_email')
+ self.callback_verify = reverse('drfpasswordless:verify_token')
self.email_field_name = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
self.email_verified_field_name = api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME
@@ -35,8 +36,8 @@ def test_email_unverified_to_verified_and_back(self):
self.assertEqual(getattr(user, self.email_verified_field_name), False)
# Verify a token exists for the user, sign in and check verified again
- callback = CallbackToken.objects.filter(user=user, is_active=True).first()
- callback_data = {'token': callback}
+ callback = CallbackToken.objects.filter(user=user, type=CallbackToken.TOKEN_TYPE_AUTH, is_active=True).first()
+ callback_data = {'email': email, 'token': callback}
callback_response = self.client.post(self.callback_url, callback_data)
self.assertEqual(callback_response.status_code, status.HTTP_200_OK)
@@ -66,8 +67,9 @@ def test_email_unverified_to_verified_and_back(self):
self.assertEqual(getattr(user, self.email_verified_field_name), False)
# Post callback token back.
- verify_token = CallbackToken.objects.filter(user=user, is_active=True).first()
- verify_callback_response = self.client.post(self.callback_verify, {'token': verify_token.key})
+ verify_token = CallbackToken.objects.filter(user=user, type=CallbackToken.TOKEN_TYPE_VERIFY, is_active=True).first()
+ self.assertNotEqual(verify_token, None)
+ verify_callback_response = self.client.post(self.callback_verify, {'email': email2, 'token': verify_token.key})
self.assertEqual(verify_callback_response.status_code, status.HTTP_200_OK)
# Refresh User
@@ -90,10 +92,10 @@ def setUp(self):
api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER = '+15550000000'
api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED = True
- self.url = '/auth/mobile/'
- self.callback_url = '/callback/auth/'
- self.verify_url = '/verify/mobile/'
- self.callback_verify = '/callback/verify/'
+ self.url = reverse('drfpasswordless:auth_mobile')
+ self.callback_url = reverse('drfpasswordless:auth_token')
+ self.verify_url = reverse('drfpasswordless:verify_mobile')
+ self.callback_verify = reverse('drfpasswordless:verify_token')
self.mobile_field_name = api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME
self.mobile_verified_field_name = api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME
@@ -110,8 +112,8 @@ def test_mobile_unverified_to_verified_and_back(self):
self.assertEqual(getattr(user, self.mobile_verified_field_name), False)
# Verify a token exists for the user, sign in and check verified again
- callback = CallbackToken.objects.filter(user=user, is_active=True).first()
- callback_data = {'token': callback}
+ callback = CallbackToken.objects.filter(user=user, type=CallbackToken.TOKEN_TYPE_AUTH, is_active=True).first()
+ callback_data = {'mobile': mobile, 'token': callback}
callback_response = self.client.post(self.callback_url, callback_data)
self.assertEqual(callback_response.status_code, status.HTTP_200_OK)
@@ -141,8 +143,9 @@ def test_mobile_unverified_to_verified_and_back(self):
self.assertEqual(getattr(user, self.mobile_verified_field_name), False)
# Post callback token back.
- verify_token = CallbackToken.objects.filter(user=user, is_active=True).first()
- verify_callback_response = self.client.post(self.callback_verify, {'token': verify_token.key})
+ verify_token = CallbackToken.objects.filter(user=user, type=CallbackToken.TOKEN_TYPE_VERIFY, is_active=True).first()
+ self.assertNotEqual(verify_token, None)
+ verify_callback_response = self.client.post(self.callback_verify, {'mobile': mobile2, 'token': verify_token.key})
self.assertEqual(verify_callback_response.status_code, status.HTTP_200_OK)
# Refresh User
diff --git a/tests/urls.py b/tests/urls.py
index f7020d1..120fea0 100644
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -1,5 +1,6 @@
-from django.conf.urls import url
+from django.urls import path, include
from rest_framework.urlpatterns import format_suffix_patterns
+from drfpasswordless.settings import api_settings
from drfpasswordless.views import (ObtainEmailCallbackToken,
ObtainMobileCallbackToken,
ObtainAuthTokenFromCallbackToken,
@@ -7,11 +8,10 @@
ObtainEmailVerificationCallbackToken,
ObtainMobileVerificationCallbackToken, )
-urlpatterns = [url(r'^callback/auth/$', ObtainAuthTokenFromCallbackToken.as_view(), name='auth_callback'),
- url(r'^auth/email/$', ObtainEmailCallbackToken.as_view(), name='auth_email'),
- url(r'^auth/mobile/$', ObtainMobileCallbackToken.as_view(), name='auth_mobile'),
- url(r'^callback/verify/$', VerifyAliasFromCallbackToken.as_view(), name='verify_callback'),
- url(r'^verify/email/$', ObtainEmailVerificationCallbackToken.as_view(), name='verify_email'),
- url(r'^verify/mobile/$', ObtainMobileVerificationCallbackToken.as_view(), name='verify_mobile')]
+app_name = 'drfpasswordless'
+
+urlpatterns = [
+ path('', include('drfpasswordless.urls')),
+]
format_suffix_patterns(urlpatterns)