django_real_time_validation
Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications
Posted on August 9, 2021
Last part, we introduced building the student registration system. We stopped at the point of creating some additional files, tasks.py
and tokens.py
. In this part, we'll continue with the implementation.
The source code to this point is hosted on github while the source code for the entire application is:
Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications
tokens.py
and tasks.py
files
While concluding part 3 of this series, we created tokens.py
and tasks.py
files. While the former handles creating unique tokens to validate users, the latter houses the logic for sending emails via celery
. In this project, celery
, a distributed task queue, handles all background tasks which encompass sending mails. With this, we will fulfil this segment of the requirements:
...Time attacks must be addressed by sending the mails asynchronously...
The content of tokens.py
is pretty straightforward:
# accounts > tokens.py
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from six import text_type
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
return (
text_type(user.pk)
+ text_type(timestamp)
+ text_type(user.is_student)
+ text_type(user.is_lecturer)
)
account_activation_token = AccountActivationTokenGenerator()
We are inheriting Django's PasswordResetTokenGenerator
and then hashing based on the user's id (a UUID in our case), the time and other specific user attributes. It's fairly secure and unique! We then assign this to account_activation_token
which we later called in our student_signup
function.
To implement the tasks.py
, we need to install celery
with Redis
backend. Ensure you have a full working setup for redis.
In the virtual environment for this project, install using either pip
or pipenv
(if you have been using pipenv since inception) and set it up:
┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_real_time_validation]
└─$[sirneij@sirneij django_real_time_validation]$ pipenv install "celery[redis]"
Then, create a celery.py
file in your project's directory. It should be in the directory as your project's settings.py
file.
┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_real_time_validation]
└─$[sirneij@sirneij django_real_time_validation]$ touch authentication/celery.py
and populate it with:
# authentication > celery.py
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentication.settings")
app = Celery("authentication")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print(f"Request: {self.request!r}")
This was copied from using celery with django with slight modifications of putting in my app's name in lines 6 and 8.
To ensure that the app is loaded when Django starts so that the @shared_task decorator will use it, import this app in your project_name/__init__.py
:
# authentication > __init__.py
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ("celery_app",)
Now to tasks.py
:
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import mail
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from celery import shared_task
@shared_task
def send_email_message(subject, template_name, user_id, ctx):
html_message = render_to_string(template_name, ctx)
plain_message = strip_tags(html_message)
mail.send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[get_user_model().objects.get(id=user_id).email],
fail_silently=False,
html_message=html_message,
)
It is a simple function decorated with celery's shared_task
. It uses Django's mail
to send the messages. It is very important to ensure that you do not pass a user object into a celery task. Passing only one attribute of the user model, in this case, user_id
, is the solution. Passing model objects or instances leads to a common Object not serializable
error. To wrap up the configurations, let's append to the settings.py
this snippet:
CELERY_BROKER_URL = config("REDIS_URL", default="")
CELERY_RESULT_BACKEND = config("REDIS_URL", default="")
CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
Your REDIS_URL
is your local redis host and port(of the form redis://host:port
). A good practice is to put this in a .env
file and never upload it to GitHub
by including the file path in your .gitignore
file so as not to upload it for others to see.
urls.py
Now that the preliminaries are taken care of, let's examine the student_signup
view function written in the last part. First, we initialized the StudentRegistrationForm
and then checked that the request coming in is POST
. If true, we made a copy of the request data and subsequently retrieved the email
, username
and password
the request user inputted. If the email
conforms with the rules created in the last part, a user instance is created and then, we test the user's password
and email
against other validations. If they scale through, we insert other user parameters into the instance created and proceed to send the user a mail for confirmation. Take note of the context we passed into the celery task:
...
ctx = {
"fullname": user.get_full_name(),
"domain": str(get_current_site(request)),
"uid": urlsafe_base64_encode(force_bytes(user.pk)),
"token": account_activation_token.make_token(user),
}
Ensure you stringify the get_current_site(request)
, if not you will run into a celery
problem of not being able to serialize request
data.
If the user's password and username do not conform with our rules, such user is deleted from the database: get_user_model().objects.get(email=post_data.get("email")).delete()
. Let's now add this to our urls.py
file:
# accounts > urls.py
...
urlpatterns = [
...
path("student-sign-up/", views.student_signup, name="student_signup"),
]
We also need some functions to inform users that they need to check their email, and another to activate the user after clicking the link:
# accounts > views.py
...
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
...
def activate(request, uidb64, token):
try:
uid = force_text(urlsafe_base64_decode(uidb64))
user = get_user_model().objects.get(pk=uid)
except (TypeError, ValueError, OverflowError):
user = None
# checking if the user exists, if the token is valid.
if user is not None and account_activation_token.check_token(user, token):
# if valid set active true
user.is_active = True
user.save()
messages.success(
request, f"Your email has been verified successfully! You are now able to log in."
)
return redirect("accounts:login")
else:
return render(request, "accounts/activation_invalid.html")
def activation_sent_view(request):
return render(request, "accounts/activation_sent.html")
The activate
function uses the value from uidb64
to get the user the token belongs to and then checks the token's validity before activating the user:
# accounts > views.py
...
user.is_active = True
user.save()
...
Let's include them in our urls.py
file:
# accounts > urls.py
...
urlpatterns = [
...
path("sent/", views.activation_sent_view, name="activation_sent"),
path("activate/<uidb64>/<token>/", views.activate, name="activate"),
]
To see what we have done so far, let's put in some html and css. Create accounts/activation_sent.html
(mail sent notification template), accounts/activation_invalid.html
(invalid token template), accounts/student_signup.html
(student registration), accounts/activation_request.txt
(for text-base emails) and accounts/activation_request.html
(html-based email).
┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_real_time_validation]
└─$[sirneij@sirneij django_real_time_validation]$ touch templates/accounts/activation_sent.html templates/accounts/activation_invalid.html templates/accounts/student_signup.html templates/accounts/activation_request.txt templates/accounts/activation_request.html
activation_request.txt
should look like:
<!--templates/accounts/activation_request.txt-->
{% autoescape off %}
Hi {{ fullname }},
Thank you for joining us on this great platform.
Please click the following button to confirm your registration...
By the way, if the above button is not clickable, paste the following link in your browser.
http://{{ domain }}{% url 'accounts:activate' uidb64=uid token=token %}
Django Authentication Webmaster
{% endautoescape %}
Make activation_request.html
appear as follows:
<!--templates/accounts/activation_request.html-->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<style>
* {
margin: 0;
padding: 0;
font-size: 100%;
font-family: "Avenir Next", "Helvetica Neue", "Helvetica", Helvetica,
Arial, sans-serif;
line-height: 1.65;
}
img {
max-width: 100%;
margin: 0 auto;
display: block;
}
body,
.body-wrap {
width: 100% !important;
height: 100%;
background: #f8f8f8;
}
a {
color: #206bc4;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
.button {
display: inline-block;
color: #ffffff;
background: #206bc4;
border: solid #206bc4;
border-width: 10px 20px 8px;
font-weight: bold;
border-radius: 4px;
}
.button:hover {
text-decoration: none;
color: #ffffff;
background-color: #1b59a3;
border-color: #195398;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 20px;
line-height: 1.25;
}
h1 {
font-size: 32px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 24px;
}
h4 {
font-size: 20px;
}
h5 {
font-size: 16px;
}
p,
ul,
ol {
font-size: 16px;
font-weight: normal;
margin-bottom: 20px;
}
.container {
display: block !important;
clear: both !important;
margin: 0 auto !important;
max-width: 580px !important;
}
.container table {
width: 100% !important;
border-collapse: collapse;
}
.container .masthead {
margin-top: 20px;
padding: 80px 0;
background: #206bc4;
color: #ffffff;
}
.container .masthead h1 {
margin: 0 auto !important;
max-width: 90%;
text-transform: uppercase;
}
.container .content {
background: #ffffff;
padding: 30px 35px;
}
.container .content.footer {
background: none;
}
.container .content.footer p {
margin-bottom: 0;
color: #888;
text-align: center;
font-size: 14px;
}
.container .content.footer a {
color: #888;
text-decoration: none;
font-weight: bold;
}
.container .content.footer a:hover {
text-decoration: underline;
}
</style>
<title>Verify your email address.</title>
</head>
<body>
<!-- auto -->
{% autoescape off %}
<table class="body-wrap">
<tr>
<td class="container">
<!-- Message start -->
<table>
<tr>
<td align="center" class="masthead">
<h1>Welcome to Django Authentication System...</h1>
</td>
</tr>
<tr>
<td class="content">
<h2>
Hi
<strong style="text-transform: capitalize"
>{{ fullname }}</strong
>,
</h2>
<p>Thank you for joining us on this great platform.</p>
<p>
Please click the following button to confirm your
registration...
</p>
<table>
<tr>
<td align="center">
<p>
<a
href="http://{{ domain }}{% url 'accounts:activate' uidb64=uid token=token %}"
class="button"
>Yes, I'm in!</a
>
</p>
</td>
</tr>
</table>
<p>
By the way, if the above button is not clickable, paste the
following link in your browser.
<!-- email link -->
http://{{ domain }}{% url 'accounts:activate' uidb64=uid token=token %}
</p>
<p><em>– Django Authentication Webmaster</em></p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="container">
<!-- Message start -->
<table>
<tr>
<td class="content footer" align="center">
<p>
Sent by <a href="{{ domain }}">Django Authentication</a>,
Federal University of Technology, Akure, South Gate, Ondo
State, Nigeria.
</p>
<p>
<a href="mailto:nelsonidogun@gmail.com"
>nelsonidogun@gmail.com</a
>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- end auto -->
{% endautoescape %}
</body>
</html>
Just a simple HTML file. It incorporates some best practices for HTML emails.
activation_sent.html
has this:
<!--templates/accounts/activation_sent.html-->
{% extends 'base.html' %}
<!-- title -->
{% block title %} Verification email sent {% endblock title %}
<!-- static files -->
{% load static %}
<!-- content starts -->
{% block content %}
<div class="row center-content">
<div class="col s12" style="max-width: 30rem">
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<span class="card-title">Thank you for creating an account!</span>
<p>
An email has been sent to the e-mail address you provided during
registeration for confirmation.
</p>
<p>
Make sure you visit the link provided in mail as it will soon be
revoked.
</p>
</div>
</div>
</div>
</div>
<!-- content ends -->
{% endblock content %}
As for activation_invalid.html
, it should be like this:
{% extends 'base.html' %}
<!-- title -->
{% block title %} Verification email failed {% endblock title %}
<!-- static files -->
{% load static %}
<!-- content starts -->
{% block content %}
<div class="row center-content">
<div class="col s12" style="max-width: 30rem">
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<span class="card-title">Invalid activation link!!</span>
<p>
Oops! There were issues with the activation link, it was highly
perceived to have been used before... Please, consider requesting for
an
<a
href="{% url 'accounts:resend_email' %}"
class="btn waves-effect waves-light"
>
activate link resend </a
>.
</p>
</div>
</div>
</div>
</div>
<!-- content ends -->
{% endblock content %}
Let's call it a day here. We'll continue from here next time!
Happy birthday to me 🎂✨🥳🤩.
Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and Twitter.
If you found this article valuable, consider sharing it with your network to help spread the knowledge!
Posted on August 9, 2021
Sign up to receive the latest update from our blog.