Codementor Events

Django | User registration with Twillio

Published Sep 03, 2017Last updated Sep 13, 2017

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:
export-e1489590119826.png

So, we need a total of three forms:

  1. RegisterForm
  2. PhoneVerificationForm
  3. LoginForm

All three can be found here accounts/forms.py

We need four views:

  1. RegisterView
  2. PhoneVerificationView
  3. DashboardView
  4. 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

login.png

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! 🙂

Discover and read more posts from Rishabh Agrahari
get started
post commentsBe the first to share your opinion
Smartybrainy
3 years ago

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.

Show more replies