Ditch the Passwords! Building a Passwordless Web Application in Django

Photo by FLY:D on Unsplash

Ditch the Passwords! Building a Passwordless Web Application in Django

Introduction

Passwordless Authentication is a method of verifying a user without requiring the entry of a password. It offers a more secure and convenient alternative to traditional password-based authentication, which is susceptible to various attacks. Users often resort to reusing passwords across different websites, leading to vulnerabilities and password fatigue—a feeling experienced by individuals managing an excessive number of passwords in their daily routine.

Passwords have long been deemed insecure due to their difficulty to remember, susceptibility to misplacement, and their status as prime targets for cybercriminals. According to Verizon's annual Data Breach Index Report, 81 percent of breaches involve weak or stolen passwords.

Passwordless Authentication typically relies on factors such as:

  1. Possession factors: Verification that the user possesses a specific device or token. Examples include:

    • One-time passwords (OTPs): Temporary passwords sent to the user's phone or email address, expiring after a short time.

    • Security tokens: Physical devices generating unique codes for user authentication.

  2. Biometrics: Verifying the user's identity based on unique physical characteristics like fingerprints, facial features, or voice patterns.

  3. Magic Links: A link sent to the user's email or phone, granting access to the intended application without requiring a password.

Advantages of Passwordless Authentication over traditional methods include:

  1. Improved User Experience: Streamlined and convenient, eliminating the need to remember multiple passwords and adhere to complex rules, reducing password fatigue.

  2. Enhanced Security: Mitigates vulnerabilities associated with traditional passwords, such as credential theft and phishing attacks.

  3. Improved Password Hygiene: Eliminates the need for users to manage multiple passwords, reducing the risks of reuse and fatigue.

  4. Simplified Account Management: Eases user account management by eliminating password resets and recovery hassles, providing centralized authentication systems.

Passwordless Authentication is gaining popularity in applications like online banking, corporate logins, and consumer-facing websites. As technology advances and security concerns grow, it is likely to play a more prominent role in secure user authentication.

In the following article, you will learn how to set up a Passwordless Authentication system in Django.

To follow along, a basic knowledge of Django is recommended.

Also, You can get the source code from this Github Repository

Start a New Django Application

Create a new folder and run the following commands in any terminal to set up a virtual environment named venv and activate it.

python -m venv venv
source venv/Scripts/activate  # on Windows
source venv/bin/activate  # on Linux

Next, install Django, start a new project named passwordless, and create a new app named core.

pip install django
django-admin startproject passwordless .
python manage.py startapp core

Add the newly created app to the INSTALLED_APPS in your project's settings.py file.

INSTALLED_APPS = [
    ...
    'core.apps.CoreConfig',
]

In your app directory, create a new file urls.py

touch core/urls.py

In your project's urls.py file, add a path to the urls.py file of the newly created app, don't forget to import include alongside the path.

# passwordless/urls.py
from django.contrib import admin
from django.urls import path, include

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

Creating Views

Create a view in core.views.py with the name homepage.

# core/views.py
def homepage(request):
    return render(request, 'core/homepage.html', {})

Create the template folder in your app directory, and within it, create core> homepage.html. Input the following HTML code.


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Passwordless</title>
</head>

<body>
    <p>Authentication Status.</p>
    {% if request.user.is_authenticated %}
    <h4>{{request.user}}, <br>user is Authenticated: {{request.user.is_authenticated}}
        <br>{{request.user.email}}
    </h4>
    {% else %}
    <p>User is not Authenticated</p>
    {% endif %}
</body>

</html>

In the core/urls.py, copy and paste the following.

# core/urls.py
from django.urls import path
from . import views

app_name = 'password'

urlpatterns = [
    path('home', views.homepage, name='home'),
]

Start your server ```python manage.py runserver``` , visit the URL http://127.0.0.1:8000/password/home, and you should see something similar to

Home page image

As shown above, the user is currently not authenticated.

In the core/models.py Create a model to track the tokens generated for each user.

# core/models.py
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class UserToken(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=155, unique=True)

    def __str__(self):
        return self.user

Create migration tables.

python manage.py makemigrations && python manage.py migrate

Create a view to handle the generation and sending of one-time login link, and also an utility function to assist in creating and updating UserToken instance.

Your core/views.py file should now contain;

# core/views.py
from django.http import HttpResponseRedirect
from django.shortcuts import render, HttpResponse
from django.contrib.auth.tokens import default_token_generator
from django.urls import reverse
from django.core.mail import send_mail
from django.utils.html import format_html
from django.contrib.auth.models import User
from django.conf import settings
from .models import UserToken

def homepage(request):

    return render(request, 'core/homepage.html', {})


def one_time_login(request):
    if request.method == 'POST':
        mail = request.POST.get('email', '')
        try:
            user = User.objects.get(email=mail)
        except User.DoesNotExist:
            return HttpResponse('Provided mail is not for a valid user')
        token = default_token_generator.make_token(user)
        create_user_token(user, token)
        login_link = request.build_absolute_uri(reverse('password:passwordless_login', args=[token]))
        subject = 'One Time Login Link'
        message = format_html(
            f"Hello {user.username},<br><br>Please click on the following link to login to your account:<br><br><a href='{login_link}'>{login_link}</a><br><br>This link is only valid once.<br><br>Sincerely,<br>The Team")

        send_mail(subject, message, settings.EMAIL_HOST_USER, [user.email], fail_silently=False)
        return HttpResponse(f"One time login sent to {mail}")
    return render(request, 'core/one_time_login.html', {})


def create_user_token(user, token):
    try:
        user_token = UserToken.objects.get(user=user)
    except UserToken.DoesNotExist:
        user_token = UserToken.objects.create(user=user)
    user_token.token=token
    user_token.save()
    return

default_token_generator.make_token(user) function is part of the authentication system and is used to generate a token for a specific user. This token is commonly employed in scenarios like email confirmation, password reset, or one-time login links.

Here's a breakdown of the components involved:

  1. default_token_generator: Django provides a default token generator as part of its authentication framework. This generator uses a combination of the user's primary key, the user's hashed password, the user's last login timestamp, and the current timestamp to create a unique token.

  2. make_token(user): This method takes a user object as an argument and generates a token associated with that user. The token is essentially a string that holds information allowing Django to verify the authenticity of the request associated with that token.

Create another file under template > core > one_time_login.html and paste the following HTML code.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Send Link</title>
</head>

<body>
    <form action="" method="post">
        {% csrf_token %}
        <input type="text" name="email" style="width: 50%; padding: 15px;"><br> <br>
        <input type="submit" value="Send One-Time Link">
    </form>
</body>

</html>

Add a new URL path to the view that would send the one-time link to the user's email in the application's urls.py file.

path('one-time-login', views.one_time_login, name='one_time_login'),

Create a view to authenticate users that clicked on the one-time login link.

# core/views.py
def passwordless_login(request, token):
    try:
        user_token = UserToken.objects.get(token=token)
    except UserToken.DoesNotExist:
        return HttpResponse("invalid request")
    try:
        if token and default_token_generator.check_token(user_token.user, token):
            login(request, user_token.user)
        else:
            return HttpResponse("invalid request")
    except (User.DoesNotExist, BadSignature):
        return HttpResponse("invalid token")
    return HttpResponseRedirect(reverse('password:home'))

default_token_generator.check_token function is used to verify the validity of a token and retrieve the associated user. Let's break down the parameters in default_token_generator.check_token(user_token.user, token):

1. user_token.user: This is typically the user for whom the token was generated. When generating a token, you often associate it with a specific user. The user_token.user parameter is used to provide the user for whom the token was generated.

2. token: This is the token that you want to check and verify. It's the token that was likely generated using default_token_generator.make_token(user).

- If the token is valid and hasn't expired, check_token returns True, else False if token is invalid or has expired.

Your core/views.py should now contain;

# core/views.py
from django.http import HttpResponseRedirect
from django.shortcuts import render, HttpResponse
from django.contrib.auth.tokens import default_token_generator
from django.urls import reverse
from django.core.mail import send_mail
from django.utils.html import format_html
from django.contrib.auth.models import User
from django.conf import settings
from .models import UserToken
from django.core.signing import BadSignature
from django.contrib.auth import login, logout

def homepage(request):

    return render(request, 'core/homepage.html', {})


def one_time_login(request):
    if request.method == 'POST':
        mail = request.POST.get('email', '')
        try:
            user = User.objects.get(email=mail)
        except User.DoesNotExist:
            return HttpResponse('Provided mail is not for a valid user')
        token = default_token_generator.make_token(user)
        create_user_token(user, token)
        login_link = request.build_absolute_uri(reverse('password:passwordless_login', args=[token]))
        subject = 'One Time Login Link'
        message = format_html(
            f"Hello {user.username},<br><br>Please click on the following link to login to your account:<br><br><a href='{login_link}'>{login_link}</a><br><br>This link is only valid once.<br><br>Sincerely,<br>The Team")

        send_mail(subject, message, settings.EMAIL_HOST_USER, [user.email], fail_silently=False)
        return HttpResponse(f"One time login sent to {mail}")
    return render(request, 'core/one_time_login.html', {})


def create_user_token(user, token):
    try:
        user_token = UserToken.objects.get(user=user)
    except UserToken.DoesNotExist:
        user_token = UserToken.objects.create(user=user)
    user_token.token=token
    user_token.save()
    return


def passwordless_login(request, token):
    try:
        user_token = UserToken.objects.get(token=token)
    except UserToken.DoesNotExist:
        return HttpResponse("invalid request")
    try:
        if token and default_token_generator.check_token(user_token.user, token):
            login(request, user_token.user)
        else:
            return HttpResponse("invalid request")
    except (User.DoesNotExist, BadSignature):
        return HttpResponse("invalid token")
    return HttpResponseRedirect(reverse('password:home'))

Create a URL path for the view passwordless_login in the core/urls.py , your core./urls.py should now be;

# core/urls.py
from django.urls import path
from . import views

app_name = 'password'

urlpatterns = [
    path('home', views.homepage, name='home'),
    path('one-time-login', views.one_time_login, name='one_time_login'),
    path('p-login/<token>', views.passwordless_login, name='passwordless_login'),
]

Testing the Setup

Create some users with which you are going to test the one-time-login link.

$ python manage.py shell
In [1]: from django.contrib.auth.models import User
In [2]: User.objects.create_user(username='test', password='test', email='test@gmail.com')
Out[2]: <User: test>

Next, Add the following to your passwordless/settings.py file which simulates the receipt of mail on the console.

# passwordless/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Visit http://127.0.0.1:8000/password/home, and you should see something similar to;

Homepage

To send one-time login link to a mail, visit http://127.0.0.1:8000/password/one-time-login;

Send One time login link

Provide an email of a valid user, then click on the send button.

Check your console; you should have something similar to

One time login link

Copy and paste the link on your browser, and, you should be redirected to the homepage with the User automatically authenticated if the token is still valid.

Homepage Authenticated user

Try and play around with it to see how well it works.

Conclusion

This article has provided a foundation for implementing Passwordless Authentication in Django, combining security and maintaining user's convenience. Utilizing one-time login links, we've showcased a setup that emphasizes a streamlined user experience while enhancing security by eliminating the use of traditional passwords.

As technology advances and security needs increases, Passwordless Authentication is gaining popularity across various applications. This guide equips you with essential steps to tailor the solution to your needs. Keep in mind, this is meant to give you an idea on how to implement Passwordless Authentication experience using Django, and does is not mostly suited for a production environment.

Thank you for reading, and I would see you on the next one.

Would love to discuss your thoughts on this topic! Connect with me on LinkedIn @Lawal Afeez