We'll build a sample Django app to demonstrate the simplest way to add email sign-in, aka magic link sign-in, to Django.
I like using email sign in because it's simple to add and avoids needing passwords. The user will:
Let's get started 🐎
pip install django python-dotenv
django-admin startproject core .
python manage.py startapp sim
sim
to INSTALLED_APPS
in core/settings.py
:INSTALLED_APPS = [
... # Other apps
'sim',
]
We'll add a Custom User model to our app from the start, in keeping with good practice.
- In sim/models.py
, create a User model by adding the below code:
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
has_verified_email = models.BooleanField(default=False)
core/settings.py
:AUTH_USER_MODEL = 'sim.User'
python manage.py makemigrations
python manage.py migrate
.env
at core/.env
and add the below to it. We'll use this to load our environment variables, without adding them to version control.EMAIL_VERIFICATION_URL='http://localhost:8000/verify-email'
EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST='smtp.gmail.com'
EMAIL_HOST_USER=<your email address>
EMAIL_HOST_PASSWORD=<your email app password>
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_USE_SSL=False
If you want to use Gmail to send emails, follow these steps to Create & use app passwords.
EMAIL_HOST_PASSWORD
.EMAIL_HOST_USER
.If you're not using Gmail, search for "how to get email service credentials for <>", and you'll likely find a guide for your email service. Almost all email services provide this, unless they're end-to-end encrypted (e.g., ProtonMail).
.env
file with the SMTP email config from your email provider. We'll use these to send emails from your Django app.In core/settings.py
, we will set your email backend to use your environment variables.
We use these environment variables to let us set different email backends in development and production.
core/settings.py
file.import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
EMAIL_BACKEND=os.environ['EMAIL_BACKEND']
EMAIL_HOST=os.environ['EMAIL_HOST']
EMAIL_PORT=os.environ['EMAIL_PORT']
EMAIL_USE_TLS=os.environ['EMAIL_USE_TLS']
EMAIL_HOST_USER=os.environ['EMAIL_HOST_USER']
EMAIL_HOST_PASSWORD=os.environ['EMAIL_HOST_PASSWORD']
Create a file called sim/services.py
, and add the below function to send a verification email:
import os
from typing import Optional
from django.core.mail import send_mail
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from django.conf import settings
from sim.models import User
def send_sign_in_email(user: User) -> None:
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
verification_link = f"{os.environ['EMAIL_VERIFICATION_URL']}/{uid}/{token}/"
subject = 'Verify your email address 🚀'
message = (
'Hi there 🙂\n'
'Please click '
f'<a href="{verification_link}" target="_blank">here</a> '
'to verify your email address'
)
send_mail(subject, '', settings.EMAIL_HOST_USER, [user.email], html_message=message)
def decode_uid(uidb64: str) -> Optional[str]:
"""Decode the base64 encoded UID."""
try:
return urlsafe_base64_decode(uidb64).decode()
except (TypeError, ValueError, OverflowError) as e:
print(f'{e = }')
return None
def get_user_by_uid(uid: str) -> Optional[User]:
"""Retrieve user object using UID."""
try:
return User.objects.get(pk=uid)
except User.DoesNotExist as e:
print(f'{e = }')
return None
In sim/views.py
, create views to handle sign in and email verification.
We'll also add views to handle sign out and a home screen to make it easier to check our email sign in flow.
from django.http import HttpRequest
from django.utils.http import urlsafe_base64_decode
from django.contrib.auth.tokens import default_token_generator
from .models import User
from django.shortcuts import redirect
from django.contrib.auth import login
from .services import send_sign_in_email, decode_uid, get_user_by_uid
from .forms import CreateUserForm
from django.http import HttpResponse
from django.shortcuts import render
from django.views import View
def verify_email(request: HttpRequest, uidb64: str, token: str) -> HttpResponse:
"""
Verify user email after the user clicks on the email link.
"""
uid = decode_uid(uidb64)
user = get_user_by_uid(uid) if uid else None
if user and default_token_generator.check_token(user, token):
user.has_verified_email = True
user.save()
login(request, user)
return redirect('home')
print("Email verification failed")
return redirect('sign_in')
class SendSignInEmail(View):
def get(self, request: HttpRequest) -> HttpResponse:
if not request.user.is_anonymous and request.user.has_verified_email:
return redirect('home')
form = CreateUserForm()
return render(request, 'sign_in.html', {'form': form})
def post(self, request: HttpRequest) -> HttpResponse:
data = {
'username': request.POST['email'],
'email': request.POST['email'],
'password': request.POST['email']
}
user, created = User.objects.get_or_create(
email=data['email'],
defaults={'username': data['email'], 'password': data['email']}
)
return self._send_verification_and_respond(user)
@staticmethod
def _send_verification_and_respond(user: User) -> HttpResponse:
send_sign_in_email(user)
message = (
f"We've sent an email ✉️ to "
f'<a href=mailto:{user.email}" target="_blank">{user.email}</a> '
"Please check your email to verify your account"
)
return HttpResponse(message)
def sign_out(request: HttpRequest) -> HttpResponse:
request.session.flush()
return redirect('sign_in')
def home(request: HttpRequest) -> HttpResponse:
if not request.user.is_anonymous and request.user.has_verified_email:
return render(request, 'home.html')
else:
return redirect('sign_in')
Create a folder called templates
in your sim
app, and add the below files and HTML to the templates
folder:
sign_in.html
<!DOCTYPE html>
<html>
<head>
<title>Sign in</title>
<script src="https://unpkg.com/htmx.org"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
form {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 400px;
font-size: 14px;
}
h2 {
color: #333;
text-align: center;
}
input[type="text"] {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
input[type="text"]:focus {
border-color: #0056b3;
outline: none;
}
button {
width: 100%;
padding: 10px;
background-color: #0056b3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #004494;
}
p {
color: #333;
text-align: center;
}
</style>
</head>
<body>
<form method="post" hx-post="/sign-in" hx-target="#result">
<p>Sign in</p>
{% csrf_token %}
<input type="text" name="email" placeholder="Email" required>
<button type="submit">Sign in</button>
<div id="result"></div>
</form>
</body>
</html>
home.html
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<p>Hi {{ request.user }} 👋</p>
<p>You've successfully signed in using your email address ✅</p>
<p>Click here to <a href="/sign-out">sign out</a></p>
</body>
</html>
Create the file sim/urls.py
, and add the below code to it to set up the URL for email verification:
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('sign-in', views.SendSignInEmail.as_view(), name='sign_in'),
path('sign-out', views.sign_out, name='sign_out'),
path('verify-email/<uidb64>/<token>/', views.verify_email, name='verify_email'),
]
Add include sim.urls
in your core/urls.py
.
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]
sim/forms.py
and add the below code to it:from django.forms import ModelForm
from .models import User
class CreateUserForm(ModelForm):
class Meta:
model = User
fields = ['username', 'email', 'password']
python manage.py runserver
Visit http://localhost:8000/
to sign in with your email address. You should receive an email with a link to verify your email address. Click the link, and you will see the home screen.
For your production server, I'd recommend connecting a production-grade server (Examples: Amazon SES, Mailgun, Sendgrid, etc.) to your Django app. This is a good idea because you get:
After you’ve signed up for a production-grade email service, connect the service to your app simply by adding the service’s details as environment variables to your production server.
If you’re unsure about how to do that, see me adding environment variables to a production server in this guide Deploy an instant messaging app with Django 🚀
Do you also dream of creating Django products so quickly they break the space-time continuum? Yeah, me too. We're like Django wizards, eager to turn our magical ideas into reality at the snap of our fingers.
Well, let me introduce you to the magic wand I'm building: Photon Designer. It's a visual editor that puts the 'fast' in 'very fast.' When Photon Designer gets going, it slings Django templates at you faster than light escaping a black hole.
Warning: may cause excessive joy and productivity.