Skip to content

Authentication

Theory

Data of users of a website written in Django is stored in the database. Django allows us to manage users:

  • from the administrator panel

  • from the application code

Django includes a full implementation of authentication mechanisms such as:

  • Registration

  • Login

  • Authentication

We identify users with a pair of values:

  • Name

  • Password

The password is secret information. Passwords should not even know the database where we store them.

So how do you authenticate users?

  • We take from the user the password

  • We take from the database the salt drawn at the time of setting up the password

  • To the password we add salt (we often call it "salting the password")

  • Using the given algorithm we calculate the value of the hash function of the salted password as many times as indicated by the number of iterations

Thanks to this procedure, each time we will obtain from the user a string unique for him at the time of creating the password.

And why do we need all this?

  • Password - a secret string known to the user confirming his identity

  • Hash algorithm - irreversible and deterministic function that transforms a string into another string of characters with possibly equal distribution of collisions

    • Irreversibility - on the basis of the hash, it is impossible to determine the truncated value using any algorithm known to mankind

    • Deterministic - always gives the same result under the influence of the same data provided to her

    • Equal distribution of collisions - the same hash should appear for the smallest possible number of different input data

  • Number of iterations - significantly hinders (and in practice makes it impossible) to use rainbow tables to determine a password based on a hash

  • Salt - makes it difficult to use rainbow tables to determine a password based on a hash and prevents you from determining whether two users have the same password

Models

Now let's do some experiments in the Django shell.

  • python manage.py shell

    • from django.contrib.auth.models import User

    • peter = User.objects.create_user('peter', password='regpasswd')

    • peter.username

      • 'peter'
    • peter.password

      • 'pbkdf2_sha256$\$$180000$\$$q5kBMEjiSY6l$\$$dFdsErUZHhOOES/OhIp4SLZorsBmAlt7xZbuLKwAidc='
    • peter.set_password('correctpasswd')

    • peter.save()``

    • User.objects.get(username='peter').password

      • 'pbkdf2_sha256$\$$180000$\$$1VLnQ5GUBREJ$\$$lAdGG5WXgXmKHVnHCNn5bDyZII9ZHqvF3vXJG7pT5fQ='
    • from django.contrib.auth importauthenticate

    • authenticate(username='peter', password='correctpasswd')

      • <User: peter>
    • authenticate(username='peter', password='wrongpasswd')

      • None

Any user's password can also be changed with manage.py

python manage.pychangepassword peter

Views

In views, the request object has a user field that stores the logged-in user's database object. If the user is not logged in, request.user contains a special object of type AnonymousUser, unrelated to the database interface but having all its fields with the same name. To check if the user is logged in, refer to the field request.user.is_authenticated. In practice, we use the decorator @ login_required for the view function. We use the login and logout functions to log in and out of the session.

CBV

In the case of classes, decoration is better replaced by inheritance. It is enough for the class view to inherit from the LoginRequired class.

from django.contrib.auth.mixins import LoginRequiredMixin


class MovieDetailView(LoginRequiredMixin, DetailView):
  template_name = 'movie_detail.html'
  model = Movie


class MovieCreateView(LoginRequiredMixin, CreateView):
  template_name = 'form.html'
  form_class = MovieForm
  success_url = reverse_lazy('viewer:movie_list')


class MovieUpdateView(LoginRequiredMixin, UpdateView):
  template_name = 'form.html'
  model = Movie
  form_class = MovieForm
  success_url = reverse_lazy('viewer:movie_list')


class MovieDeleteView(LoginRequiredMixin, DeleteView):
  template_name = 'movie_confirm_delete.html'
  model = Movie
  success_url = reverse_lazy('viewer:movie_list')

Login

Django comes with a ready-to-use login view LoginView. Just register it in the urls.py file.

from django.contrib.auth.views import LoginView
urlpatterns = [
  path('accounts/login/', LoginView.as_view(), name='login'),
  path('admin/', admin.site.urls),
  path('', IndexView.as_view(), name='index'),
  path('viewer/', include('viewer.urls', namespace='viewer'))
]

Unfortunately, django.contrib.auth does not provide default templates. Therefore, we need to prepare our own template. This template must be placed in such a location that it will be found after reference to accounts/login.html (i.e. hollymovies/viewer/templates/accounts/login.html). Since logging in is a regular form, it is enough if in the new template we put only the tag extending the form form.html

{% extends "form.html" %}

Now let's try to change our assumptions a bit. To handle the login page, let's use the previously created form template. To achieve this, we need to create an authentication view inheriting from LoginView, in which we will set the form.html template. Finally, you will of course need to replace it in urls.py

views.py

from django.contrib.auth.views import LoginView
class SubmittableLoginView(LoginView):
  template_name = 'form.html'

urls.py

from viewer.views import IndexView, SubmittableLoginView
urlpatterns = [
  path('accounts/login/', SubmittableLoginView.as_view(), name='login'),
  path('admin/', admin.site.urls),
  path('', IndexView.as_view(), name='index'),
  path('viewer/', include('viewer.urls', namespace='viewer'))
]

Architecture of the authentication code

Authentication is an independent application belonging to the project because:

  • django.contrib.auth is a separate application inINSTALLED_APPS in settings.py

  • We have one authentication model for each potential application

We decided to change the default authentication structures, so this application has become ours and deserves its folder in our directory structure. From now on, all authentication structures should be kept in the accounts application.

Other operations on authentication models

django.contrib.auth provides more operations that we can adapt to our needs

# The views used below are normally mapped in django.contrib.admin.urls.py
# This URLs file is used to provide a reliable view deployment for test purposes.
# It is also provided as a convenience to those who want to deploy these URLs
# elsewhere.

from django.contrib.auth import views
from django.urls import path

urlpatterns = [
  path('login/', views.LoginView.as_view(), name='login'),
  path('logout/', views.LogoutView.as_view(), name='logout'),

  path('password_change/', views.PasswordChangeView.as_view(), name='password_change'),
  path('password_change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),

  path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
  path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
  path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
  path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]

Let's add logging out and changing the password to our project. For this, we need to override the default password change view. By the way, we will disable the template code with the confirm button. At the end, the views described above should be registered in urls.py.

from django.contrib.auth.views import LoginView, PasswordChangeView
from django.urls import reverse_lazy


class SubmittableLoginView(LoginView):
  template_name = 'form.html'


class SubmittablePasswordChangeView(PasswordChangeView):
  template_name = 'form.html'
  success_url = reverse_lazy('index')
from django.contrib.auth.views import LogoutView
from django.urls import path

from accounts.views import SubmittableLoginView, SubmittablePasswordChangeView

app_name = 'accounts'
urlpatterns = [
  path('login/', SubmittableLoginView.as_view(), name='login'),
  path('logout/', LogoutView.as_view(), name='logout'),
  path(
    'password-change/', SubmittablePasswordChangeView.as_view(),
    name='password_change'
  )
]
LOGIN_REDIRECT_URL = 'index'
LOGOUT_REDIRECT_URL = 'index'

Templates

Let's add authentication links in the base template. Depending on whether the user is logged in or not (value user.is_authenticated), display a link to log in or log out and change the password.

<div class="navbar-nav ml-auto">
  <div class="nav-item dropdown">
    {% if user.is_authenticated %}
      <a class="nav-link active dropdown-toggle" href="#" data-toggle="dropdown">
        {% if user.first_name %}Hello, {{ user.first_name }}!
        {% else %}Hello, {{ user.username }}!{% endif %}
      </a>
      <div class="dropdown-menu dropdown-menu-right">
        <a class="dropdown-item text-right" href="{% url 'accounts:logout' %}">Logout</a>
        <a class="dropdown-item text-right" href="{% url 'accounts:password_change' %}">Change password</a>
      </div>
    {% else %}
      <a class="nav-link active dropdown-toggle" href="#" data-toggle="dropdown">
        You are not logged in.
      </a>
      <div class="dropdown-menu dropdown-menu-right">
        <a class="dropdown-item text-right" href="{% url 'accounts:login' %}">Login</a>
      </div>
    {% endif %}
  </div>
</div>

Extending the User Model

There are several methods for extending the user's back-end. We will discuss the one-to-one relationship method. This method extends the information stored in the database about the user.

Registration form

First, let's prepare a standard form...

from django.contrib.auth.forms import (
  AuthenticationForm, PasswordChangeForm, UserCreationForm
)
class SignUpForm(UserCreationForm):

  class Meta(UserCreationForm.Meta):
    fields = ['username', 'first_name']

  def save(self, commit=True):
    self.instance.is_active = False
    return super().save(commit)

...and a view of registering new users on the site.

from django.views.generic import CreateView

from accounts.forms import (
  SignUpForm, SubmittableAuthenticationForm, SubmittablePasswordChangeForm
)
class SignUpView(CreateView):
  template_name = 'form.html'
  form_class = SignUpForm
  success_url = reverse_lazy('index')

Let's register the created view in accounts/urls.py

from django.contrib.auth.views import LogoutView
from django.urls import path

from accounts.views import (
  SignUpView, SubmittableLoginView, SubmittablePasswordChangeView
)

app_name = 'accounts'
urlpatterns = [
  path('login/', SubmittableLoginView.as_view(), name='login'),
  path('logout/', LogoutView.as_view(), name='logout'),
  path(
    'password-change/', SubmittablePasswordChangeView.as_view(),
    name='password_change'
  ),
  path('sign-up/', SignUpView.as_view(), name='sign_up')
]

And let's link it in the base template

<div class="dropdown-menu dropdown-menu-right">
  <a class="dropdown-item text-right" href="{% url 'accounts:login' %}">Login</a>
  <a class="dropdown-item text-right" href="{% url 'accounts:sign_up' %}">Sign up</a>
</div>

Now let's prepare a model that stores additional information about the user

from django.contrib.auth.models import User
from django.db.models import CASCADE, Model, OneToOneField, TextField


class Profile(Model):
  user = OneToOneField(User, on_delete=CASCADE)
  biography = TextField()

Profile information will be stored in a one-to-one relationship database with the table auth_user.

Next, let's handle saving the profile in the form

from django.db.transaction import atomic
from django.forms import CharField, Form, Textarea

from accounts.models import Profile


class SignUpForm(UserCreationForm):

  class Meta(UserCreationForm.Meta):
    fields = ['username', 'first_name']

  biography = CharField(
    label='Tell us your story with movies', widget=Textarea, min_length=40
  )

  @atomic
  def save(self, commit=True):
    self.instance.is_active = False
    result = super().save(commit)
    biography = self.cleaned_data['biography']
    profile = Profile(biography=biography, user=result)
    if commit:
      profile.save() 
    return result

Decorate the save method with the function @atomic. This will ensure that the changes will be applied to the database only if no error occurs. Otherwise, the changes will not be applied to the base. Thanks to this, we will not leave the base inconsistent.

We've added a new model, so don't forget to migrate!

python manage.py makemigrations

python manage.py migrate