Omonbude Emmanuel
Backend Engineer
Fullstack Web Developer
Mobile Application Developer
  • Residence:
    Nigeria
  • Phone:
    +2349032841265
  • Email:
    budescode@gmail.com
HTML/CSS
JAVASCRIPT
PYTHON
NODE JS
DJANGO/DJANGO REST FRAMEWORK
EXPRESS JS
FLUTTER
DART
ANGULAR
TYPESCRIPT

Implementing Two-Factor Authentication with Google Authenticator in Django

Omonbude Emmanuel | Feb. 21, 2024, 3:14 p.m.

Introduction

 

Two-factor authentication adds an extra layer of security to your Django application by requiring users to provide two forms of identification before granting access.

In this tutorial, we'll focus on using Google Authenticator as the second factor in our two-factor authentication setup. Google Authenticator is a popular choice for 2FA because it's easy to use, supports multiple accounts, and generates time-based one-time passwords (TOTPs) that are valid for a short period of time.
By the end of this tutorial, you'll have a solid understanding of how to implement two-factor authentication with Google Authenticator in your Django application, helping to keep your users' accounts safe and secure.

Prerequisites

 

  • Basic understanding of Python and Django: You should be comfortable with the Python programming language and have a basic understanding of Django, including models, views, templates, and URLs.
  • Familiarity with Google Authenticator: It's helpful to have some knowledge of how Google Authenticator works, including how to generate and use time-based one-time passwords (TOTPs).
  • Google Authenticator app: To test the two-factor authentication process, you'll need to install the Google Authenticator app on your mobile device or use a similar TOTP-based authentication app. You can download it from apple store or playstore.
  • Python and django

 

LETS BEGIN!!

CREATING THE DJANGO PROJECT

 

We start by creating a new django project that we'd be using for this tutorial

django-admin startproject authenticator

 

INSTALLING THE REQUIREMENTS

We need the django-otp package to be able to genetare TOTP and verify as well for the project.

pip install django-otp

 

 

CREATE DJANGO APP

After the installation, we can proceed to create the user app

python3 manage.py startapp user

 

User Registration

At this point, we need to create our registration and login page so that users can be able to register and login in the project.

 

Create Custom User in user/models.py 
Paste the code below

from django.db import models
from django.contrib.auth.models import AbstractUser
from django_otp.plugins.otp_totp.models import TOTPDevice

class CustomUser(AbstractUser):
    fullname = models.CharField(max_length=50, default='')
    email = models.EmailField(unique=True)
    authenticator_secret = models.TextField(blank=True, null=True)
    def __str__(self):
        return self.email

 

forms.py

Create forms.py file in user folder and paste the following code
 

 

from django import forms
from django.core.exceptions import ValidationError
from .models import CustomUser
from django.contrib.auth import authenticate

class RegistrationForm(forms.ModelForm):
    password = forms.CharField(widget=forms.PasswordInput)
    confirm_password = forms.CharField(widget=forms.PasswordInput)

    class Meta:
        model = CustomUser
        fields = ['fullname', 'email', 'username', 'password']

    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        confirm_password = cleaned_data.get('confirm_password')

        if password != confirm_password:
            raise ValidationError('Passwords do not match')

        return cleaned_data

 


The RegistrationForm is used to register our users and validate their details as well.

views.py

paste the code below in the views.py
 

from django.contrib.auth import authenticate, login, logout, get_user_model
from django.contrib import messages
from user.forms import LoginForm, RegistrationForm
 

def homeView(request):
    return render(request, 'index.html')

def signupView(request):
    form = RegistrationForm() 
    if request.method == 'POST':
        form = RegistrationForm(request.POST)
        password = request.POST['password']
        if form.is_valid():
            user = form.save()  
            user.set_password(password)  
            user.save()     
            messages.success(request, 'Registration successful')            
            return redirect('userurl:index')
    return render(request, 'signup.html', {'form':form})

We have a function based view to display our home page and signup page. The function also handles user registration as well.

Adding Templates

Create a folder called templates in the project folder (authenticator/templates)
Create two files named message.html, index.html and signup.html
paste the code below to signup.html

 

 

<div>
    <h3 >Register An Account</h3>
    <form  action="{% url 'userurl:signup' %}" method="post"> {% csrf_token %}
        {{ form.as_p }}
                                            
        {% include 'messages.html' %}

        <button type="submit">Signup</button>
        
        <p >Already registered? <a href="">Login here</a></p>
    </form>
</div>

 

Paste the code below to index.html

 

{% if request.user.is_authenticated %}
<p>Hello, {{ request.user.username }}</p>
<a href=""><button>Logout</button></a>

{% endif %}

{% include 'messages.html' %}

<ul>
    <li><a href="">Login</a></li>
    <li><a href="{% url 'userurl:signup' %}">Register</a></li>
</ul>
 

 

Finally, paste the code below to message.html

 

{% if messages %}
{% for message in messages %}
<div  style="color: {% if message.tags == 'error' %}red {% else %} green{% endif %}">
    <strong>
        {{ message }}
    </strong>
</div>
{% endfor %}
{% endif %}

 

message.html is used to display message if we have any django messages. It will be included in other files

 

urls.py

Create a url.py file in user folder and paste the code below

 

from django.urls import path
from . import views
from .views import *
app_name='userurl' 
urlpatterns = [
    
    path('', views.homeView, name='index'),
    path('signup/', views.signupView, name='signup')

]

 

Update Project url

Go to the project urls.py file (authenticator/urls.py) and paste the code below

 

 

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('user.urls', namespace='user')),
]

 

Update the settings.py

Paste the following under installed apps

 

    'user',
    'django_otp',
    'django_otp.plugins.otp_totp',

 

We have to add our user app to settings.py and add out django_otp plugin too

Update settings.py

add the code below in the settings.py 

AUTH_USER_MODEL = 'user.CustomUser'


When you set AUTH_USER_MODEL = 'user.CustomUser', you're telling Django to use a custom user model called CustomUser that is defined in the user app of your project.
Django will use this model instead of the built-in User model for authentication and user management.

 

Update the templates section and add the code below

'DIRS': [os.path.join(BASE_DIR, 'templates')],

 

Make Migrations

Run the migration commands to makemigration and migrate our tables to our database

 

python3 manage.py makemigrations user

python3 manage.py migrate

 

Runserver

Run the server command to start up our development server

 

python3 manage.py runserver

 

Open your browser and paste the local server url http://127.0.0.1:8000

Register a user and make sure its successful.

 

Adding Google Authenticator on sign in
 

Upon user sign-in, our system will automatically generate a unique QR code, enabling the user to seamlessly register their account on the Authenticator app. This QR code serves as a secure means of verifying the login process, enhancing overall account security.

 

edit the user/models.py and add the code below

 

from django_otp.plugins.otp_totp.models import TOTPDevice #import form the top of your code

class EmailTOTPDevice(TOTPDevice, models.Model):
    email = models.EmailField(unique=True)   
    def __str__(self):
        return self.email
    

 

Create a folder called helpers and under this folder, create a file called generate_verify_otp.py

Paste the following code under this file

 

 

import re
from user.models import EmailTOTPDevice
from django.contrib.auth import get_user_model
User = get_user_model()


def generate_totp_qr_code(email):
    user = User.objects.get(email__iexact=email)
    try:
        totp_device = EmailTOTPDevice.objects.get(email=user.email, user=user)
    except EmailTOTPDevice.DoesNotExist:
        totp_device = EmailTOTPDevice.objects.create(email=user.email, tolerance=0, user=user)
        #tolerance is set to 0 because we do not want to accept codes that have passed 30 seconds 
    name = f'Django Auth: {email}'
    modified_otp_uri = re.sub(r'otpauth://totp/[^?]+', f'otpauth://totp/{name}', totp_device.config_url)
    extract_secret(modified_otp_uri)
    return modified_otp_uri

def verify_otp(email, code):
    user = User.objects.get(email__iexact=email)
    totp_device = EmailTOTPDevice.objects.get(email=user.email, user=user)
    return  totp_device.verify_token(code)

def extract_secret(uri):
    secret_match = re.search(r"secret=(.*?)(&|$)", uri)
    secret = secret_match.group(1)
    return secret

 

 

1. generate_totp_qr_code(email):

  • This function takes an email address as input and generates a QR code for setting up 2FA for that user.
  • It first retrieves the user object using the email address.
  • It then tries to get an existing EmailTOTPDevice object associated with the user. If it doesn't exist, it creates a new one with tolerance set to 0 (meaning only valid within 30 seconds).
  • It constructs a name for the QR code entry and modifies the original configuration URL to include this name.
  • The extract_secret function (not shown) extracts the secret key from the modified URL.
  • Finally, the modified URL is returned, which can be used to generate a QR code for the user to scan and set up 2FA.

2. verify_otp(email, code):

  • This function takes an email address and an OTP code as input and verifies if the code is valid for that user.
  • It retrieves the user object and the corresponding EmailTOTPDevice object.
  • It then calls the verify_token method on the device object, passing the provided OTP code.
  • The verify_token method checks if the code is valid for the device and within the allowed time window.
  • The function returns True if the code is valid, False otherwise.

3. extract_secret(uri):

  • This function, extracts the secret key from a provided URI string.
  • It uses regular expressions to find the part of the URI containing the secret key.
  • It extracts the secret key and returns it.

 

 

Add the code below to forms.py

 

class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput)
    def clean(self):
        cleaned_data = super().clean()
        username = cleaned_data.get('username')
        password = cleaned_data.get('password')

        if username and password:
            user = authenticate(username=username, password=password)
            if not user or not user.is_active:
                raise forms.ValidationError('Invalid username or password.')
            return cleaned_data


This form is used to validate and login our user.

 

We have to update our views.py now so that our users can be able to login and verify TOTP
Paste the following code in your views.py

 

from django.shortcuts import redirect, render
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.contrib import messages
from helpers.generate_otp import extract_secret, generate_totp_qr_code, verify_otp
from user.forms import LoginForm, RegistrationForm
#imports should be at the top of your code

 


def loginView(request):    
    form = LoginForm()
    if request.method == 'POST':
        form = LoginForm(request.POST)
        if form.is_valid():
            username = form.cleaned_data['username']
            password = form.cleaned_data['password']
            print(username, password, "printed..")
            user = authenticate(username=username, password=password)

            if user is not None:
                secret = ''
                secret_stored = False 
                #used to know if the secret has been stored before or not so that we can display the qr code in frontend or not
                if user.authenticator_secret==None or user.authenticator_secret == '':                
                    qs = generate_totp_qr_code(user.email)
                    secret = extract_secret(qs)
                    
                else:
                    secret_stored = True               
            
                return render(request, 'verify.html', {'qs':qs, 'email':user.email, 'secret_stored':secret_stored, 'secret':secret})

    return render(request, 'signin.html', {'form':form})

def verifyOtp(request):
    if request.method == 'POST':
        otpcode = request.POST.get('otp')
        email = request.POST.get('email')        
        verify  = verify_otp(email, otpcode)
        if verify == False:
            messages.error(request, 'Invalid Code')
            return redirect('userurl:login')                
        user = User.objects.get(email__iexact=email)
        login(request, user)   
        if user.authenticator_secret==None or user.authenticator_secret == '':                
            qs = generate_totp_qr_code(user.email)
            secret = extract_secret(qs)
            user.authenticator_secret = secret 
            user.save()
        messages.success(request, 'Authentication Successful')     
        return redirect('userurl:index')        
    return redirect('userurl:login')        
    
    
def logoutUser(request):
    logout(request)
    return redirect('/login/')

 


We proceed to update our urls.py
Add the following codes to your urls.py

 

    path('login/', views.loginView, name='login'),
    path('verify-otp/', views.verifyOtp, name='verify_otp'),
    path('logout/', views.logoutUser, name='logout'),

 

Create signin.html and verify.html files in the template folder to display the signin and verify page
Paste the following code inside the signin.html
 

<div >
    <h3 >Login Your Account</h3>
    <form class="form-signup" action="{% url 'userurl:login' %}" method="post"> {% csrf_token %}
        
        {{ form.as_p }}
        <button type="submit" >Log In</button>
        {% include 'messages.html' %}
        
        <p >Don't have an account?  <a href="{% url 'userurl:signup' %}">Signup here</a></p>
    </form>
</div>

 

 Paste the code below to verify.html

 

 

<div>
        <h3>Verify OTP</h3>
        <p>Verify your login with google authenticator</p>
        <form method="post" action="{% url 'userurl:verify_otp' %}"> {% csrf_token %}
            <div>                    
                    {% if secret_stored == False %}
                    <div id="qrcode" style="width: 250px; height:250px;"></div>
                    <div>
                        <p>Key: {{secret}}</p>
                    </div>
                    
                    {% endif %}
                    <br>
                    <input type="hidden" name="email" value="{{email}}">
                    <input name="otp" type="text" placeholder="OTP" class="form-control required name" required>
            </div>
        
            <button type="submit" >Submit</button>
            {% include 'messages.html' %}
            
        </form>
</div>
                                
<script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>


<script>
    var qrCodeData = "{{ qs }}"; 
    var qrCodeOptions = {
        width: 200,
        height: 200,        
        colorLight: "#ffffff",
        correctLevel: QRCode.CorrectLevel.H,
    };
    var qrCode = new QRCode(document.getElementById("qrcode"), qrCodeData, qrCodeOptions);
</script>

 

Update the urls in the index.html page to our app urls

go to your browser and paste http://127.0.0.1:8000/login/ 

Proceed to login, and verify with the authenticator app

 

 

CONCLUSION
 

In conclusion, implementing 2FA with Google Authenticator in your Django application is a wise investment in user security. By following the guidelines outlined in this article and tailoring them to your specific needs, you can provide a strong authentication mechanism that fosters trust and protects user data. Remember, security is an ongoing process, and staying informed about updates and best practices is key to maintaining a secure environment for your users.

© 2023 Omonbude Emmanuel

Omonbude Emmanuel