Skip to content

Commit

Permalink
Introduce a way to override how auth tokens are created
Browse files Browse the repository at this point in the history
This creates a new setting PASSWORDLESS_AUTH_TOKEN_CREATOR. This is a
string representing the function used to construct an authentication
token after receiving a valid passwordless token.
  • Loading branch information
aleffert committed Dec 8, 2019
1 parent dcf520f commit 3750aee
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 2 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ DEFAULTS = {
# Automatically send verification email or sms when a user changes their alias.
'PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN': False,
# What function is called to construct an authentication tokens when
# exchanging a passwordless token for a real user auth token. This function
# should take a user and return a tuple of two values. The first value is
# the token itself, the second is a boolean value representating whether
# the token was newly created.
'PASSWORDLESS_AUTH_TOKEN_CREATOR': 'drfpasswordless.utils.create_authentication_token'
}
```
Expand Down
3 changes: 3 additions & 0 deletions drfpasswordless/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
# Automatically send verification email or sms when a user changes their alias.
'PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN': False,

# What function is called to construct an authentication tokens when
# exchanging a passwordless token for a real user auth token.
'PASSWORDLESS_AUTH_TOKEN_CREATOR': 'drfpasswordless.utils.create_authentication_token'
}

# List of settings that may be in string import notation.
Expand Down
6 changes: 6 additions & 0 deletions drfpasswordless/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.core.mail import send_mail
from django.template import loader
from django.utils import timezone
from rest_framework.authtoken.models import Token
from drfpasswordless.models import CallbackToken
from drfpasswordless.settings import api_settings

Expand Down Expand Up @@ -189,3 +190,8 @@ def send_sms_with_callback_token(user, mobile_token, **kwargs):
"Number entered was {}".format(user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)))
logger.debug(e)
return False


def create_authentication_token(user):
""" Default way to create an authentication token"""
return Token.objects.get_or_create(user=user)
5 changes: 3 additions & 2 deletions drfpasswordless/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from django.utils.module_loading import import_string
from rest_framework import parsers, renderers, status
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.views import APIView
Expand Down Expand Up @@ -130,7 +130,8 @@ def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid(raise_exception=True):
user = serializer.validated_data['user']
token = Token.objects.get_or_create(user=user)[0]
token_creator = import_string(api_settings.PASSWORDLESS_AUTH_TOKEN_CREATOR)
(token, _) = token_creator(user)

if token:
# Return our key for consumption.
Expand Down
47 changes: 47 additions & 0 deletions tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,53 @@ def tearDown(self):
api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER = DEFAULTS['PASSWORDLESS_MOBILE_NOREPLY_NUMBER']


def dummy_token_creator(user):
token = Token.objects.create(key="dummy", user=user)
return (token, True)


class OverrideTokenCreationTests(APITestCase):
def setUp(self):
super().setUp()

api_settings.PASSWORDLESS_AUTH_TOKEN_CREATOR = 'tests.test_authentication.dummy_token_creator'
api_settings.PASSWORDLESS_AUTH_TYPES = ['EMAIL']
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = '[email protected]'

self.email = '[email protected]'
self.url = '/auth/email/'
self.challenge_url = '/callback/auth/'

self.email_field_name = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
self.user = User.objects.create(**{self.email_field_name: self.email})

def test_token_creation_gets_overridden(self):
"""Ensure that if we change the token creation function, the overridden one gets called"""
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}

# 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_200_OK)

# Verify Auth Token
auth_token = challenge_response.data['token']
self.assertEqual(auth_token, Token.objects.filter(key=auth_token).first().key)
self.assertEqual('dummy', Token.objects.filter(key=auth_token).first().key)

def tearDown(self):
api_settings.PASSWORDLESS_AUTH_TOKEN_CREATOR = DEFAULTS['PASSWORDLESS_AUTH_TOKEN_CREATOR']
api_settings.PASSWORDLESS_AUTH_TYPES = DEFAULTS['PASSWORDLESS_AUTH_TYPES']
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = DEFAULTS['PASSWORDLESS_EMAIL_NOREPLY_ADDRESS']
self.user.delete()
super().tearDown()


class MobileLoginCallbackTokenTests(APITestCase):

def setUp(self):
Expand Down

0 comments on commit 3750aee

Please sign in to comment.