Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications - 4

sirneij

John Owolabi Idogun

Posted on August 9, 2021

Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications - 4

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.

Source code

The source code to this point is hosted on github while the source code for the entire application is:

GitHub logo Sirneij / django_real_time_validation

Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications

django_real_time_validation

Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications




Step 7: 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()
Enter fullscreen mode Exit fullscreen mode

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]"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}")

Enter fullscreen mode Exit fullscreen mode

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",)
Enter fullscreen mode Exit fullscreen mode

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,
    )
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.

Step 8: Revisit and connect the student registration function to 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),
            }
Enter fullscreen mode Exit fullscreen mode

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"),
]
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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()
...
Enter fullscreen mode Exit fullscreen mode

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"),
]
Enter fullscreen mode Exit fullscreen mode

Step 9: Creating login and other templates

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
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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 %}

Enter fullscreen mode Exit fullscreen mode

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 %}

Enter fullscreen mode Exit fullscreen mode

Let's call it a day here. We'll continue from here next time!

Outro

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!

💖 💪 🙅 🚩
sirneij
John Owolabi Idogun

Posted on August 9, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related