# Understand how Google Authenticator works by building a Django App

Have you ever used apps like Google Authenticator and Microsoft authenticator for two-factor authentication? These applications provide an easy-to-use and safe method for authenticating your account across many different websites. You may want to integrate this authentication technique into your application as well. In this article, we will understand the authentication workflow by building a Django app.

## Understand the workflow

Generally, to activate and use these authenticator applications one must go through the following steps:

1. Enable 2FA on your account.
    
2. Scan the provided QR code using Google/Microsoft Authenticator (or manually enter the token provided) and input the generated TOTP in the website for activation.
    
3. Each time you log in, the website will ask you to enter an OTP.
    
4. The authenticator apps generate a time-based OTP that can be used for authentication.
    

The time-sensitive OTPs generated are formally known as ***TOTP (Time-based One Time Password).*** These TOTPs are generated using an algorithm called the HOTP algorithm. Since these OTPs are time-sensitive, the UNIX time system (seconds since January 1, 1970) is used to synchronize the time between the TOTP generators and validators. This will make more sense as we move forward and create the application. If you want to learn more about the algorithm and all the moving parts involved, I would highly recommend that you go through the official [IETF RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) standard.

> “For the things we have to learn before we can do them, we learn by doing them” - Aristotle

To better understand the authentication workflow, let's begin developing the Django app. The application you will build using this article can be found in my [GitHub repository here](https://github.com/mukulboro/django-authenticator). If you like this article, be sure to star the repository!

<details data-node-type="hn-details-summary"><summary>Disclaimer</summary><div data-type="detailsContent">All code snippets in the upcoming tutorial assume that you are running a Linux-based system with Python and the pip package manager installed. The tutorial also assumes that you have a basic understanding of how Django projects are structured and does not go into much detail. If you have any issues understanding the Django lingo used in the tutorial, refer to the official <a target="_blank" rel="noopener noreferrer nofollow" href="https://docs.djangoproject.com/en/5.1/intro/tutorial01/" style="pointer-events: none">"Writing your first Django app" documentation.</a></div></details>

## Building the Django Server

### Initial Setup

First things first, create a new directory at the location of your choice and `cd` into the directory. Then, create a new Python virtual environment using the command `python3 -m venv .venv`. Activate the virtual environment using `source .venv/bin/activate`.

Now install the required packages using `pip3 install Django qrcode`.

* The `Django` package will provide all methods to develop our web server.
    
* The `qrcode` package will be used to generate a QR code to be scanned by the authenticator applications.
    

Then, create a new Django project with whatever name you want (I am choosing the name `django-authenticator`) with the command `django-admin startproject django-authenticator`. Then, cd into the new directory `django-authenticator`.

Now we are going to create the Django app. I am creating an app called `auther` using `python3 manage.py startapp auther`. You then need to add the new app to the `INSTALLED_APPS` list in `settings.py`. Make sure to create the `urls.py` file in the new app and include those URLs in the `urls.py` file of the main project application.

### Extending the User model

We will be using the default Django `User` model to make the registration and login process as easy as possible. But, for our project, we need to add an extra `has_2fa` boolean field in the model. This field will let us know if the user has enabled 2FA in their account or not. In the `models.py` file of our `auther` app, add the following code:

```python
from django.db import models from django.contrib.auth.models import AbstractUser

class ExtendedUser(AbstractUser): 
    has_2fa = models.BooleanField(default=False)
```

* Create a new model with a name `ExtendedUser` that inherits the attributes and method of the base `AbstractUser` model.
    
* Add a boolean field `has_2fa` and make the default value `False` as new users will not have 2FA enabled by default.
    

Also, you need to override the default user model by adding the following line in `settings.py`: `AUTH_USER_MODEL = 'auther.ExtendedUser'`

Finally, to make the new user model appear on the Django admin site, add the following code to the `admin.py` file of the `auther` app:

```python
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import ExtendedUser

class ExtendedUserAdmin(UserAdmin):
    # Since we overrode the default User model, including new model in admin panel
    fieldsets = UserAdmin.fieldsets + (
        (None, {'fields': ('has_2fa',)}), 
    )

    add_fieldsets = UserAdmin.add_fieldsets + (
        (None, {'fields': ('has_2fa',)}), 
    )

admin.site.register(ExtendedUser, ExtendedUserAdmin)
```

### Creating the forms

We will create some Django forms to accept and validate the different responses received by our web server. Since this is a simple web server, we shall only be using 4 different forms:

* **Registration Form**: The default Django form for user registration, but using our custom `ExtendedUser` model.
    
* **Login Form**: A form to input the username and password of the user for login.
    
* **Enable 2FA Form**: Used to validate the TOTP provided by the user and enable 2FA (if this is confusing, this will make sense later).
    
* **Verify TOTP Form**: This form is used to input the TOTP provided by a user during login if they have 2FA enabled.
    

To create the forms, first create a `forms.py` file in the `auther` app and add the following code in the file:

```python
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import ExtendedUser

class RegisterForm(UserCreationForm): # Use default django user registration form
    email = forms.EmailField()

    class Meta:
        model = ExtendedUser
        fields = ["username", "email", "password1", "password2"]


class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput())

class Enable2FAForm(forms.Form):
    code = forms.IntegerField()

class VerifyTOTPForm(forms.Form):
    """
    Although this form is similar to "Enable2FA" form, it is coded as a seperate form,
    incase future upgrades are required (e.g. if we need to include CAPTCHA)
    """
    code = forms.IntegerField()
```

### Generate token and QR Code

As previously mentioned in the blog post, the authenticator apps usually scan a QR code or you have to manually enter a secret token associated with the user. So, what is in the QR code? The QR code contains a special identifier associated with the user that is Base32 encoded. Base32 encoding is chosen for this purpose as it only contains uppercase letters A-Z and numbers 2-7. This reduces the human error if the token is to be entered manually.

Usually, a unique and unpredictable string is stored in a separate databse and is associated with a user. This unique string is then used to generate the token for maximum security. In our case, since we are creating a simple application, we are using the user's email address to generate the base32 encoded token. Other than the token, an identifier, the issuer, the algorithm (which is SHA1 according to the standard), the number of digits of TOTP (usually 6), and finally the valid time period of TOTP (mostly 30 seconds) is stored in the QR code.

In the `auther` app folder, create a `utils.py` folder and define a function `generate_qr` inside.

```python
import base64
import qrcode
import urllib.parse
from io import BytesIO

TIME_PERIOD_SECONDS = 30 # 30s default in Google Authenticator

def generate_qr(email:str):
    key = email.encode("utf-8") # The user's email is the key for the hash
    token = base64.b32encode(key)
    token = token.decode("utf-8") # Make token human readable
    safe_email = urllib.parse.quote(email) # Making email URL safe
    qr_string = f"otpauth://totp/{safe_email}?secret={token}&issuer=Mukul&algorithm=SHA1&digits=6&&period={TIME_PERIOD_SECONDS}"
    # ^ Creating URL with proper format to be recognized by authenticator apps
    qr_code = qrcode.make(qr_string)
    stream = BytesIO()
    qr_code.save(stream)
    return base64.b64encode(stream.getvalue()).decode('utf-8'), token
    # ^ Image is converted to base64 so it can be directly rendered in frontend
```

* The function takes in the user's email and returns a base64 encoded QR code image along with a base32 encoded token.
    
* The QR code is converted to base64 so that it can be easily integrated into our webpage template.
    
* Since we are using an email address as the identifier, it is important to make it URL-safe before including it in our QR code.
    

### Generate TOTP

While authenticator apps handle the generation of TOTPs on their end, we also also need to generate TOTPs on the backend to verify and cross-check the ones provided by the users. In order to do that, we again need the user's email address (since that is what we used to generate the token) along with the current UTC time to generate a valid TOTP. Do note that if the time on your system is invalid (i.e. not synchronized with the international time), the validity of the TOTP generated on your end cannot be verified.

We create a new function `generate_totp` inside the `utils.py` as follows:

```python
import hmac
import hashlib
import math
import time

def generate_totp(email:str):
    # Following standards to generate OTP
    key = email.encode("utf-8")
    t = math.floor(time.time() // TIME_PERIOD_SECONDS)
    hmac_object = hmac.new(key, t.to_bytes(length=8, byteorder="big"), hashlib.sha1)
    hmac_sha1 = hmac_object.hexdigest()
    offset = int(hmac_sha1[-1], 16)
    binary = int(hmac_sha1[(offset * 2):((offset * 2) + 8)], 16) & 0x7fffffff
    # ^ Binary Magic 🧙
    totp = str(binary)[-6:]
    return int(totp)
```

You can generate a custom QR code with whatever dummy e-mail address you want and then scan it using any authenticator app (I use Google Authenticator). Then, you may manually generate a TOTP on your server and verify it against the one displayed on the app.

### Creating Jinja2 Templates

To allow the users to interact with our Django server, we have to create certain web pages. While this tutorial does not go through all the code required for the templates, you can view all of them [here](https://github.com/mukulboro/django-authenticator/tree/main/auther/templates). This simple project has a total of 7 Jinja2 templates:

* **base.html**: The base template that contains all boilerplate required by all other templates.
    

```xml
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
    <title>{% block title %}{% endblock %}</title>
</head>

<body>
    <div class="mainbody">
        {% block content %}
        {% endblock %}
    </div>
</body>
</html>
```

* **register.html**: A registration form for new users.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1724683850135/a772fc58-ba20-4123-8eca-c0c0b41d5ce1.png align="center")

* **login.html:** Login page for existing users.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1724683887647/d1a40326-6df0-4346-9035-d693339daee3.png align="center")

* **verify.html**: Separate page to accept code for 2FA after logging in.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1724684085093/4e128e2d-b8c8-4d53-9877-c4e99df693b4.png align="center")

* **dashboard.html**: Page seen by users after logging in. Shows if 2FA is enabled and allows users to enable it if not.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1724683972023/fdb62635-50cb-412d-b64b-3adf8e0c72fc.png align="center")

* **enable\_2fa.html**: If users want to enable 2FA, they are sent to this page. The page holds the QR code to be scanned by authenticator apps and input to check whether the TOTP generated by the apps is valid or not.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1724684004029/2e0fd4ea-53f4-48fc-bd13-f7971fdd9eb5.png align="center")

* **error.html**: A generic error page.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1724691324410/3eeb7a69-efbf-457b-bdae-4f0bcbe85cab.png align="center")

### Creating Views

Now, we have to code the main logic behind the different routes in the web server. If you are already familiar with Django, this will be very easy and understandable for you. First inside the `views.py` file of the `auther` app, we import all required libraries, functions, models, and forms.

```python
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from .forms import RegisterForm, LoginForm, Enable2FAForm, VerifyTOTPForm
from .utils import generate_qr, generate_totp
from .models import ExtendedUser
```

Now, we start coding all the required views.

1. `login_page` **view:**
    

* If the request is GET, render the login page.
    
* Otherwise, authenticate the user using the provided credentials. If 2FA is not enabled, redirect to the dashboard else redirect to the TOTP verification route.
    

```python
def login_page(request):
    if request.method == "POST":
        form = LoginForm(request.POST)
        if not form.is_valid():
            return render(request, "login.html", {"form":form})
        uname, pwd = form.cleaned_data["username"], form.cleaned_data["password"]
        user = authenticate(username=uname, password=pwd)
        if user is not None:
            has_2fa = user.has_2fa
            if not has_2fa: # if 2FA is not enabled, directly log the user in
                login(request, user)
                return redirect("/")
            request.session["verify_email"] = user.email # Passing the user email to another route
            return redirect("/verify-login")
        messages.error(request, "Invalid Credentials")
        return render(request, "login.html", {"form":form})
    else:
        form = LoginForm()
        return render(request, "login.html", {"form":form})
```

2. `register_page` **view:**
    

* If the request is GET, render the registration page.
    
* Otherwise, create new user with provided details.
    

```python
def register_page(request):
    if request.method == "POST":
        form = RegisterForm(request.POST)
        if not form.is_valid():
            return render(request, "register.html", {"form":form})
        form.save()
        messages.success(request, f"User {form.cleaned_data["username"]} added")
        return redirect("/login")
    else:
        form = RegisterForm()
        return render(request, "register.html", {"form":form})
```

3. `verify_login_page` **view:**
    

* If the request is GET, render the verify TOTP page.
    
* Otherwise, get the user email from the request session. Then create a TOTP using the provided email. Verify the user-provided TOTP with the one generated on the server.
    
* If the user-provided TOTP is valid, log the user in. Otherwise, throw an error.
    

```python
def verify_login_page(request):
    if request.method == "GET":
        form = VerifyTOTPForm()
        return render(request, "verify.html", {"form":form})
    elif request.method == "POST":
        email = request.session["verify_email"]
        valid_totp = generate_totp(email) # Generate TOTP ASAP to avoid timing conflicts
        form = VerifyTOTPForm(request.POST)
        if not form.is_valid():
            return render(request, "verify.html", {"form":form})
        user_totp = int(form.cleaned_data["code"])

        if valid_totp == user_totp:
            user = ExtendedUser.objects.get(email=email)
            login(request, user)
            return redirect("/")
        else:
            messages.error(request, "Invalid TOTP")
            return render(request, "verify.html", {"form":form})
```

4. `dashboard_page` **view:**
    

* Make sure the user is logged in and then render the dashboard.
    
* Otherwise, redirect to the error page.
    

```python
def dashboard_page(request):
    if request.user.is_authenticated:
        return render(request, "dashboard.html", {"user":request.user})
    else:
        messages.error(request, "You must be logged in to access this page")
        return redirect("/error")
```

5. `enable_2fa_page` **view:**
    

* Make sure the user is logged in and then render the 2FA enable page along with the token and QR code. Otherwise, redirect to the error page (if it is a GET request).
    
* If it is POST request, generate a valid TOTP for the user. Verify the user-provided TOTP with the one generated on the server.
    
* If the user-provided TOTP is valid, enable 2FA for the user account. Otherwise, throw an error.
    

```python
def enable_2fa_page(request):
    if request.user.is_authenticated:
        if request.method == "GET":
            qr, token = generate_qr(request.user.email)
            form = Enable2FAForm()
            return render(request, "enable_2fa.html", {"user":request.user, "qr":qr, "token":token, "form":form})
        elif request.method == "POST":
            valid_totp = generate_totp(request.user.email) # Generating ASAP to reduce time conflicts
            form = Enable2FAForm(request.POST)
            if not form.is_valid():
                return render(request, "enable_2fa.html", {"user":request.user, "qr":qr, "token":token, "form":form})
            user_totp = int(form.cleaned_data["code"])

            if not valid_totp == user_totp:
                qr, token = generate_qr(request.user.email)
                messages.error(request, "Invalid TOTP provided")
                return render(request, "enable_2fa.html", {"user":request.user, "qr":qr, "token":token, "form":form})
            user_model = ExtendedUser.objects.get(pk=request.user.id)
            # ^ Extra DB query since user is not logged in by this point
            user_model.has_2fa = True
            user_model.save() # Update the user model and enable 2FA
            messages.success(request, "Enabled 2FA")
            return redirect("/")

    else:
        messages.error(request, "You must be logged in to access this page")
        return redirect("/error")
```

6. `error_page` **view:**
    

```python
def error_page(request):
    return render(request, "error.html")
```

7. `logout_route` **view:**
    

```python
def logout_route(request):
    logout(request)
    messages.success(request, "Logged Out")
    return redirect("/login")
```

### Set up URLs

Now, assign appropriate URLs to each view inside the `urls.py` file of the `auther` app as follows:

```python
from django.urls import path
from . import views

urlpatterns = [
    path("", views.dashboard_page, name="dashboard"), # Main Dash
    path("login/", views.login_page, name="login"), # Login 
    path("register/", views.register_page, name="regsiter"), # Register
    path("verify-login/", views.verify_login_page, name="verify-login"), # Verify TOTP code
    path("enable-2fa/", views.enable_2fa_page, name="enable-2fa"), # Enable 2FA for users
    path("error/", views.error_page, name="error"), # Generic error page
    path("logout/", views.logout_route, name="logout") # Logout route
]
```

### Run and test the web server

Good job! We are done with all the code. Now it is time to run the webserver and manually check all the routes we have coded in.

Before running the web server for the first time, ensure the following commands are run:

* `python3 manage.py makemigrations`
    
* `python3 manage.py migrate`
    
* `python3 manage.py createsuperuser`
    

Then, you can run the server using `python3 manage.py runserver` and access the server on `localhost:8000`. First, visit the `/register` route and then you may serially go through other pages as necessary.

## Conclusion

Congratulations! You have reached the end of this blog post. I hope that this post was helpful to you. If you hope to read more posts like this, consider following me on Hashnode, and joining the newsletter.

If you have any queries or suggestions, please leave a comment or reach out to me directly by sending an email to mukul.development@gmail.com.
