Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications - 2
Tags: webdev, django, javascript, tutorial
Published: 2021-07-24 09:15:20

In the previous part, we designed the database schema to address this part of the specification:

> Build a multi-user authentication system. A user can either be a **Student** or a **Lecturer** ...

## Source code

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

{% github Sirneij/django_real_time_validation %}

The project is also live on heroku and can be accessed via this [django-authentication-app.herokuapp.com][6]

In this part, we'll take a tour of how the logic will be implemented. Some part of `views.py`, `urls.py`, `forms.py`, and `authentication.py` will be implemented.

Let's put our coding hart 👲 on and get our hands 🧰 dirty!

## Step 2: Creating other files

First off, we'll be using additional files as follows:

- `accounts/forms.py`: this holds everything form related.
- `accounts/utils.py`: to avoid cluttering the `views.py` file, helper functions will be domiciled here.
- `accounts/authentication.py`: this houses the custom authentication backend we'll be using to enable signing in with both email address and username.

To create the files, navigate to your terminal and run the following command:

```bash
┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_real_time_validation]
└─$[sirneij@sirneij django_real_time_validation]$ touch accounts/utils.py accounts/forms.py accounts/authentication.py
```

## Step 3: Custom authentication backend

A section of the specification we are implementing says:

> ...Users can either login with any of **Email/Password** or **Username/Password** combination...

To do this, we need a custom authentication backend. Luckily, [django gives us a pointer to how this can be done][2]. Fire up you text editor and make `accounts/authentication.py` look like this:

```python
# accounts > authentication.py

from .models import User


class EmailAuthenticationBackend(object):
    """
    Authenticate using an e-mail address.
    """

    def authenticate(self, request, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):  # and user.is_active:
                return user
            return None
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None
```

We aint inheriting any built-in backend here but this still works. However, we still fall back to Django's default authentication backend which authenticates with username.

Though we have written this self-explanatory code snippet, it does nothing yet. To make it do something, we need to register it. Append the snippet below to your project's `settings.py` file:

```python
# authentication > settings.py
...
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "accounts.authentication.EmailAuthenticationBackend", # our new authentication backend
]
...
```

Let's add our new `User` model to django's admin page. Open up `accounts/admin.py` and append the following:

```python
# accounts > admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import User


class CustomUserAdmin(UserAdmin):
    model = User
    readonly_fields = [
        "date_joined",
    ]
    actions = [
        "activate_users",
    ]
    list_display = (
        "username",
        "email",
        "first_name",
        "last_name",
        "is_staff",
        "is_student",
        "is_lecturer",
    )

    def get_inline_instances(self, request, obj=None):
        if not obj:
            return list()
        return super(CustomUserAdmin, self).get_inline_instances(request, obj)

    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        is_superuser = request.user.is_superuser
        disabled_fields = set()

        if not is_superuser:
            disabled_fields |= {
                "username",
                "is_superuser",
            }
        # Prevent non-superusers from editing their own permissions
        if not is_superuser and obj is not None and obj == request.user:
            disabled_fields |= {
                "is_staff",
                "is_superuser",
                "groups",
                "user_permissions",
            }
        for f in disabled_fields:
            if f in form.base_fields:
                form.base_fields[f].disabled = True

        return form

    def activate_users(self, request, queryset):
        cannot = queryset.filter(is_active=False).update(is_active=True)
        self.message_user(request, "Activated {} users.".format(cannot))

    activate_users.short_description = "Activate Users"  # type: ignore

    def get_actions(self, request):
        actions = super().get_actions(request)
        if not request.user.has_perm("auth.change_user"):
            del actions["activate_users"]
        return actions


admin.site.register(User, CustomUserAdmin)
```

We have set up custom user admin business logic. In the code, we added a custom action `activate user` which allows a large number of users to be activated at once. This was implemented in case the registration flow we are planning fails and we want the superuser to be empowered with the ability to mass-activate users. We also hide a couple of fields from any user who has access to the admin page but not a `superuser`. This is for security concerns. To learn more about this, [Haki Benita][3]'s article is an awesome guide.

## Step 4: Login view logic

It's time to test our `custom authentication` backend. First, we need a form to login users. Let's create it.

```python
# accounts > forms.py

from django import forms


class LoginForm(forms.Form):
    username = forms.CharField(widget=forms.TextInput(attrs={"placeholder": "Username or Email"}))
    password = forms.CharField(widget=forms.PasswordInput(attrs={"placeholder": "Password"}))

    def __init__(self, *args, **kwargs):
        super(LoginForm, self).__init__(*args, **kwargs)
        for visible in self.visible_fields():
            visible.field.widget.attrs["class"] = "validate"
```

It is a very simple form with two fields: `username` and `password`. However, the `username` field also accommodates `email` addresses. This is to conform with our specification. The `__init__` dunder method applies `class=validate` to all the visible fields in the form. It is a nice shortcut mostly when you are working with `ModelForms`. This `validate` class is available in `materialize css`. The next agender is to use this form in the `views.py` file.

```python
# accounts > views.py

from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.shortcuts import redirect, render
from django.urls.base import reverse

from .forms import LoginForm

...

def login_user(request):
    form = LoginForm(request.POST or None)
    msg = "Enter your credentials"
    if request.method == "POST":
        if form.is_valid():
            username = form.cleaned_data.get("username").replace("/", "")
            password = form.cleaned_data.get("password")
            user = authenticate(username=username, password=password)
            if user is not None:
                if user.is_active:
                    login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
                    messages.success(request, f"Login successful!")
                    if "next" in request.POST:
                        return redirect(request.POST.get("next"))
                    else:
                        return redirect("accounts:index")
                else:
                    messages.error(
                        request,
                        f"Login unsuccessful! Your account has not been activated. Activate your account via {reverse('accounts:resend_email')}",
                    )
                    msg = "Inactive account details"
            else:
                messages.error(request, f"No user with the provided details exists in our system.")
        else:
            messages.error(request, f"Error validating the form")
            msg = "Error validating the form"
    context = {
        "form": form,
        "page_title": "Login in",
        "msg": msg,
    }
    return render(request, "accounts/login.html", context)

```

It is a basic authentication logic. Some pointers are removing all forward slashes , `/` from the inputted username, in the case of students, and using our custom authentication backend:

```python
...
login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
...

```

to login users in. We also covered the part of the specification that says:

> **Until confirmation, no user is allowed to log in**.

Though, by default, you can not login if `is_active=False` but since we are using custom `authentication` backend, I feel we should enforce that. We could have done this earlier on in the `authentication` backend code. Next, we check whether there is a page we need to redirect to by checking the content of `next`. We will put this in our template soon. It is a nice way to redirect users back to wherever they wanted to visit before being asked to login.

Let's add this and django's built-in `logout` view to our `urls.py` file.

```python
# accounts > urls.py

from django.contrib.auth import views as auth_views
...

urlpatterns = [
    ...

    path("login", views.login_user, name="login"),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]

```

By extension, let's register this in our `settings.py` file too.

```python
# accounts > settings.py

...

AUTH_USER_MODEL = "accounts.User"
LOGIN_URL = "accounts:login"
LOGOUT_URL = "accounts:logout"
LOGOUT_REDIRECT_URL = "accounts:index"

...

```

We always want to go back to home page when we logout.

FInally, it's time to render it out.

```jinja
{% extends "base.html" %}
<!--static-->
{% load static %}
<!--title-->
{% block title %}{{page_title}}{% endblock %}
<!--content-->
{% block content%}
<h4 id="signup-text">Welcome back</h4>
<div class="form-container">
  <!--  <h5 class="auth-header">Assignment Management System</h5>-->
  <div class="signin-form">
    <form method="POST" action="" id="loginForm">
      {% csrf_token %}
      <!---->
      <h5 style="text-align: ceneter">{{msg}}</h5>
      <div class="row">
        {% for field in form %}
        <div class="input-field col s12">
          {% if forloop.counter == 1 %}
          <i class="material-icons prefix">email</i>
          {% elif forloop.counter == 2 %}
          <i class="material-icons prefix">vpn_key</i>
          {% endif %}
          <label for="id_{{field.label|lower}}"> {{field.label}}* </label>
          {{ field }}
          <!---->
          {% if field.errors %}
          <span class="helper-text email-error">{{field.errors}}</span>
          {% endif %}
        </div>
        {% endfor %}
      </div>

      <!---->
      {% if request.GET.next %}
      <input type="hidden" name="next" value="{{request.GET.next}}" />
      {% endif %}
      <button
        class="btn waves-effect waves-light btn-large"
        type="submit"
        name="login"
        id="loginBtn"
      >
        Log in
        <i class="material-icons right">send</i>
      </button>
    </form>
    <ul>
      <li class="forgot-password-link">
        <a href="#"> Forgot password?</a>
      </li>
    </ul>
  </div>
  <div class="signup-illustration">
    <img
      src="{% static 'img/sign-up-illustration.svg' %}"
      alt="Sign in illustration"
    />
  </div>
</div>

{% endblock %}

```

It is a basic materialize css form with icons. Since we have only two fields, `username/email` and `password`, we use `if` statement to check the `forloop` counter and put `icons` appropriately. Noticed this line?:

```jinja
 {% if request.GET.next %}
      <input type="hidden" name="next" value="{{request.GET.next}}" />
 {% endif %}
```

This is what saves the `next` field we discussed previously. It is a hidden input since we don't want users to see it's content, just for reference.

To incept the real-time form validation we've been clamouring for, let's add a bit of JavaScript to this form. At first, we want the `Log in` button to be disabled until users type in both the `username or email` and `password`. That's enough for now.

Append this code to `templates/accounts/login.html` file:

```jinja
<!---->
{% block js %}
<script>
  const loginForm = document.getElementById("loginForm");
  const formElements = document.querySelectorAll("#loginForm  input");
  loginForm.addEventListener("keyup", (event) => {
    let empty = false;
    formElements.forEach((element) => {
      if (element.value === "") {
        empty = true;
      }
    });

    if (empty) {
      $("#loginBtn").addClass("disabled");
    } else {
      $("#loginBtn").removeClass("disabled");
    }
  });
</script>
{% endblock js %}

```

It simply listens to `keyup` events in any of the form's input elements. If any is empty, the button remains disabled, else? Enabled! Simple huh 😎!

Modify the button to be disabled by default.

```jinja
...

<button class="btn waves-effect waves-light btn-large disabled"
        type="submit"
        name="login"
        id="loginBtn"
      >
        Log in
        <i class="material-icons right">send</i>
      </button>

...
```

We have already created a `js` block at the bottom of `templates/base.html` file

Now, update your `templates/includes/_header.html` so we can have easy navigation for both mobile and desktop portions.

```jinja
...

<li><a href="{% url 'accounts:logout' %}">Logout</a></li>

...

 <li><a href="{% url 'accounts:login' %}">Login</a></li>

...
```

Can we test it out now? Because I can't wait 💃🕺.

![Login page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0mxknatsotlzv2na1h4.png)

Damn! It is appealing 🤗... Create a superuser account and test it out with either `Email or username` and `password`.

You want the code to this end? Get it on [github][4]

Let's end it here, it's becoming unbearably too long 😌. See ya 👋 🚶!!!

## Outro

Enjoyed this article, consider [contacting me for a job, something worthwhile or buying a coffee ☕](mailto:sirneij@gmail.com).

[1]: https://github.com/Sirneij/django_real_time_validation/tree/9798a33b7ffcd46851318d7764e3f5417c4de89f "Full code for the first part"
[2]: https://docs.djangoproject.com/en/3.2/topics/auth/customizing/ "Customizing authentication in Django"
[3]: https://realpython.com/manage-users-in-django-admin/ "What You Need to Know to Manage Users in Django Admin"
[4]: https://github.com/Sirneij/django_real_time_validation/tree/6ce9af1ca64cd154d30f7db2ce9dfd861cd31546 "Authentication backend and Login, logout"

[6]: https://django-authentication-app.herokuapp.com/ "Live version of the application"