Django | User registration with Twillio
A small website which has following features:
- Uses custom user model extending django’s inbuilt AbstractBaseUser class for registration and authentication.
- Using Authy Phone Verification API for confirmation of user’s identity by sending One-time password to user’s phone number.
- Optional two factor authentication feature for users.
Source code can be found on Github link.
Methodology:
Creating Custom User Model
Create a new app for registration, authentication and other user functionalities. Let’s name it accounts
. In this app let’s define our User
model in models.py
.
accounts/models.py
:
from datetime import date
from django.db import models
from django.contrib.auth.models import PermissionsMixin, AbstractUser
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _
from .manager import UserManager
class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(_('username'), max_length=130, unique=True)
full_name = models.CharField(_('full name'), max_length=130, blank=True)
is_staff = models.BooleanField(_('is_staff'), default=False)
is_active = models.BooleanField(_('is_active'), default=True)
date_joined = models.DateField(_("date_joined"), default=date.today)
phone_number_verified = models.BooleanField(default=False)
change_pw = models.BooleanField(default=True)
phone_number = models.BigIntegerField(unique=True)
country_code = models.IntegerField()
two_factor_auth = models.BooleanField(default=False)
objects = UserManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['full_name', 'phone_number', 'country_code']
class Meta:
ordering = ('username',)
verbose_name = _('user')
verbose_name_plural = _('users')
def get_short_name(self):
"""
Returns the display name.
If full name is present then return full name as display name
else return username.
"""
if self.full_name != '':
return self.full_name
else:
return self.username
Let’s look at the User model, I inherits AbstractBaseUser and PermissionMixin and contains required fields, one of which is phone_number_verified
to identify if user has verified his/her phone number or not. two_factor_auth
for optional two-factor authentication feature.
USERNAME_FIELD
defines a string describing the name of the field on the user model that is used as the unique identifier, here ‘username’. REQUIRED_FIELDS
contains fields mandatory for user object creation.
Sub class Meta
contains attributes like odering which sets order of fields in user creation forms.. verbose_name of user and it plurals etc. Similarly get_short_name
returns the display name of user.
I didn’t talk about UserManager
let’s see it.
accounts/manager.py:
from django.contrib.auth.models import UserManager
class UserManager(UserManager):
use_in_migrations = True
def _create_user(self, phone_number, password, **extra_fields):
if not phone_number:
raise ValueError('The given phone number must be set')
user = self.model(phone_number=phone_number, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, phone_number, password=None, **extra_fields):
extra_fields.setdefault('is_superuser', False)
return self._create_user(phone_number, password, **extra_fields)
def create_superuser(self, phone_number, password, **extra_fields):
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_staff', True)
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(phone_number, password, **extra_fields)
Our UserManager
manages creation of our custom User
model objects overwriting default methods of create_user
and create_superuser
according to our custom User
model, normal users and superusers. It’s quite straightforward so I’m not going in-depth of it.
Cool.. so we half done. Let’s look at the other half of this project.
Authy Phone Verification API
The Authy Phone Verification APIallows you to verify that the user has the device in their possession. The Authy Phone Verification API lets you request a verification code to be sent to the user and also verify that the code received by the user is valid. The REST API is designed to use HTTP response codes to indicate status. The body of the response will also include more information.
Creating a AUTHY account and then an application on authy dashboard gives a AUTHY_KEY
which we’ll be using in our API calls.
So let’s get started and see how I used this cool API for my project:
So my website’s flow:
So, we need a total of three forms:
RegisterForm
PhoneVerificationForm
LoginForm
All three can be found here accounts/forms.py
We need four views:
RegisterView
PhoneVerificationView
DashboardView
LoginView
All four can be found here accounts/views.py
Let’s take a look at Authy API functions:
from django.conf import settings
import requests
def send_verfication_code(user):
data = {
'api_key': settings.AUTHY_KEY,
'via': 'sms',
'country_code': user.country_code,
'phone_number': user.phone_number,
}
url = 'https://api.authy.com/protected/json/phones/verification/start'
response = requests.post(url,data=data)
return response
def verify_sent_code(one_time_password, user):
data= {
'api_key': settings.AUTHY_KEY,
'country_code': user.country_code,
'phone_number': user.phone_number,
'verification_code': one_time_password,
}
url = 'https://api.authy.com/protected/json/phones/verification/check'
response = requests.get(url,data=data)
return response
We have two API functions:
Registration and verification
Take a look at RegisterView
and PhoneVerificationView
class RegisterView(SuccessMessageMixin, FormView):
template_name = 'register.html'
form_class = RegisterForm
success_message = "One-Time password sent to your registered mobile number.\
The verification code is valid for 10 minutes."
def form_valid(self, form):
user = form.save()
username = self.request.POST['username']
password = self.request.POST['password1']
user = authenticate(username=username, password=password)
try:
response = send_verfication_code(user)
except Exception as e:
messages.add_message(self.request, messages.ERROR,
'verification code not sent. \n'
'Please re-register.')
return redirect('/register')
data = json.loads(response.text)
print(response.status_code, response.reason)
print(response.text)
print(data['success'])
if data['success'] == False:
messages.add_message(self.request, messages.ERROR,
data['message'])
return redirect('/register')
else:
kwargs = {'user': user}
self.request.method = 'GET'
return PhoneVerificationView(self.request, **kwargs)
Once a valid form is submitted, user is saved and send_verification_code
function is called, it makes a request to AUTHY API to send a OTP to user’s phone number and passes the request together with user
to PhoneVerificationView
the user is served verification page which contains PhoneVerficationForm
.
Let’s take a look at PhoneVerificationView
:
def PhoneVerificationView(request, **kwargs):
template_name = 'phone_confirm.html'
if request.method == "POST":
username = request.POST['username']
user = User.objects.get(username=username)
form = PhoneVerificationForm(request.POST)
if form.is_valid():
verification_code = request.POST['one_time_password']
response = verify_sent_code(verification_code, user)
print(response.text)
data = json.loads(response.text)
if data['success'] == True:
login(request, user)
if user.phone_number_verified is False:
user.phone_number_verified = True
user.save()
return redirect('/dashboard')
else:
messages.add_message(request, messages.ERROR,
data['message'])
return render(request, template_name, {'user':user})
else:
context = {
'user': user,
'form': form,
}
return render(request, template_name, context)
elif request.method == "GET":
try:
user = kwargs['user']
return render(request, template_name, {'user': user})
except:
return HttpResponse("Not Allowed")
After user submits OTP verify_sent_code
function is called which requests AUTHY API to verify the user. If response contains success
= True
then it implies that the user has entered the correct OTP and user’s phone_number_verified
field is set True
. User is redirected to dashboard else user is reserved same form to enter the correct otp.
Now User has verified his/her phone number and hence user’s account is verified.
Two factor authentication
On dashboard user has option to enable two-factor authentication (two_factor_auth
= True) which if enabled would modify the login process for user by adding a new layer of protection, i.e., the user will not only have to enter the username and password but after that verification code as a second step to login successfully.
Login Process
LoginForm
has login
method for logging in users and clean
method for raising errors of invalid credentials.
class LoginForm(forms.Form):
username = forms.CharField()
password = forms.CharField()
class Meta:
fields = ['username','password']
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
user = authenticate(username=username, password=password)
if not user:
raise forms.ValidationError("Sorry, that login was invalid. Please try again.")
return self.cleaned_data
def login(self, request):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
user = authenticate(username=username, password=password)
return user
Let’s look at LoginView
class LoginView(FormView):
template_name = 'login.html'
form_class = LoginForm
success_url = '/dashboard'
def dispatch(self, request, *args, **kwargs):
if self.request.user.is_authenticated:
messages.add_message(self.request, messages.INFO,
"User already logged in")
return redirect('/dashboard')
else:
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
user = form.login(self.request)
print(user.two_factor_auth)
if user.two_factor_auth is False:
login(self.request, user)
return redirect('/dashboard')
else:
try:
response = send_verfication_code(user)
pass
except Exception as e:
messages.add_message(self.request, messages.ERROR,
'verification code not sent. \n'
'Please retry logging in.')
return redirect('/login')
data = json.loads(response.text)
if data['success'] == False:
messages.add_message(self.request, messages.ERROR,
data['message'])
return redirect('/login')
print(response.status_code, response.reason)
print(response.text)
if data['success'] == True:
self.request.method = "GET"
print(self.request.method)
kwargs = {'user':user}
return PhoneVerificationView(self.request, **kwargs)
else:
messages.add_message(self.request, messages.ERROR,
data['message'])
return redirect('/login')
dispatch
method makes sure already logged in user should be redirected to dashboard. If user has entered correct credentials then it checks the two_factor_auth
field of user. If user has not enabled two factor authentication then user is simply logged in and redirected to dashboard else request is made to Authy Phone Verification API to send an OTP to user and the request passes to PhoneVerificationView
and just like registration process user is logged in if he/she enters correct OTP.
Happy Ending! 🙂
Thank you so much sir, I really took the challenge even though I am just starting to work in django frame work it took me a lot of time and finale I succeeded in the tutorial.
I wish you more wisdom
God bless you
warm regards
Smarty.