NOTE: This is the final part of this multi-part series.
Part 3 is the end.
Final Step 5 - Writing final views.py
We arrive at the final and longest snippet of our code that will connect all dots and make the previous steps more relevant to us. We partition all code contents from views.py
into 3 parts. Specifically, the first part will detail the class ValidatePhoneSendOTP
, the second part on the two very important functions generate_otp
and push_to_sparrow
and the third part will complete this tutorial with ValidateOTP
.
Starting with imports
from django and django rest framework, the main focus is on the class ValidatePhoneSendOTP
that inherits the APIView
of rest_framework.views
.
The main objectives of this class are to:
- check user input against the database for an already existing phone with verified or unverified status, count of OTP requests to sparrow SMS API and a completely new phone entry to register in the database.
- only allow authenticated users to access the API endpoint with allowed methods POST, OPTIONS and HEAD.
- make changes to
PhoneOtp
ofmodels.py
and its objects likephone_number
,otp_code
andotp_count
.
Here we import two necessary models AccountUser
and PhoneOtp
from models.py on line 10. Given the UI validation plus API restrictions work valid, the def post
method expects a clean input. Then it checks against the AccountUser
and PhoneOtp
database models to check if the phone already exists, verified status and its username attached.
Fetching the contents from the database, we run the if else elif and not
conditions check all over the class to perform our required actions step by step. On line 35, we take the param phone_number
and pass it to generate_otp
function to be saved in a key
variable.
So when a user has their phone already entered in the database plus the verified status is False then the number of otp_count
is checked for too many requests. If the count has not reached the max limit which you can set in line 42 then we use the parameters of key
as otp and phone_number
as phone_number to pass it to push_to_sparrow
function that will call the Sparrow SMS API to send a generated OTP to that phone number in line 50.
From line 55 we start the same checks that we processed above but for a totally new number. Firstly calling generate_otp
function and then calling push_to_sparrow
in lines 56, 58.
However, a new code appears in line 59, PhoneOtp.objects.create
adds a new entry into the PhoneOtp
model with its required objects phone_number
, otp_code
and otp_count
; completing the objective of this class to work with new numbers.
def generate_otp(phone_number):
if phone_number:
key = random.randint(99999, 999999)
return key
else:
return False
def push_to_sparrow(otp, phone_number):
with open('/etc/config.json') as config_file:
config = json.load(config_file)
if config:
msg = " is your verification code for example.com. Thank you for joining us. Download our app at https://example.com/"
r = requests.post(
"http://api.sparrowsms.com/v2/sms/",
data={'token': config['SPARROW_TOKEN'],
'from': config['SPARROW_IDENTITY'],
'to': phone_number,
'text': "<#> \n" + str(otp) + msg + " \nwu+aGJn10+0"
})
print("<#> \n" + str(otp) + msg + " \nwu+aGJn10+0")
status_code = r.status_code
response = r.text
response_json = r.json()
else:
return Response({
'status': False,
'details': 'Could not parse configurations.'
})
return status_code, response, response_json
Next up we create two functions generate_otp
and push_to_sparrow
to do exactly verbatim in its naming.
The generate_otp
function requires a parameter phone_number
. Given the input, a key
variable stores a randomly generated integer from (start, end). In the code below, we're trying to generate a 6 digit only number so if you want shorter or longer otp digits then use the (start, end) accordingly.
The push_to_sparrow
function is completely dedicated to Sparrow SMS API calls. For better security practices, we save all the secret keys and tokens in a config_file in JSON format. Since this config file contains all crucial keys, we put the only copy into a file directory that is inaccessible from the public web in a Linux environment. Hence, the /etc/config.json
file path.
Next, we use very popular python library requests to send HTTP requests to our SMS API endpoint. The URL enclosed within the quotes is provided by the Sparrow SMS Gateway to the developers. So using other gateway services like Twilio would provide you with a completely different URL.
SPARROW_IDENTITY
param works as an identifier between you and the SMS gateway whereas the SPARROW_TOKEN
is a key to allow access to use their gateway. Both params are provided by the Sparrow SMS to their developers from their web dashboard.
r = requests.post()
sends the request to the API endpoint. And we python print the otp
, msg
and a unique code as a final SMS to be received by the user's phone.
As you'll notice at the tail of the print, we've set a unique code. This is Google's SMS Retriever API code. "With the SMS Retriever API, you can perform SMS-based user verification in your Android app automatically, without requiring the user to manually type verification codes, and without requiring any extra app permissions." - Google Identity
class ValidateOTP(APIView):
permission_classes = [IsAuthenticated, ]
def post(self, request, *args, **kwargs):
phone_number_sent_by_user = request.data.get('phone', False)
otp_sent_by_user = request.data.get('otp', False)
username_sent = request.data.get('username', False)
if otp_sent_by_user and phone_number_sent_by_user and username_sent:
username_queryset = AccountUser.objects.filter(username__iexact=username_sent)
phone_queryset = PhoneOtp.objects.filter(phone_number__iexact=phone_number_sent_by_user)
if username_queryset.exists() and phone_queryset.exists():
verified_qs = username_queryset.values('phone_verified')[0].values()
verified_status = list(verified_qs)[0]
if verified_status is True:
return Response({
'status': 403,
'details': 'Either phone or user is already verified.'
}, status=status.HTTP_403_FORBIDDEN)
user_qs = username_queryset.first()
qs = phone_queryset.first()
otp_in_db = qs.otp_code
if str(otp_sent_by_user) == otp_in_db:
qs.validated = True
user_qs.phone_verified = True
user_qs.phone_number = phone_number_sent_by_user
qs.save()
user_qs.save()
return Response({
'status': 200,
'details': 'Success! You have verified your phone number.'
}, status=status.HTTP_200_OK)
else:
return Response({
'status': 400,
'details': 'Incorrect OTP. Try again'
}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({
'status': 400,
'details': 'Phone number or username is mistaken. Please check.'
}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({
'status': 400,
'details': 'Invalid phone, username or otp provided.'
}, status=status.HTTP_400_BAD_REQUEST)
Finally, we create an APIView class ValidateOTP
which has the sole purpose of validating three entities involved in our phone verification system:
- the authenticity of the OTP code sent from the gateway service to the actual receiver,
- an exact match of the OTP code input from the user's phone and stored in the database,
- and the integrity of the phone number in the database and the one sent by the user.
Given these data points are validated, our code above checks five different cases to perform five different actions.
- given username and phone querysets return True; check the verified_status; if True then respond with 403 error that the phone is already verified. Hence skip all other actions.
- check if
otp_sent_by_user
againstotp_in_db
; respond with 200 OK that the phone is now verified for the first time. - else there's a mismatch in codes and hence respond with error 400 incorrect requests.
- one step out of the if scope above, the code jumps back to (1.) when the given username and phone querysets returns False; hence respond with 400 error that the input is mistaken or wrong on the user's end.
- a step more out of the scope and we respond with a 400 error that the inputs passed from class
ValidatePhoneSendOTP
are invalid in this validation step.
about TODOS in the code examples
# todo1 new username with phone: null trying to push matching phone of the existing user could raise a unique error;
# todo2 handle this error in a user-friendly way from within API level
# todo3 a loophole in case of leaked OTP, a hacker could POST unverified 'username' & leaked 'otp' to set the phone numbers to other accounts.