Django authentication and authorization

Archie To

Authentication and authorization are two vital concepts in web development. You don’t want somebody to break into your house, view your private rooms, rearrange furnitures, or take away your stuffs. The same thing applies for your web app. You don’t want a random person to see private contents, make changes or delete important information. Fortunately, Django provides developer an easy way to implement authentication and authorization out of the box.

Set up

This is very simple and straightforward. In settings.py, make sure django.contrib.auth is in your INSTALLED_APPS. This is included by default when you create a Django project. After that, simply run:

python manage.py migrate

to create the necessary database schema.

Authentication

Authentication verifies a user’s identity to confirm that they are who they claim to be. Using the house example above, if a person claims to be a tenant, we would check if that person has the house key and if the house key can open the front door. If yes, we let them into the house.

Authentication with username and password (The classic)

With the help of authenticate and login functions from django.contrib.auth, this cannot be any easier:

from django.contrib.auth import authenticate, login


def my_view(request):
    username = request.POST["username"]
    password = request.POST["password"]
    user = authenticate(request, username=username, password=password)      # Check if there is any user with the provided username and password
    if user is not None:
        login(request, user)
        # Redirect to a success page.
        ...
    else:
        # Return an 'invalid login' error message.
        ...

authenticate checks if the credentials provided match a user in the database. If yes, it returns an User object. Otherwise, it returns None.

login stores the current user id into the session, and set request.session.user to the user that we passed in as the second parameter. It does a bit more than that but these two things are the most important.

Authentication with username only (a possible use case for SSO)

What is Single Sign-on (SSO)?

Single Sign-On (SSO) authentication is a user authentication process that allows a user to access multiple applications with one set of centrally managed login credentials. For example, when a user authenticates with your Django app, they are redirected to UVic’s institutional identity provider where they will have to enter their credentials. Once they log in successfully, they are redirected back to the Django app and immediately log in to the app successfully.

Use Django authentication for SSO

Django doesn’t actually provide a built-in SSO integration with 3rd party identity providers. However, if you decide to implement SSO with Django, after the authentication process, you will receive a token that contains the current user’s information. If you want your Django app to realize this user as an authenticated user, you can read the username from the token and do the following:

from django.http import JsonResponse
from django.contrib.auth import authenticate, login
from django.contrib.auth.models import User


def my_view(request):
    # SSO implementation code
    ... 
    
    username = read_username_from_token(token)
    try:
        user = User.objects.get(username=username)
    except User.DoesNotExist:
        return JsonResponse({"message": "User not found"}, status=404)
    
    login(request, user)
    # Redirect to a success page.
    ...

Check if user is logged in

Checking the current user in session:

def my_view(request):
    if not request.user.is_authenticated:
        return redirect(f"{settings.LOGIN_URL}?next={request.path}")

The login_required decorator:

from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
...

login_required() does the following:

  • If the user isn’t logged in, redirect to settings.LOGIN_URL, passing the current absolute path in the query string.
  • If the user is logged in, execute the view normally. The view code is free to assume the user is logged in.

login_required() also takes an optional login_url parameter:

from django.contrib.auth.decorators import login_required


@login_required(login_url="/accounts/login/")
def my_view(request):
...

Authorization

Authorization determines whether a user has permission to perform a given action or access specific data within the application. Going back to the house example, this will be deciding what rooms a tenant can enter and what they can do in each room.

There are three main concepts when it comes to Django authorization:

  • User: Represents an individual using the Django application, like a tenant.
  • Permission: Rights to perform specific actions on Django model instances.
  • Group: Collection of users sharing the same permissions set.

User objects have two many-to-many fields: groups and user_permissions. User objects can access their related objects in the same way as any other Django model:

myuser.groups.set([group_list])
myuser.groups.add(group, group, ...)
myuser.groups.remove(group, group, ...)
myuser.groups.clear()
myuser.user_permissions.set([permission_list])
myuser.user_permissions.add(permission, permission, ...)
myuser.user_permissions.remove(permission, permission, ...)
myuser.user_permissions.clear()

Default permissions

When django.contrib.auth is listed in your INSTALLED_APPS setting, it will ensure that four default permissions – add, change, delete, and view – are created for each Django model defined in one of your installed applications.

These permissions will be created when you run manage.py migrate; the first time you run migrate after adding django.contrib.auth to INSTALLED_APPS, the default permissions will be created for all previously-installed models, as well as for any new models being installed at that time. Afterward, it will create default permissions for new models each time you run manage.py migrate (the function that creates permissions is connected to the post_migrate signal).

Assuming you have an application with an app_label foo and a model named Bar, to test for basic permissions you should use:

  • add: user.has_perm('foo.add_bar')
  • change: user.has_perm('foo.change_bar')
  • delete: user.has_perm('foo.delete_bar')
  • view: user.has_perm('foo.view_bar')

The Permission model is rarely accessed directly.

Groups

django.contrib.auth.models.Group models are a generic way of categorizing users so you can apply permissions, or some other label, to those users. A user can belong to any number of groups.

A user in a group automatically has the permissions granted to that group. For example, if the group Site editors has the permission can_edit_home_page, any user in that group will have that permission.

Beyond permissions, groups are a convenient way to categorize users to give them some label, or extended functionality. For example, you could create a group ‘Special users’, and you could write code that could, say, give them access to a members-only portion of your site, or send them members-only email messages.

Custom permissions

To create custom permissions for a given model object, use the permissions model Meta attribute.

This example Task model creates two custom permissions, i.e., actions users can or cannot do with Task instances, specific to your application:

class Task(models.Model):
    ...

    class Meta:
        permissions = [
            ("change_task_status", "Can change the status of tasks"),
            ("close_task", "Can remove a task by setting its status as closed"),
        ]

The only thing this does is create those extra permissions when you run manage.py migrate (the function that creates permissions is connected to the post_migrate signal). Your code is in charge of checking the value of these permissions when a user is trying to access the functionality provided by the application (changing the status of tasks or closing tasks). Continuing the above example, the following checks if a user may close tasks:

def close_task_view(request):
    if not user.has_perm("app.close_task"):
        # Return 400 error
        ...

Conclusion

Django provides a robust authentication and authorization mechanism that will save developers a tremendous amount of time. Leveraging this feature will make your code much shorter and more maintainable. Django authentication and authorization can be used to build a custom login system as well as integrate with a third party identity provider such as Keycloak.