Session Authentication#

This is the 28th article in the Django Ninja tutorial series.
Welcome to Chapter 7! This chapter has two articles:
- Article 28: Authentication — Session Authentication and Global Authentication
- Article 29: Unit Testing — Testing APIs with Test Client and pytest
The core functions of these topics are not implemented by Django Ninja, but the framework still provides a certain level of integration. Moreover, these features are crucial for any Django project.
This article introduces a requirement in almost every API project: Authentication.
We will explore how to leverage Django's built-in session-based authentication in Django Ninja to implement complete login verification, and further explain how to set up global authentication to reduce code duplication.
For all code changes in this article, please refer to this PR.
GitHub Example Project#
The Two Levels of Authentication#
Before entering implementation, we need to understand what authentication actually means.
Using "username/password + session authentication" as an example, the scope of authentication mainly covers two stages.
First, when a user logs in with a username and password, the system checks these inputs and confirms the identity is valid. After a successful login, the system saves the user information (such as user ID) to the session to maintain the login state.
This is authentication at login, which is what we most commonly refer to as authentication. (Narrow-sense authentication)
Second, when the user tries to access APIs protected by "authentication protection", the system checks the session and verifies the identity, ensuring that each API request comes from a legitimately logged-in user.
In short:
- Stage 1: Identity verification during the initial login.
- Stage 2: Identity verification during subsequent requests.
These two levels complement each other, ensuring that the service can provide appropriate security protection during user login and subsequent operations.
Implementing the "User Login" API#
After understanding the two levels above, we will first implement the "narrow-sense" authentication—which is the login verification itself.
We will create a "User Login" API and directly use Django's built-in authenticate and login functions to handle username/password verification and login state—very convenient!
authenticate is used to verify whether the username and password entered by the user are correct, and login saves the user's login state to the session.
Code Implementation#
First, add a login request Schema:
# user/schemas.py
class LoginRequest(Schema):
username: str = Field(examples=['Alice'])
password: str = Field(examples=['password123'])
Then the view function:
from django.contrib.auth import authenticate, login
from user.schemas import CreateUserRequest, LoginRequest
...
@router.post('/users/login/', summary='User Login')
def login_user(
request: HttpRequest, payload: LoginRequest
) -> dict[str, str]:
"""
Login user
"""
user = authenticate(
request,
username=payload.username,
password=payload.password
)
if user is not None:
login(request, user) # Save user login state to session
return {'message': 'Login successful'}
else:
raise HttpError(401, 'Incorrect username or password')
Very simple!
By the way, I don't like "unnecessary" else statements in the code. The writing style at this point is still not ideal because the else can be completely omitted.
In the latest code, you can see that I have changed it to:
user = authenticate(...)
if user is None:
raise HttpError(401, 'Incorrect username or password')
login(request, user) # Save user login state to session
return {'message': 'Login successful'}
This practice is the so-called Guard Clause or Early Return (although it is a raise here).
Brief Review and Important Additions#
The way authenticate and login are used is consistent and easy to understand:
authenticatereturns the correspondingUserobject upon successful verification, andNoneon failure.loginreturns nothing, butrequestanduserare required parameters.
After a successful login, you will get a 200 response and two cookies:

This matters when using an API client such as Postman. Browsers store cookies automatically, whereas API clients might not—although the RapidAPI client I use does store and send them automatically!
(I was wondering why the authentication protection failed during my API tests 🤣)
If your tool does not do this for you, remember to add them manually to the request headers:
authenticate defaults to using the username and password fields of AbstractUser as the verification criteria. If you want to use other fields, such as email, you must override Django's authentication backend yourself.
Adding "Authentication Protection" to APIs#
Once the login function is completed, the next step is to add authentication protection to APIs that "require login to access", using django_auth provided by Django Ninja—this is designed specifically for Django's built-in session authentication.
Let's use the "Upload Avatar" API as an example:
from ninja.security import django_auth
...
@router.post(
path='/users/{int:user_id}/avatar/',
summary='Upload Avatar',
auth=django_auth # Add this parameter
)
In this example, auth=django_auth ensures that only "logged-in users" can access this API, otherwise they will get a 401 or 403 response.
request.auth in Django Ninja#
But you might think:
Just verifying "logged in" isn't enough, right?
"Upload Avatar" should only upload for "oneself"; you shouldn't be able to upload avatars for "others"!
Exactly. Therefore, we need another layer of verification inside the view function.
request.user in Django#
In a traditional Django project, we obtain current user information via the first parameter of the function—request, using request.user. For example: (refer to the documentation)
if request.user.is_authenticated:
# Do something for authenticated users.
...
else:
# Do something for anonymous users.
...
Specifically:
- When the user is logged in,
request.useris aUserinstance, representing the currently logged-in user. - When not logged in,
request.useris anAnonymousUserinstance, representing the unauthenticated user.
When the user is logged in, we can check properties of request.user, such as request.user.id, to confirm whether they are "themselves".
request.auth in Django Ninja#
However, in Django Ninja, you need to use request.auth provided by the framework. The implementation is as follows:
...
def upload_avatar(...) -> dict[str, str]:
"""
Upload avatar
"""
# Check if the logged-in user is "themself"
if request.auth.id != user_id:
raise HttpError(403, 'You cannot upload an avatar for another user')
...
Let's test this. Log in and call this API with someone else's ID in the URL path:
Very good!
Understanding request.auth#
Although we use request.auth here instead of request.user, the underlying meaning is very different.
In Django Ninja, request.auth represents the result returned by the authentication process. In addition, Django Ninja allows you to customize authentication methods, so the content of request.auth is not fixed.
Let's look into it.
Authentication Results#
request.auth contains the value returned by the current authentication method.
- This value can be of any type, depending on how you implement the authentication logic.
- This gives developers great flexibility. It can be a
Userobject, a string, a Python dictionary, etc.
Authentication Methods and Common Use Cases#
- When using Django's session authentication,
request.authis Django'sUserobject. - For API key authentication,
request.authmight be the API key itself or related information. - In JWT authentication,
request.authmight contain decoded token information.
In short, just remember that if you want to get authentication information inside the view function, do it through request.auth.
Now that the authentication has been implemented, we can make things a bit "simpler".
Configuring Global Authentication and Exceptions#
Configuring authentication protection for each API one by one feels a bit tedious—especially when there are many APIs.
To address this, Django Ninja supports Global Authentication, protecting all APIs by default. Developers then need only opt out on Routes that should remain public.
The implementation is very simple. Django Ninja directly provides the SessionAuth authentication class to handle global session-based authentication.
Implementing Global Authentication: Using SessionAuth#
Add the following to the project's api.py:
# NinjaForum/api.py
from ninja.security import SessionAuth
...
api = NinjaAPI(
auth=SessionAuth(), # Set global authentication
...
)
By doing this, all APIs have authentication protection by default. You can exclude it in specific APIs, such as "Login User":
In the route decorator, set auth to None to remove authentication protection.
Testing Authentication Protection#
Let's test an "authentication-protected" API. You will find that when unauthenticated, trying different HTTP methods on the API will yield different error responses:
- GET: 401 Unauthorized
- POST: 403 Forbidden
That's why we mentioned earlier that you will get a "401 or 403" response.
Testing the "Get All Users" API#
In our project design, only logged-in users can access the "Get All Users" API.
When unauthenticated, you will get a 401 response:
Testing the "Create Post" API#
Unauthenticated users also cannot access the "Create Post" API—which is obviously very reasonable, otherwise the post would have no author 😅
You will get a 403 response:
You might think: "Strange? Why is it CSRF check Failed?"
This is Django's CSRF protection mechanism. Because our API uses the POST method, Django automatically checks for the CSRF token. Since we did not provide a CSRF token, this error occurs.
Summary and Next Steps#
In this article, we explored the integration of Django's session authentication with Django Ninja, implemented the "User Login" API, and added authentication protection to other APIs. Finally, we demonstrated how to implement global authentication to make the entire workflow simpler.
For the final practice of this series, we will write tests for our project!
The next article will discuss how to use test client and pytest to write unit tests for our Django APIs. This not only helps us verify existing features but also provides an extra layer of security for future development and refactoring.