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
anddjango.contrib.contenttypes
(which theauth
app depends on)
The middleware classes are:
SessionMiddleware
to store data about a user in a sessionAuthenticationMiddleware
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
andpassword
from the user - Calls the
django.contrib.auth.authenticate
function with theusername
andpassword
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 orsettings.LOGIN_REDIRECT_URL
if thenext
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 isFalse
. Only staff-level users are allowed to use the built-in Django admin site. You can also use thestaff_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 theuser_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 Createpizzas.view_topping
for Readpizzas.change_topping
for Updatepizzas.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.