Hello! Welcome to the next lesson in our journey into Backend Engineering.
In the last lesson, we dove deep into creating powerful, fine-grained custom permission classes. We built an IsOwnerOrReadOnly permission, which is a classic example of object-level authorization—controlling who can edit a specific database record.
Today, we'll take a small step back to solidify a concept that is even more fundamental: securing entire API endpoints based on a user's basic authentication status. Instead of asking, "Can this user edit this specific post?", we will ask a simpler question: "Is this user even logged in?". This is the first line of defense for any private API.
By the end of this lesson, you will be able to use Django REST Framework's built-in permission classes to easily secure your API endpoints, allowing access only to authenticated users.
1. A Quick Refresher: Authentication vs. Permissions
Before we dive into the code, it's crucial to be crystal clear on the distinction between authentication and permissions. They work together, but they are not the same thing.
Understanding Permissions with Django REST Framework
This article from Supertype AI provides a concise and accurate distinction between these two core concepts. Let's start by reading the introductory section.
Please read the section 'Understanding Permissions with Django REST Framework'. Focus on the definitions of Authentication, Permissions, and Throttling, and how they relate to each other in the request cycle.
To summarize the key idea:
- Authentication is about identity. It answers the question, "Who are you?". The system checks your credentials (like a username/password or a token) and attaches a user object (
request.user) to the incoming request. It doesn't, by itself, grant or deny access. - Permissions (or Authorization) is about access rights. It answers the question, "What are you allowed to do?". It uses the identity provided by the authentication step to decide if the requested action is permitted.
Today, we are focusing on the simplest and most common permission check: is the user identified as an authenticated user, or are they anonymous?
2. DRF's Built-in Permissions for Authentication Status
Django REST Framework (DRF) provides a set of pre-built permission classes for the most common use cases, so you don't have to write them from scratch every time.
Let's turn to the official DRF documentation to understand the most important built-in permission classes related to authentication status.
Please read the sections for IsAuthenticated and IsAuthenticatedOrReadOnly. These are two of the most frequently used permission classes in DRF.
Here’s a quick breakdown:
-
permissions.IsAuthenticated: This class is very straightforward. It will deny access to any unauthenticated (anonymous) user and allow access otherwise. This is your go-to choice for endpoints that should be completely private and accessible only to registered users of your application (e.g., a user's profile page, a list of their private orders). -
permissions.IsAuthenticatedOrReadOnly: This class is slightly more nuanced. It will allow any user (authenticated or not) to perform read-only requests (GET, HEAD, OPTIONS). However, for any action that modifies data (POST, PUT, PATCH, DELETE), the user must be authenticated. This is perfect for public APIs where you want everyone to be able to see the data, but only registered users can contribute or make changes (e.g., a blog, a public forum).
3. Implementing IsAuthenticated
Let's see how easy it is to protect an endpoint. Imagine you have a view that shows a user their order history. This is sensitive data that should absolutely not be public.
The following video demonstrates how to apply the IsAuthenticated permission to a view and what happens when an unauthenticated user tries to access it.
Django REST Framework - Permissions and Testing Permissions
This video from BugBytes provides a clear, practical walkthrough of securing an endpoint meant only for authenticated users.
Please watch from the beginning to 4:31. The video first highlights the error that occurs when an endpoint expects an authenticated user but doesn't enforce it. Then, it explains what permissions are, introduces IsAuthenticated, and shows how to add it to a view's permission_classes list.
As you saw in the video, applying the permission is as simple as adding a permission_classes attribute to your view:
# In your views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class UserOrdersView(APIView):
# This line is the key. DRF will run this permission check for every request to this view.
permission_classes = [IsAuthenticated]
def get(self, request, format=None):
# Because of the permission class, we can now be certain that
# `request.user` is an authenticated user instance.
user = request.user
orders = get_orders_for_user(user) # Your business logic here
serializer = OrderSerializer(orders, many=True)
return Response(serializer.data)
When an unauthenticated user tries to GET /api/user-orders/, they will be denied. The browsable API shows a clear message: "Authentication credentials were not provided."

4. Under the Hood: 401 Unauthorized vs. 403 Forbidden
When a permission check fails, DRF returns an error response. You'll typically see one of two HTTP status codes: 401 Unauthorized or 403 Forbidden. While they seem similar, they have distinct meanings, and understanding them is key for building robust APIs.
The DRF documentation explains the specific rules that determine which status code is returned.
Please read the section 'How permissions are determined' again, but this time focus on the bulleted list at the end that explains the rules for returning a '403 Forbidden' vs. a '401 Unauthorized' response.
In essence:
- 401 Unauthorized: This response means "You need to authenticate to access this." The request has failed because it lacks valid authentication credentials. This is often sent when no credentials were provided at all. It usually includes a
WWW-Authenticateheader telling the client how to authenticate (e.g., "Use a Bearer token"). - 403 Forbidden: This response means "I know who you are, but you are not allowed to perform this action." The system successfully authenticated you, but you simply don't have the necessary permissions for this specific resource.
DRF's default behavior, as seen in the video, is often to return a 403 response even if the user is not authenticated. The exact code depends on the configured authentication schemes, but the key takeaway is that the client is being told they cannot access the resource.
5. Testing Your Permissions (The Right Way)
You should never assume your security works. You must prove it with tests. Writing automated tests for permissions is a critical skill for any backend developer.
A good test suite for a protected endpoint should verify two scenarios:
- An unauthenticated user is blocked (receives a 403 or 401).
- An authenticated user is allowed access (receives a 200).
Django REST Framework - Permissions and Testing Permissions
Let's continue with the BugBytes video, which does an excellent job demonstrating how to write tests for both of these scenarios.
Please watch from 12:38 to 16:40. This segment covers two essential tests: one to ensure an unauthenticated request is forbidden, and another to ensure the right status code is used. Pay close attention to how self.client is used and how the expected status code is asserted. The use of the status module is a great best practice to adopt.
Here's an example of what those tests would look like in your tests.py file, based on the video:
# In your tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
class UserOrdersViewTests(APITestCase):
def test_unauthenticated_user_cannot_access_orders(self):
"""
Verify that an unauthenticated request to the user-orders endpoint is denied.
"""
url = reverse('user-orders') # Assuming you named your URL 'user-orders'
response = self.client.get(url)
# We expect to be forbidden from accessing this endpoint
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_authenticated_user_can_access_orders(self):
"""
Verify that an authenticated user can successfully access the endpoint.
"""
# Create a user and authenticate the client
user = User.objects.create_user(username='testuser', password='password123')
self.client.force_login(user)
url = reverse('user-orders')
response = self.client.get(url)
# We expect a successful response
self.assertEqual(response.status_code, status.HTTP_200_OK)
By writing these two simple tests, you create a safety net that guarantees your endpoint is, and remains, secure against unauthenticated access.
Test your understanding!
You are building an API for a public-facing blog. You have a single view PostListCreateAPIView that handles both listing posts (GET) and creating new posts (POST) at the endpoint /api/posts/.
The requirements are:
- Anyone, including anonymous visitors, should be able to see the list of posts.
- Only logged-in users should be able to create a new post.
Which single, built-in DRF permission class would you add to PostListCreateAPIView to achieve this?
Show answer
You would use permission_classes = [IsAuthenticatedOrReadOnly].
This class perfectly matches the requirements:
- For the
GETrequest (listing posts), it allows access to everyone, satisfying requirement #1. - For the
POSTrequest (creating a post), it checks if the user is authenticated, satisfying requirement #2.
Conclusion
In this lesson, you've mastered the most fundamental aspect of API authorization: securing endpoints based on a user's authentication status. This is a non-negotiable skill for building any real-world backend system.
Key Takeaways:
- Authentication identifies, Permissions authorize. They are separate but related concepts.
- DRF provides powerful, reusable, built-in permission classes like
IsAuthenticatedandIsAuthenticatedOrReadOnly. - You can secure an entire view simply by adding the
permission_classesattribute. - Understanding the difference between
401 Unauthorizedand403 Forbiddenhelps you debug and build better APIs. - Always write automated tests to verify that your security rules are correctly enforced for both unauthenticated and authenticated users.
Preview of the Next Lesson:
We've now covered how to check if a user is logged in. But what if you need to be more specific? In our next lesson, we will build on this foundation to secure endpoints based on a user's role or specific permissions, using tools like IsAdminUser and Django's group system.