Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 43

API Security: Authentication Basics

Recommended resources

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.

Permissions

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."

Django REST Framework Browsable API with Authenticated User
This image shows what a successful request looks like for an authenticated user. After logging in (notice 'userbaru' in the top right), the user can access the endpoint and receives a `200 OK` response with the requested data. This is the outcome we are aiming for after applying our permission correctly.

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.

Permissions

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-Authenticate header 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:

  1. An unauthenticated user is blocked (receives a 403 or 401).
  2. 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:

  1. Anyone, including anonymous visitors, should be able to see the list of posts.
  2. 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 GET request (listing posts), it allows access to everyone, satisfying requirement #1.
  • For the POST request (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 IsAuthenticated and IsAuthenticatedOrReadOnly.
  • You can secure an entire view simply by adding the permission_classes attribute.
  • Understanding the difference between 401 Unauthorized and 403 Forbidden helps 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.