Skip to content

Commit 0caa8a6

Browse files
committed
Verification serializers and views
1 parent 7a3ec78 commit 0caa8a6

File tree

4 files changed

+190
-10
lines changed

4 files changed

+190
-10
lines changed

drfpasswordless/serializers.py

+63-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ def validate(self, attrs):
4444

4545
if alias:
4646
# Create or authenticate a user
47-
# Return a token for them to log in
48-
# Consider moving this into somewhere else. Serializer should only serialize.
47+
# Return THem
4948

5049
if api_settings.PASSWORDLESS_REGISTER_NEW_USERS is True:
5150
# If new aliases should register new users.
@@ -88,6 +87,68 @@ class MobileAuthSerializer(AbstractBaseAliasAuthenticationSerializer):
8887
mobile = serializers.CharField(validators=[phone_regex], max_length=15)
8988

9089

90+
"""
91+
Verification
92+
"""
93+
94+
95+
class AbstractBaseAliasVerificationSerializer(serializers.Serializer):
96+
"""
97+
Abstract class that returns a callback token based on the field given
98+
Returns a token if valid, None or a message if not.
99+
"""
100+
@property
101+
def alias_type(self):
102+
# The alias type, either email or mobile
103+
raise NotImplementedError
104+
105+
def validate(self, attrs):
106+
alias = attrs.get(self.alias_type)
107+
108+
if alias:
109+
# Get request.user
110+
# Get their specified valid endpoint
111+
# Validate
112+
113+
request = self.context.get("request")
114+
if request and hasattr(request, "user"):
115+
user = request.user
116+
user = user.refresh_from_db()
117+
118+
if user:
119+
if not user.is_active:
120+
# If valid, return attrs so we can create a token in our logic controller
121+
msg = _('User account is disabled.')
122+
123+
else:
124+
if hasattr(user, self.alias_type):
125+
# Has the appropriate alias type
126+
alias = getattr(user, self.alias_type)
127+
128+
if alias:
129+
attrs['user'] = user
130+
return attrs
131+
else:
132+
msg = _('This user doesn\'t have an %s.' % self.alias_type)
133+
raise serializers.ValidationError(msg)
134+
else:
135+
msg = _('There was a problem with your request.')
136+
raise serializers.ValidationError(msg)
137+
else:
138+
msg = _('Missing %s.') % self.alias_type
139+
raise serializers.ValidationError(msg)
140+
141+
142+
class EmailVerificationSerializer(AbstractBaseAliasAuthenticationSerializer):
143+
144+
alias_type = 'email'
145+
146+
147+
class MobileVerificationSerializer(AbstractBaseAliasAuthenticationSerializer):
148+
149+
alias_type = 'mobile'
150+
151+
91152
"""
92153
Callback Token
93154
"""

drfpasswordless/utils.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,12 @@ def send_email_with_callback_token(self, user, email_token, **kwargs):
133133
html_message=html_message,)
134134

135135
else:
136-
log.debug("Failed to send login email. Missing PASSWORDLESS_EMAIL_NOREPLY_ADDRESS.")
136+
log.debug("Failed to send token email. Missing PASSWORDLESS_EMAIL_NOREPLY_ADDRESS.")
137137
return False
138138
return True
139139

140140
except Exception as e:
141-
log.debug("Failed to send login email to user: %d."
141+
log.debug("Failed to send token email to user: %d."
142142
"Possibly no email on user object. Email entered was %s" %
143143
(user.id, getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME)))
144144
log.debug(e)
@@ -170,7 +170,7 @@ def send_sms_with_callback_token(self, user, mobile_token, **kwargs):
170170
)
171171
return True
172172
else:
173-
log.debug("Failed to send login sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.")
173+
log.debug("Failed to send token sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.")
174174
return False
175175
except ImportError:
176176
log.debug("Couldn't import Twilio client. Is twilio installed?")
@@ -179,7 +179,7 @@ def send_sms_with_callback_token(self, user, mobile_token, **kwargs):
179179
log.debug("Couldn't send SMS."
180180
"Did you set your Twilio account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?")
181181
except Exception as e:
182-
log.debug("Failed to send login SMS to user: %d. "
182+
log.debug("Failed to send token SMS to user: %d. "
183183
"Possibly no mobile number on user object or the twilio package isn't set up yet. "
184184
"Number entered was %s" % (user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)))
185185
log.debug(e)

drfpasswordless/views.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
from rest_framework.response import Response
55
from rest_framework.views import APIView
66
from .settings import api_settings
7-
from .serializers import EmailAuthSerializer, MobileAuthSerializer, CallbackTokenAuthSerializer, CallbackTokenVerificationSerializer
7+
from .serializers import (EmailAuthSerializer,
8+
MobileAuthSerializer,
9+
CallbackTokenAuthSerializer,
10+
CallbackTokenVerificationSerializer,
11+
EmailVerificationSerializer,
12+
MobileVerificationSerializer,)
813
from .utils import send_sms_with_callback_token, send_email_with_callback_token, create_callback_token_for_user
914

1015
log = logging.getLogger(__name__)
@@ -44,7 +49,7 @@ def post(self, request, *args, **kwargs):
4449
# Only allow auth types allowed in settings.
4550
return Response(status=status.HTTP_404_NOT_FOUND)
4651

47-
serializer = self.serializer_class(data=request.data)
52+
serializer = self.serializer_class(data=request.data, context={'request': request})
4853
if serializer.is_valid(raise_exception=True):
4954
# Validate -
5055
user = serializer.validated_data['user']
@@ -94,7 +99,7 @@ class ObtainMobileCallbackToken(AbstractBaseObtainCallbackToken):
9499

95100

96101
class ObtainEmailVerificationCallbackToken(AbstractBaseObtainCallbackToken):
97-
serializer_class = EmailAuthSerializer
102+
serializer_class = EmailVerificationSerializer
98103
send_action = send_email_with_callback_token
99104
success_response = "A verification token has been sent to your email."
100105
failure_response = "Unable to email you a verification code. Try again later."
@@ -110,7 +115,7 @@ class ObtainEmailVerificationCallbackToken(AbstractBaseObtainCallbackToken):
110115

111116

112117
class ObtainMobileVerificationCallbackToken(AbstractBaseObtainCallbackToken):
113-
serializer_class = MobileAuthSerializer
118+
serializer_class = MobileVerificationSerializer
114119
send_action = send_sms_with_callback_token
115120
success_response = "We texted you a verification code."
116121
failure_response = "Unable to send you a verification code. Try again later."

tests/test_verification.py

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from rest_framework import status
2+
from rest_framework.authtoken.models import Token
3+
from rest_framework.test import APITestCase
4+
5+
from django.contrib.auth import get_user_model
6+
from drfpasswordless.settings import api_settings, DEFAULTS
7+
from drfpasswordless.utils import CallbackToken
8+
9+
User = get_user_model()
10+
11+
12+
class AliasEmailVerificationEndpointTests(APITestCase):
13+
14+
def setUp(self):
15+
api_settings.PASSWORDLESS_AUTH_TYPES = ['EMAIL']
16+
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = 'noreply@example.com'
17+
api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED = True
18+
19+
self.url = '/auth/email/'
20+
self.callback_url = '/callback/auth/'
21+
self.verify_url = '/verify/email/'
22+
self.verify_callback = '/callback/verify/'
23+
self.email_field_name = api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
24+
self.email_verified_field_name = api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME
25+
26+
def test_email_unverified_to_verified_and_back(self):
27+
email = 'aaron@example.com'
28+
data = {'email': email}
29+
30+
# create a new user
31+
response = self.client.post(self.url, data)
32+
self.assertEqual(response.status_code, status.HTTP_200_OK)
33+
user = User.objects.get(**{self.email_field_name: email})
34+
self.assertNotEqual(user, None)
35+
self.assertEqual(getattr(user, self.email_verified_field_name), False)
36+
37+
# Verify a token exists for the user, sign in and check verified again
38+
callback = CallbackToken.objects.filter(user=user, is_active=True).first()
39+
callback_data = {'token': callback}
40+
callback_response = self.client.post(self.callback_url, callback_data)
41+
self.assertEqual(callback_response.status_code, status.HTTP_200_OK)
42+
43+
# Verify we got the token, then check and see that email_verified is now verified
44+
token = callback_response.data['token']
45+
self.assertEqual(token, Token.objects.get(user=user).key)
46+
47+
# Refresh and see that the endpoint is now verified as True
48+
user.refresh_from_db()
49+
self.assertEqual(getattr(user, self.email_verified_field_name), True)
50+
51+
# Change email, should result in flag changing to false
52+
setattr(user, self.email_field_name, 'aaron2@example.com')
53+
user.save()
54+
user.refresh_from_db()
55+
self.assertEqual(getattr(user, self.email_verified_field_name), False)
56+
57+
# Use verification endpoints to mark as true
58+
59+
60+
def tearDown(self):
61+
api_settings.PASSWORDLESS_AUTH_TYPES = DEFAULTS['PASSWORDLESS_AUTH_TYPES']
62+
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS = DEFAULTS['PASSWORDLESS_EMAIL_NOREPLY_ADDRESS']
63+
api_settings.PASSWORDLESS_USER_MARK_EMAIL_VERIFIED = DEFAULTS['PASSWORDLESS_USER_MARK_MOBILE_VERIFIED']
64+
65+
66+
class AliasMobileVerificationEndpointTests(APITestCase):
67+
68+
def setUp(self):
69+
api_settings.PASSWORDLESS_TEST_SUPPRESSION = True
70+
api_settings.PASSWORDLESS_AUTH_TYPES = ['MOBILE']
71+
api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER = '+15550000000'
72+
api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED = True
73+
74+
self.url = '/auth/mobile/'
75+
self.callback_url = '/callback/auth/'
76+
self.mobile_field_name = api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME
77+
self.mobile_verified_field_name = api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME
78+
79+
def test_mobile_unverified_to_verified_and_back(self):
80+
mobile = '+15551234567'
81+
data = {'mobile': mobile}
82+
83+
# create a new user
84+
response = self.client.post(self.url, data)
85+
self.assertEqual(response.status_code, status.HTTP_200_OK)
86+
user = User.objects.get(**{self.mobile_field_name: mobile})
87+
self.assertNotEqual(user, None)
88+
self.assertEqual(getattr(user, self.mobile_verified_field_name), False)
89+
90+
# Verify a token exists for the user, sign in and check verified again
91+
callback = CallbackToken.objects.filter(user=user, is_active=True).first()
92+
callback_data = {'token': callback}
93+
callback_response = self.client.post(self.callback_url, callback_data)
94+
self.assertEqual(callback_response.status_code, status.HTTP_200_OK)
95+
96+
# Verify we got the token, then check and see that email_verified is now verified
97+
token = callback_response.data['token']
98+
self.assertEqual(token, Token.objects.get(user=user).key)
99+
100+
# Refresh and see that the endpoint is now verified as True
101+
user.refresh_from_db()
102+
self.assertEqual(getattr(user, self.mobile_verified_field_name), True)
103+
104+
# Change email, should result in flag changing to false
105+
setattr(user, self.mobile_field_name, '+15557654321')
106+
user.save()
107+
user.refresh_from_db()
108+
self.assertEqual(getattr(user, self.mobile_verified_field_name), False)
109+
110+
def tearDown(self):
111+
api_settings.PASSWORDLESS_TEST_SUPPRESSION = DEFAULTS['PASSWORDLESS_TEST_SUPPRESSION']
112+
api_settings.PASSWORDLESS_AUTH_TYPES = DEFAULTS['PASSWORDLESS_AUTH_TYPES']
113+
api_settings.PASSWORDLESS_MOBILE_NOREPLY_ADDRESS = DEFAULTS['PASSWORDLESS_EMAIL_NOREPLY_ADDRESS']
114+
api_settings.PASSWORDLESS_USER_MARK_MOBILE_VERIFIED = DEFAULTS['PASSWORDLESS_USER_MARK_MOBILE_VERIFIED']

0 commit comments

Comments
 (0)