Implementing Two-Factor Authentication with Google Authenticator in Django
644
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.