Django Magic Link Authentication

Django Magic Link Authentication

Table Of Contents

Introduction

Magic link authentication is where the user is sent an email with a secure link. Once the user clicks the link, the application logs the user in. Magic link is a smart way to authenticate users because it takes away the need to set and remember a strong password.

An important thing to take care of is that anyone can use the link to authenticate. So make sure you don’t accidentally expose the link to others.

Now let’s see how we can set up magic link authentication in a Django app by leveraging the Django authentication system.

Read this

This is just an experiment to show how magic links work. Check out django-sesame for production.

Basic Setup

# prepare the env
virtualenv .venv
source .venv/bin/activate

# install Django
pip install django
django-admin startproject config .

Create a new app to manage our authentication and add it to INSTALLED_APPS.

python manage.py startapp accounts

Install and add Redis as a cache backend. We can later use Redis for storing our magic link tokens.

pip install django-redis

Add the following to settings.py

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
    }
}

SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

LOGIN_REDIRECT_URL = "dashboard"
LOGOUT_REDIRECT_URL = "home"
LOGIN_URL = "home"

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Here we set up Redis to be our cache backend as well as for session cache. LOGIN_REDIRECT_URL and LOGOUT_REDIRECT_URL tell our application to redirect users after login and logout, respectively. LOGIN_URL is where our application would redirect our user if the user goes to a protected route without logging in. Finally, EMAIL_BACKEND is set to the console.

The Views

Let’s add some basic views to accounts/views.py

from django.shortcuts import render
from django.http.request import HttpRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_GET, require_http_methods


@require_http_methods(["GET", "POST"])
def home(request: HttpRequest):
    return render(request, "magic.html")


@require_GET
@login_required
def dashboard(request: HttpRequest):
    return render(request, "dashboard.html")

Here the home view handles the login form as well as sending the email. We will implement this feature in a later section.

The Templates

Create the corresponding templates in the templates folder in the root directory. Make sure you set the templates directory in the settings.py file as well.

<!-- templates/base.html -->

<html>
	<head>
		<title></title>
	</head>
	<body>
		{% block content %}{% endblock content %}
	</body>
</html>
<!-- templates/magic.html -->

{% extends 'base.html' %} {% block content %}
<h1>Login</h1>
<br />
<form method="post">
	{% csrf_token %}
	<input type="email" name="email" required />
	<input type="submit" value="Send Login Instructions" />
</form>
{% endblock content %}
<! -- templates/dashboard.html -->

{% extends 'base.html' %} {% block content %}
<h1>Dashboard</h1>
<br />
<h3>Hello {{user.email}}</h3>
{% endblock content %}

Here are the steps to send the magic link

  • Get an email address from the user
  • Generate a token for the user
  • Set the (token, email) pair on Redis for 10 minutes(10 * 60)
  • Send the email with the link http://localhost:8000/magic-link/{token}

Update the home view to send the magic link

from .forms import MagicLinkForm
from django.core.mail import send_mail
from django.core.cache import cache
import secrets


@require_http_methods(["GET", "POST"])
def home(request: HttpRequest):
    if request.POST:
        form = MagicLinkForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data["email"]
            token = secrets.token_urlsafe(nbytes=32)
            link = f"http://localhost:8000/magic-link/{token}"
            cache.set(token, email, timeout=10 * 60)
            send_mail(
                subject="Magic Link",
                message=f"You link: {link}",
                from_email="[email protected]",
                recipient_list=[email],
                fail_silently=True,
            )
    return render(request, "magic.html")

The MagicLinkForm comes from accounts/forms.py

# accounts/forms.py

from django import forms


class MagicLinkForm(forms.Form):
    email = forms.EmailField()

Navigate to http://localhost:8000 and use a random email to submit the form. Check your console; you should’ve received an email.

Email sent to console

Verify The Token

Now that the magic link is set up, we need to verify the token and authenticate the user.

  • Get token from the request
  • Check if the token exists
  • Create a user if not exist
  • Login user
# accounts/views.py

from django.http.response import HttpResponseBadRequest
from django.contrib.auth.models import User
from django.contrib.auth import login

@require_GET
def autheticate_via_magic_link(request: HttpRequest, token: str):
    email = cache.get(token)
    if email is None:
        return HttpResponseBadRequest(content="Magic Link invalid/expired")
    cache.delete(token)
    user, _ = User.objects.get_or_create(email=email)
    login(request, user)
    return redirect("/dashboard")

Add the URL for the view to accounts/urls.py

# accounts/urls.py

urlpatterns = [
    path("", home, name="home"),
    path("dashboard", dashboard, name="dashboard"),
    path("magic-link/<str:token>", autheticate_via_magic_link, name="magic_link"),
]

Check the new URL using a random token or an expired one.

Wrong Token

Demo

Django Magic Link Demo

The objective of this experiment is to demonstrate how magic links work. However, for your production Django application, I would recommend not using the same approach. This is because the production environment requires a lot of security, and django-sesame is recommended.