Open-Source Internship opportunity by OpenGenus for programmers. Apply now.
Table of Contents
- What is authentication?
- Source Code
- Building the application
a. Setup a virtual environment
b. Authentication settings
c. Accounts application
d. Notes application - Authorization Mixin
- Authentication & Authorization in action
- Conclusion
What is authentication?
Authentication is verifying who someone claims to be. A user can be authenticated using the following factors:
- What you know - Usernames & passwords, Security questions
- Who you are - Iris pattern, Fingerprints, Facial symmetry
- What you have - Phone, Smart Card
The second authentication factor is sometimes sub categorized into two based on the volatility of the authentication parameters:
- static - The attributes used here never change. i.e. Fingerprint, Iris pattern
- Dynamic - These can change with time or for other reasons like sickness, aging. For example: On some occasions, your voice might be hoarse, squeaky, trembling.
The preceding factors form the basis of Multi-Factor Authentication(MFA). MFA is where we use multiple factors to authenticate a user. MFA constitutes 2-Factor authentication(2FA) and 3-Factor authentication(3-FA).
At this point, It's important to draw a clear line between authentication and authorization as the two are not synonymous. While authentication verifies who the user claims to be, authorization verifies the scope of the authenticated user's actions. In a multi-user operating system, authorization is like preserving a particular set of actions such as creating user accounts, deleting user accounts to only the root user, system administrators.
In this article at OpenGenus, we shall demonstrate the username & password authentication by building a notes takings application based on django. This application will allow users to CRUD notes(only their notes), support markdown in notes, and encrypt their notes.
Source Code
The source code of the application is available
at github.com/OpenGenus/Django-User-Authentication
Building the application
Setup a virtual environment
After downloading the source code, go ahead and create a virtual environment and install the project dependencies.
└─$ virtualenv -p /usr/bin/python3.11 django-auth-demo
pip install -r requirements.txt
Authentication settings
To use django's authentication system, django.contrib.auth and django.contrib.sessions apps must be included in the installed apps.
# Application definition
INSTALLED_APPS = [
...
'django.contrib.auth',
'django.contrib.sessions',
...
]
In addition to the above settings, the session middleware - django.contrib.sessions.middleware.SessionMiddleware must be included in the MIDDLEWARE list.
MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
...
]
If your project is scaffolded with django-admin, you don't have to do anything as the above settings are preconfigured for you.
Accounts app
Models - User
A user is an imperative component in the authentication process. In order to authenticate users, application have to store user identification data(probably given by the user) in the database or some other form of persistent storage so that it can be referenced later. This is commonly known as registration or signup.
Django comes with a default user model that represents a typical user of the application. It comprises the following attributes:
- first_name
- last_name
- username
- password
- date_joined
- is_active
- is_staff
- is_superuser
The only required fields are username and password.
For the requirements of our application, this model suffices since it already defines a username and password which are key attributes in user authentication.
Views
Authentication
Function based approach
from django.contrib.auth import authenticate, login
def login_view(request):
username = request.POST["username"]
password = request.POST["password"]
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Redirect to a success page.
...
else:
# Return an 'invalid login' error message.
...
Firstly, we extract the username and password from the POST data submitted with the request. After the extraction, we verify if we have a user identified by the given username and password by calling authenticate(request, username=username, password=password). The authenticate() function returns None if no user is found with such credentials.
A successful authentication is followed by a session setup and this accomplished by calling login(request, user).
Given the prevalence of authentication in web applications, django comes with a LoginView class that takes away all the heavy lifting from you regarding authenticating users.
Class Based View approach
from django.contrib.auth import views as auth_views
from .forms import SigninForm
class LoginView(auth_views.LoginView):
template_name = 'accounts/signin.html'
form_class = SigninForm
As opposed to the function based approach, this approach is quite simple and elegant and the complexity has been alleviated by encapsulating the authentication steps(the ones we wrote in the function based approach) in a form - django.contrib.auth.forms.AuthenticationForm. In the above class definition, we have customized the authentication form(SigninForm) for some reason we shall see later on. If we were to refactor our function based approach, we would rewrite it as follows:
auth_form = AuthenticationForm(data=request.POST)
if auth_form.is_valid():
# Successful authenticaiton
else:
# Failed authentication
Sometimes, the defaults that might need a little tweak to fit your needs, and as for our case, we need an intuitive template name and a customised form class that will render an eye icon 👁️ for toggling the visibility of the password.
Class based views can have an overwhelming number of options. It allows inspection of the MRO(Method Resolution Order), code of the view methods, and above all, it keeps up with the latest django release while also allowing access to the class based view implementations of the previous releases. I prefer to call it the Wayback Machine of Django's Class based Views
Forms
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext_lazy as _
class SigninForm(AuthenticationForm):
password = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={"autocomplete": "current-password", "data-toggle": "password"}),
)
This form is the same as the default authentication form except that, a data-toggle attribute has been set on the password widget. The data-toggle attribute is used by the bootstrap-show-password package to render an eye icon button 👁️ that toggles the visibility of the password.
Notes app
Model - Note
class Note(models.Model):
title = models.CharField(max_length=50, unique=True)
slug = models.SlugField(editable=False, max_length=50, unique=True)
body = EncryptedTextField()
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notes")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = NotesQueryset.as_manager()
def as_html(self):
md_extensions = ["codehilite", "nl2br", "fenced_code", "sane_lists", "wikilinks", "toc"]
return mark_safe(
markdown.markdown(self.body, extensions=md_extensions, output_format="html")
)
def as_pdf(self):
"""TODO: Allow users to download note as pdf"""
pass
def __str__(self):
return self.title
class Meta:
ordering = ("-updated_at", "-created_at")
The EncryptedTextField encrypts the note(body) when saving to the database and also decrypts it when reading from the database as shown in the following screenshot.
The receding sections aim to introduce how authentication and authorization have been used in the MyNotes application.
Authorization Mixins
from django.core.exceptions import PermissionDenied
from django.views.generic.edit import SingleObjectMixin
class NoteOwnerRequiredMixin(SingleObjectMixin):
def get_object(self, queryset=None):
note = super().get_object(queryset)
if not note.is_owner(self.request.user):
raise PermissionDenied
return note
This mixin is imperative towards maintaining that users only update or delete notes that belong to them.
Class based views are basically a composition of mixins with each mixin contributing a particular functionality.
Similar, for class based views that operate on a single object e.g. UpdateView, DeleteView, they inherit from the SingleObjectMixin. The SingleObjectMixin retrieves an object from the database based on the primary key or a slug token specified in the URL. Consequently, it qualifies to be the right mixin to customize in order to do further checks on the object(note) retrieved.
After subclassing the SingleObjectMixin, we override the get_object() method in order to tap into the object retrieval process and then determine if the retrieved object(note) belongs to the user.
Authentication and Authorization in action
...
class NoteEditView(LoginRequiredMixin, NoteOwnerRequiredMixin, SuccessMessageMixin, UpdateView):
model = Note
form_class = EditNoteForm
template_name = "notes/notes_list.html"
extra_context = {
"add_note_form": AddNoteForm(),
"edit_note_form": EditNoteForm(),
}
success_message = "Your note has been updated"
def form_invalid(self, form):
notes = self.get_queryset().for_user(self.request.user)
context = self.get_context_data()
context.update({"edit_note_form": form, "notes": notes})
return self.render_to_response(context)
def get_success_url(self):
return reverse("notes_list_or_add")
def get(self, request, **kwargs):
return redirect(reverse("notes_list_or_add"))
...
If you revisit the authorization mixin, you will notice that it doesn't check if the user is authenticated(checking for request.user.is_authenticated) and we are not being sloppy here but trying to make our view read like a narrative. The other reason is that, there is already a mixin that does that - LoginRequiredMixin*.
Therefore, the NoteEditView has to inherit from both the LoginRequiredMixin and NoteOwnerRequiredMixin to authenticate a user and also ascertain that the authenticated user is owner of the note being edited.
Conclusion
Class based views can be daunting to use, but I can attest, over time, they become your most favorite option. For the beginners, I urge you to start with function based views and then progress towards class views in order to get a hang of
the request-response cycle. Secondly, as you advance in django, you will realize that class based views are basically function based views.