Skip to content

Commit 3750aee

Browse files
committed
Introduce a way to override how auth tokens are created
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.
1 parent dcf520f commit 3750aee

File tree

5 files changed

+66
-2
lines changed

5 files changed

+66
-2
lines changed

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,13 @@ DEFAULTS = {
291291
292292
# Automatically send verification email or sms when a user changes their alias.
293293
'PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN': False,
294+
295+
# What function is called to construct an authentication tokens when
296+
# exchanging a passwordless token for a real user auth token. This function
297+
# should take a user and return a tuple of two values. The first value is
298+
# the token itself, the second is a boolean value representating whether
299+
# the token was newly created.
300+
'PASSWORDLESS_AUTH_TOKEN_CREATOR': 'drfpasswordless.utils.create_authentication_token'
294301
}
295302
```
296303

drfpasswordless/settings.py

+3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@
6969
# Automatically send verification email or sms when a user changes their alias.
7070
'PASSWORDLESS_AUTO_SEND_VERIFICATION_TOKEN': False,
7171

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

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

drfpasswordless/utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.core.mail import send_mail
66
from django.template import loader
77
from django.utils import timezone
8+
from rest_framework.authtoken.models import Token
89
from drfpasswordless.models import CallbackToken
910
from drfpasswordless.settings import api_settings
1011

@@ -189,3 +190,8 @@ def send_sms_with_callback_token(user, mobile_token, **kwargs):
189190
"Number entered was {}".format(user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)))
190191
logger.debug(e)
191192
return False
193+
194+
195+
def create_authentication_token(user):
196+
""" Default way to create an authentication token"""
197+
return Token.objects.get_or_create(user=user)

drfpasswordless/views.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
2+
from django.utils.module_loading import import_string
23
from rest_framework import parsers, renderers, status
3-
from rest_framework.authtoken.models import Token
44
from rest_framework.response import Response
55
from rest_framework.permissions import AllowAny, IsAuthenticated
66
from rest_framework.views import APIView
@@ -130,7 +130,8 @@ def post(self, request, *args, **kwargs):
130130
serializer = self.serializer_class(data=request.data)
131131
if serializer.is_valid(raise_exception=True):
132132
user = serializer.validated_data['user']
133-
token = Token.objects.get_or_create(user=user)[0]
133+
token_creator = import_string(api_settings.PASSWORDLESS_AUTH_TOKEN_CREATOR)
134+
(token, _) = token_creator(user)
134135

135136
if token:
136137
# Return our key for consumption.

tests/test_authentication.py

+47
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,53 @@ def tearDown(self):
213213
api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER = DEFAULTS['PASSWORDLESS_MOBILE_NOREPLY_NUMBER']
214214

215215

216+
def dummy_token_creator(user):
217+
token = Token.objects.create(key="dummy", user=user)
218+
return (token, True)
219+
220+
221+
class OverrideTokenCreationTests(APITestCase):
222+
def setUp(self):
223+
super().setUp()
224+
225+
api_settings.PASSWORDLESS_AUTH_TOKEN_CREATOR = 'tests.test_authentication.dummy_token_creator'
226+
api_settings.PASSWORDLESS_AUTH_TYPES = ['EMAIL']
227+
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = 'noreply@example.com'
228+
229+
self.email = 'aaron@example.com'
230+
self.url = '/auth/email/'
231+
self.challenge_url = '/callback/auth/'
232+
233+
self.email_field_name = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
234+
self.user = User.objects.create(**{self.email_field_name: self.email})
235+
236+
def test_token_creation_gets_overridden(self):
237+
"""Ensure that if we change the token creation function, the overridden one gets called"""
238+
data = {'email': self.email}
239+
response = self.client.post(self.url, data)
240+
self.assertEqual(response.status_code, status.HTTP_200_OK)
241+
242+
# Token sent to alias
243+
callback_token = CallbackToken.objects.filter(user=self.user, is_active=True).first()
244+
challenge_data = {'token': callback_token}
245+
246+
# Try to auth with the callback token
247+
challenge_response = self.client.post(self.challenge_url, challenge_data)
248+
self.assertEqual(challenge_response.status_code, status.HTTP_200_OK)
249+
250+
# Verify Auth Token
251+
auth_token = challenge_response.data['token']
252+
self.assertEqual(auth_token, Token.objects.filter(key=auth_token).first().key)
253+
self.assertEqual('dummy', Token.objects.filter(key=auth_token).first().key)
254+
255+
def tearDown(self):
256+
api_settings.PASSWORDLESS_AUTH_TOKEN_CREATOR = DEFAULTS['PASSWORDLESS_AUTH_TOKEN_CREATOR']
257+
api_settings.PASSWORDLESS_AUTH_TYPES = DEFAULTS['PASSWORDLESS_AUTH_TYPES']
258+
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = DEFAULTS['PASSWORDLESS_EMAIL_NOREPLY_ADDRESS']
259+
self.user.delete()
260+
super().tearDown()
261+
262+
216263
class MobileLoginCallbackTokenTests(APITestCase):
217264

218265
def setUp(self):

0 commit comments

Comments
 (0)