Episode 10 - User Auth

On this episode, we’re going to look at working with users in a Django project. We’ll see Django’s tools for identifying users and checking what those users are permitted to do on your website.

Listen at Spotify.

Last Episode

On the last episode, I explained the structure of Django application. We also talked why this structure is significant and how Django apps benefit the Django ecosystem as a tool for sharing code.

Authentication And Authorization

Authentication: When a user tries to prove that they are who they say they are, that is authentication. A user will typically authenticate with your site via some login form or using a social provider like Google to verify their identity.

Authentication can only prove that you are you.

Authorization: What is a user allowed to do? Authorization answers that question. We use authorization to determine what permissions or groups a user belongs to, so that we can scope what a user can do on the site.

Authorization determines what you can do.

Setup

The auth features in Django require a couple of built-in Django applications and a couple of middleware classes.

The Django apps are:

  • django.contrib.auth and
  • django.contrib.contenttypes (which the auth app depends on)

The middleware classes are:

  • SessionMiddleware to store data about a user in a session
  • AuthenticationMiddleware to associate users with requests

The Django docs provide additional context about these pre-requisites so check out the auth topic installation section for more details.

Who Authenticates?

In the Django auth system, identity is tracked with a User model. This model stores information that you’d likely want to associate with anyone who uses your site. The model includes:

  • name fields,
  • email address,
  • datetime fields for when a user joins or logs in to your site,
  • boolean fields for some coarse permissions that are very commonly needed,
  • and password data.

Authenticating With Passwords

When a user wants to authenticate, the user must log in to the site. Django includes a LoginView class-based view that can handle the appropriate steps. The LoginView is a form view that:

  • Collects the username and password from the user
  • Calls the django.contrib.auth.authenticate function with the username and password to confirm that the user is who they claim to be
  • Redirects to either a path that is set as the value of the next parameter in the URL’s querystring or settings.LOGIN_REDIRECT_URL if the next parameter isn’t set
  • Or, if authentication failed, re-renders the form page with appropriate error messages

The authenticate function will loop through any auth backends that are set in the AUTHENTICATION_BACKENDS list setting. Each backend can do one of three things:

  • Authenticate correctly with the user and return a User instance.
  • Not authenticate and return None. In this case, the next backend is tried.
  • Not authenticate and raise a PermissionDenied exception. In this case, no other backends are tried.

The ModelBackend is named as it is because it uses the User model to authenticate. Given a username and password from the user, the backend compares the provided data to any existing User records.

Django doesn’t store actual passwords. To do so would be a major weakness in the framework because any leak of the database would leak all users passwords. And that’s totally not cool. Instead, the password field on the User model stores a hash of the password.

Why is this useful? By computing hashes, Django can safely store that computed value without compromising a user’s password. When a user wants to authenticate with a site, the user submits a password, Django computes the hash on that submitted value and compares it to the hash stored in the database. If the hashes match, then the site can conclude that the user sent the correct password. Only the password’s hash would match the hash stored in the User model.

Hashing is a fascinating subject. If you want to learn more about the guts of how Django manages hashes, I would suggest reading the Password management in Django docs to see the details.

Authentication Views

Is Django going to expect you to call the authenticate function and wire together all the views yourself? No!

You can add the set of views with a single include:

# project/urls.py

from django.urls import include, path

urlpatterns = [
    ...
    path("accounts/", include("django.contrib.auth.urls")),
]

This set includes a variety of features.

  • A login view
  • A logout view
  • Views to change a password
  • Views to reset a password

The All authentication views documentation provides information about each view and the name of each template to override.

We’ve now seen how Django authenticates users to a website with the User model, the authenticate function, and the built-in authentication backend, ModelBackend. We’ve also seen how Django provides views to assist with login, logout, and password management.

Once a user is authenticated, what is that user allowed to do? We’ll see that next as we explore authorization in Django.

What’s Allowed?

Authorization From User Attributes

The User model includes an is_authenticated attribute. Predictably, users that have authenticated will return True for is_authenticated while AnonymousUser instances return False for the same attribute.

Django provides a login_required decorator that can use this is_authenticated information. The decorator will gate any view that needs a user to be authenticated.

There are other boolean values on the User model that you can use for authorization checking.

  • is_staff is a boolean to decide whether a user is a staff member or not. By default, this boolean is False. Only staff-level users are allowed to use the built-in Django admin site. You can also use the staff_member_required decorator if you have views that should only be used by members of your team with that permission.
  • is_superuser is a special flag to indicate a user that should have access to everything. This “superuser” concept is very similar to the superuser that is present in Linux permission systems. There’s no special decorator for this boolean, but you could use the user_passes_test decorator if you had very private views that you needed to protect.
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import user_passes_test
from django.http import HttpResponse

@staff_member_required
def a_staff_view(request):
    return HttpResponse("You are a user with staff level permission.")

def check_superuser(user):
    return user.is_superuser

@user_passes_test(check_superuser)
def special_view(request):
    return HttpResponse("Super special response")

Authorization From Permissions And Groups

Django comes with a flexible permission system that can let your application control who can see what. The permission system includes some convenient auto-created permissions as well as the ability to make custom permission for whatever purpose. These permission records are Permission model instances from django.contrib.auth.models.

If you have a pizzas app and create a Topping model, Django would create the following permissions:

  • pizzas.add_topping for Create
  • pizzas.view_topping for Read
  • pizzas.change_topping for Update
  • pizzas.delete_topping for Delete
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from pizzas.models import Topping

content_type = ContentType.objects.get_for_model(Topping)
permission = Permission.objects.get(
    content_type=content_type, codename="add_topping")
chef_id = 42
chef = User.objects.get(id=42)
chef.user_permissions.add(permission)

Django has an ability to create groups to alleviate the problem of adding permissions for one user at a time. The Group model is the intersection between a set of permissions and a set of users. Thus, you could create a group like “Support Team,” assign all the permissions that such a team should have, and include all your support staff on that team. Now, any time that the support staff members require a new permission, it can be added once to the group.

A user’s groups are tracked with another ManyToManyField called groups.

from django.contrib.auth.models import Group, User

support_team = Group.objects.get(name="Support Team")
support_sally = User.objects.get(username="sally")
support_sally.groups.add(support_team)

In addition to the built-in permissions that Django creates and the group management system, you can create additional permissions for your own purposes.

from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from pizzas.models import Pizza

content_type = ContentType.objects.get_for_model(Pizza)
permission = Permission.objects.create(
    codename="can_bake",
    name="Can Bake Pizza",
    content_type=content_type,
)
chef_id = 42
chef = User.objects.get(id=42)
chef.user_permissions.add(permission)

To check on the permission in our code, you can use the has_perm method on the User model. has_perm expects an application label and the permission codename joined together by a period.

>>> chef = User.objects.get(id=42)
>>> chef.has_perm('pizzas.can_bake')
True

You can also use a decorator on a view to check a permission as well. The decorator will check the request.user for the proper permission.

# pizzas/views.py

from django.contrib.auth.decorators import permission_required

@permission_required('pizzas.can_bake')
def bake_pizza(request):
    # Time to bake the pizza if you're allowed.
    ...

Working With Users In Views And Templates

The first way to interact with users is inside of views. Part of configuring the auth system is to include the AuthenticationMiddleware in django.contrib.auth.middleware.

# application/views.py

from django.http import HttpResponse

def my_view(request):
    if request.user.is_authenticated:
        return HttpResponse('You are logged in.')
    else:
        return HttpResponse('Hello guest!')

How about templates? If you had to add a user to a view’s context for every view, that would be tedious.

Thankfully, there is a context processor named auth that let’s you avoid that pain (the processor is in django.contrib.auth.context_processors). The context processor will add a user to the context of every view when processing a request.

Since the AuthenticationMiddleware adds the user to the request, the context processor has the very trivial job of creating a dictionary like {'user': request.user}. There’s a bit more to the actual implementation, and you can check out the Django source code if you want to see those details.

Now you’ve seen how Django leverages the auth middleware to make users easily accessible to your views and templates.

Summary

In this episode, we looked into the auth system.

We saw:

  • How auth is set up
  • What the User model is
  • How authentication works
  • Django’s built-in views for making a login system
  • What levels of authorization are available
  • How to access users in views and templates

Next Time

On the next episode, we’ll discuss middleware in Django. As the name implies, middleware is some code that exists in the “middle” of the request and response process.

You can follow the show on Spotify. Or follow me or the show on X at @mblayman or @djangoriffs.

Please rate or review on Apple Podcasts, Spotify, or from wherever you listen to podcasts. Your rating will help others discover the podcast, and I would be very grateful.

Django Riffs is supported by listeners like you. If you can contribute financially to cover hosting and production costs, please check out my Patreon page to see how you can help out.