DRF Authentication and Permissions
When building APIs with Django REST Framework (DRF), it’s crucial to implement proper authentication and permissions to secure your API and control access to resources. DRF provides a flexible and powerful system for handling authentication and permissions. In this section, we will explore the different authentication methods and permission classes available in DRF, and how to apply them to your API views.
Authentication
Section titled “Authentication”We can manually implement authentication by creating custom authentication classes, but djoser is a popular library that provides a set of views to handle user registration, login, logout, password reset, and more. It integrates well with DRF and can save you a lot of time when implementing authentication in your API.
Official documentation: https://djoser.readthedocs.io/en/latest/
Setup of Djoser
Section titled “Setup of Djoser”-
Install Djoser:
Terminal window uv add djoser djangorestframework-simplejwt -
Add Djoser to your
INSTALLED_APPSinsettings.py:INSTALLED_APPS = [# other apps'djoser','rest_framework_simplejwt',] -
Include Djoser’s URLs in your project’s
urls.py:from django.urls import path, includeurlpatterns = [# other urlspath('auth/', include('djoser.urls')),path('auth/', include('djoser.urls.jwt')), # for JWT authentication] -
Configure DRF to use JWT authentication in
settings.py:REST_FRAMEWORK = {'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework_simplejwt.authentication.JWTAuthentication',),} -
Configure Simple JWT settings in
settings.py:djangorestframework-simplejwtdocs: https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html
You can customize the token lifetime, rotation, and other settings as needed. Here’s an example configuration:
from datetime import timedeltaSIMPLE_JWT = {'AUTH_HEADER_TYPES': ('JWT',),"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),"REFRESH_TOKEN_LIFETIME": timedelta(days=1),"ROTATE_REFRESH_TOKENS": False,"BLACKLIST_AFTER_ROTATION": False,"UPDATE_LAST_LOGIN": False,} -
Run migrations to create the necessary database tables:
Terminal window uv run python manage.py makemigrationsuv run python manage.py migrate
Custom Serializers for Djoser
Section titled “Custom Serializers for Djoser”Djoser provides default serializers for user registration, login, and other authentication-related actions. However, you can customize these serializers to include additional fields or change the behavior of the authentication process. For example, you can create a custom serializer for user registration that includes additional fields like first_name and last_name:
# serializers.pyfrom djoser.serializers import UserCreateSerializer as BaseUserCreateSerializer, UserSerializer as BaseUserSerializer
class UserCreateSerializer(BaseUserCreateSerializer): class Meta(BaseUserCreateSerializer.Meta): fields = BaseUserCreateSerializer.Meta.fields + ('first_name', 'last_name')
class UserSerializer(BaseUserSerializer): class Meta(BaseUserSerializer.Meta): fields = BaseUserSerializer.Meta.fields + ('first_name', 'last_name')Then, you can tell Djoser to use your custom serializer in settings.py:
Docs: https://djoser.readthedocs.io/en/latest/settings.html#serializers
DJOSER = { 'SERIALIZERS': { 'user_create': 'your_app.serializers.UserCreateSerializer', 'current_user': 'your_app.serializers.UserSerializer', },}Example of User and Profile Models
# models.pyfrom django.db import modelsfrom django.contrib.auth.models import AbstractUser
class User(AbstractUser): # You can add additional fields here if neededpass
class Profile(models.Model):user = models.OneToOneField(User, on_delete=models.CASCADE)dob = models.DateField(null=True, blank=True)bio = models.TextField(null=True, blank=True)
def __str__(self): return f"{self.user.username}'s Profile"# serializers.pyfrom rest_framework import serializersfrom .models import User, Profile
# user serializer is created by djoser, we will create a profile serializer
class ProfileSerializer(serializers.ModelSerializer): class Meta: model = Profile fields = ['dob', 'bio'] # we dont need to include the user field here because # we will set it in the view using the request.user # and this api is only for the authenticated user
def create(self, validated_data): user = self.context['request'].user profile = Profile.objects.create(user=user, **validated_data) return profile# views.pyfrom rest_framework import viewsetsfrom .models import Profilefrom .serializers import ProfileSerializerfrom rest_framework.permissions import IsAuthenticated
class ProfileViewSet(viewsets.ModelViewSet): queryset = Profile.objects.all() serializer_class = ProfileSerializer permission_classes = [IsAuthenticated]
def get_queryset(self): # return only the profile of the authenticated user return self.queryset.filter(user=self.request.user)
def get_serializer_context(self): context = super().get_serializer_context() context['request'] = self.request return contextJWT also known as JSON Web Token is a compact, URL-safe means of representing claims to be transferred between two parties. It is commonly used for authentication and authorization in web applications. A JWT consists of three parts: a header, a payload, and a signature. The header typically contains the type of token and the signing algorithm used. The payload contains the claims, which are statements about an entity (typically, the user) and additional data. The signature is used to verify that the token has not been tampered with.
JWTs are often used in authentication systems to securely transmit user information between the client and the server. When a user logs in, the server generates a JWT containing the user’s information and sends it back to the client. The client then includes this token in the Authorization header of subsequent requests to access protected resources. The server can verify the token’s signature and extract the user’s information to determine if the request is authorized.
To understand the structure of a JWT, you can use online tools like jwt.io to decode and inspect the contents of a JWT token.
JWT Debugger allows you to paste a JWT token and see its header, payload, and signature in a human-readable format. This can help you understand what information is being stored in the token and how it is structured.
Djoser API Endpoints
Section titled “Djoser API Endpoints”Djoser provides a set of API endpoints for user authentication and management.
Docs: https://djoser.readthedocs.io/en/latest/getting_started.html#available-endpoints
/users/(POST for registration, GET for listing users)/users/me/(GET for current user details, PUT/PATCH for updating current user)/users/resend_activation/(POST for resending activation email)/users/set_password/(POST for setting password)/users/reset_password/(POST for resetting password)/users/reset_password_confirm/(POST for confirming password reset)/users/set_username/(POST for setting username)/users/reset_username/(POST for resetting username)/users/reset_username_confirm/(POST for confirming username reset)/token/login/(Token Based Authentication)/token/logout/(Token Based Authentication)/jwt/create/(JSON Web Token Authentication)/jwt/refresh/(JSON Web Token Authentication)/jwt/verify/(JSON Web Token Authentication)
Dojser Default API Endpoints in Detail
-
POST
/api/auth/jwt/create/Description: Login and obtain access + refresh tokens
-
Request
{"email": "user@example.com","password": "password"} -
Response
{"access": "access_token","refresh": "refresh_token"} -
Access
- Public
-
-
POST
/api/auth/jwt/refresh/Description: Get new access token
-
Request
{"refresh": "refresh_token"} -
Response
{"access": "new_access_token"} -
Access
- Public (valid refresh token required)
-
-
POST
/api/auth/jwt/verify/Description: Verify token validity
-
Request
{"token": "access_token"} -
Response
- 200 OK (valid)
- 401 Unauthorized (invalid)
-
Access
- Public
-
-
GET
/api/auth/users/Description: List all users
-
Response
[{"id": 1,"email": "user@example.com"}] -
Access
- Admin only (must restrict)
-
-
POST
/api/auth/users/Description: Register new user
-
Request
{"email": "user@example.com","password": "password"} -
Response
{"id": 1,"email": "user@example.com"} -
Access
- Public
-
-
GET
/api/auth/users/{id}/Description: Get user by ID
-
Response
{"id": 1,"email": "user@example.com"} -
Access
- Admin or Owner
-
-
PUT
/api/auth/users/{id}/Description: Fully update user
-
Request
{"email": "new@example.com"} -
Access
- Admin or Owner
-
-
PATCH
/api/auth/users/{id}/Description: Partially update user
-
Request
{"email": "updated@example.com"} -
Access
- Admin or Owner
-
-
DELETE
/api/auth/users/{id}/Description: Delete user
-
Response
- 204 No Content
-
Access
- Admin or Owner
-
-
POST
/api/auth/users/activation/Description: Activate account via email token
-
Request
{"uid": "encoded_user_id","token": "activation_token"} -
Response
- 204 No Content
-
Access
- Public (token-based)
-
-
GET
/api/auth/users/me/Description: Get current logged-in user
-
Headers
Authorization: Bearer <access_token> -
Response
{"id": 1,"email": "user@example.com"} -
Access
- Authenticated user
-
-
PUT
/api/auth/users/me/Description: Fully update current user
-
Request
{"email": "new@example.com"} -
Access
- Authenticated user
-
-
PATCH
/api/auth/users/me/Description: Partially update current user
-
Request
{"email": "updated@example.com"} -
Access
- Authenticated user
-
-
DELETE
/api/auth/users/me/Description: Delete own account
-
Response
- 204 No Content
-
Access
- Authenticated user
-
-
POST
/api/auth/users/resend_activation/Description: Resend activation email
-
Request
{"email": "user@example.com"} -
Response
- 204 No Content
-
Access
- Public
-
-
POST
/api/auth/users/reset_email/Description: Request email change
-
Request
{"email": "new@example.com"} -
Response
- 204 No Content
-
Access
- Authenticated user
-
-
POST
/api/auth/users/reset_email_confirm/Description: Confirm email reset
-
Request
{"uid": "encoded_user_id","token": "email_reset_token"} -
Response
- 204 No Content
-
Access
- Public (token-based)
-
-
POST
/api/auth/users/reset_password/Description: Request password reset
-
Request
{"email": "user@example.com"} -
Response
- 204 No Content
-
Access
- Public
-
-
POST
/api/auth/users/reset_password_confirm/Description: Confirm password reset
-
Request
{"uid": "encoded_user_id","token": "reset_token","new_password": "newpassword"} -
Response
- 204 No Content
-
Access
- Public (token-based)
-
-
POST
/api/auth/users/set_email/Description: Change email (logged-in user)
-
Request
{"email": "new@example.com"} -
Access
- Authenticated user
-
-
POST
/api/auth/users/set_password/Description: Change password (logged-in user)
-
Request
{"current_password": "oldpassword","new_password": "newpassword"} -
Response
- 204 No Content
-
Access
- Authenticated user
-
Reference URLs for below examples
# urls.pyfrom django.urls import path, includeurlpatterns = [ # other urls path('auth/', include('djoser.urls')), path('auth/', include('djoser.urls.jwt')), # for JWT authentication]Registering Users
Section titled “Registering Users”First, we will register a user using the /users/ endpoint. We can send a POST request with the user’s information to create a new user account.
curl -X POST http://localhost:8000/auth/users/ \-H "Content-Type: application/json" \-d '{ "username": "john_doe", "email": "john_doe@example.com", "password": "secure_password"}'This will create a new user with the username john_doe, email john_doe@example.com, and password secure_password.
Logging In
Section titled “Logging In”To log in, we can use the /jwt/create/ endpoint to obtain a JWT token. We will send a POST request with the user’s credentials.
curl -X POST http://localhost:8000/auth/jwt/create/ \-H "Content-Type: application/json" \-d '{ "username": "john_doe", "password": "secure_password"}'This will return a JSON response containing the access token and refresh token:
{ "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}accesstoken : This token is used to authenticate subsequent requests to protected endpoints. It typically has a short expiration time (e.g., 5 minutes) for security reasons.refreshtoken : This token is used to obtain a new access token when the current access token expires. It usually has a longer expiration time (e.g., 1 day) and can be used to maintain a user’s session without requiring them to log in again.
New Access Token using Refresh Token
Section titled “New Access Token using Refresh Token”When the access token expires, you can use the refresh token to obtain a new access token without requiring the user to log in again. You can do this by sending a POST request to the /jwt/refresh/ endpoint with the refresh token.
curl -X POST http://localhost:8000/auth/jwt/refresh/ \-H "Content-Type: application/json" \-d '{ "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}'This will return a new access token:
{ "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}Get Current User’s Profile
Section titled “Get Current User’s Profile”To get the current user’s profile, you can send a GET request to the /users/me/ endpoint with the access token in the Authorization header.
curl -X GET http://localhost:8000/auth/users/me/ \-H "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiZXhwIjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" \-H "Content-Type: application/json"If you are using Browser then use Browser Extension like ModHeader to set the Authorization header with the JWT token for testing the API endpoints. ModHeader for Chrome
Custom Actions
Section titled “Custom Actions”we can also create custom actions in our viewsets to handle specific functionality that is not covered by the standard CRUD operations. For example, we can create a custom action to allow users to change their password.
Reference code for custom action
# models.pyfrom django.db import modelsfrom django.contrib.auth import get_user_model
User = get_user_model()
class Customer(models.Model):user = models.OneToOneField(User, on_delete=models.CASCADE) # other fields
def __str__(self) -> str: return self.user.username# serializers.pyfrom rest_framework import serializersfrom .models import Customer
class CustomerSerializer(serializers.ModelSerializer): class Meta: model = Customer fields = [ 'user', # other fields ]# views.pyfrom rest_framework import viewsetsfrom .models import Customer
class CustomerViewSet(viewsets.ModelViewSet): queryset = Customer.objects.all() serializer_class = CustomerSerializer# urls.pyfrom django.urls import path, includefrom rest_framework.routers import DefaultRouterfrom .views import CustomerViewSet
router = DefaultRouter()router.register(r'customers', CustomerViewSet)
urlpatterns = [ path('', include(router.urls)),]Now think we want to create API endpoint like /customers/me/ to allow authenticated users to get their own customer profile. We can achieve this by creating a custom action in our CustomerViewSet:
from rest_framework.decorators import actionfrom rest_framework.permissions import IsAuthenticatedfrom rest_framework import viewsetsfrom .models import Customerfrom rest_framework.response import Response
class CustomerViewSet(viewsets.ModelViewSet): queryset = Customer.objects.all() serializer_class = CustomerSerializer
@action(detail=False, methods=['GET', 'PUT'], permission_classes=[IsAuthenticated]) def me(self, request): customer, _ = Customer.objects.get_or_create(user=request.user) if request.method == 'GET': serializer = self.get_serializer(customer) return Response(serializer.data) elif request.method == 'PUT': serializer = self.get_serializer(customer, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data)Let’s break down the code for the custom action:
-
detail=Falseindicates that this action is not for a specific instance (i.e., it does not require a primary key in the URL). if we setdetail=Truethen the URL for this action would be/customers/{pk}/me/which is not what we want in this case. -
methods=['GET', 'PUT']specifies that this action can handle both GET and PUT requests. GET will be used to retrieve the customer’s profile, while PUT will be used to update it. -
permission_classes=[IsAuthenticated]ensures that only authenticated users can access this endpoint. You can set different permission classes on action and the parent viewset can have its own permission classes as well. The permissions for the action will override the permissions for the viewset when accessing that specific action.
Permissions
Section titled “Permissions”Permissions decide who is allowed to do what in your API.
In Django REST Framework (DRF), you can use permission classes to answer questions like:
- Is the user logged in?
- Is the user an admin?
- Is the user allowed to edit this specific object?
This helps protect your API from unauthorized access.
There are several built-in permission classes in DRF, including:
AllowAny: Anyone can access the endpoint, even without login.IsAuthenticated: Only logged-in users can access.IsAdminUser: Only admin/staff users can access.IsAuthenticatedOrReadOnly: Anyone can read, but only logged-in users can create/update/delete.DjangoModelPermissions: Uses Django’s model permissions (add,change,delete, and optionallyview).DjangoObjectPermissions: Uses object-level permissions (permission per object).Custom permissions: You can build your own permission rules by extendingBasePermission.
Setting Global Permissions
Section titled “Setting Global Permissions”Global permissions are the default rules for your whole API. Set them once in settings.py.
These defaults apply to every view unless you override them on a specific view.
REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', # or 'rest_framework.permissions.IsAdminUser' # or 'rest_framework.permissions.AllowAny' # or any other permission class ],}In the example above, every endpoint requires authentication by default.
Setting Permissions at the View Level
Section titled “Setting Permissions at the View Level”If one endpoint needs different rules, set permission_classes on that view/viewset.
This overrides the global default for that specific view.
from rest_framework.permissions import IsAuthenticatedfrom rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer # Only authenticated users can access this view permission_classes = [IsAuthenticated]Different Permission for Different HTTP Methods
Section titled “Different Permission for Different HTTP Methods”Sometimes you want different rules in the same endpoint, for example:
- Anyone can read products (
GET) - Only logged-in users can modify products (
POST,PUT,PATCH,DELETE)
You can do this by overriding get_permissions().
from rest_framework.permissions import IsAuthenticated, AllowAnyfrom rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer
def get_permissions(self): if self.request.method in ['POST', 'PUT', 'PATCH', 'DELETE']: return [IsAuthenticated()] return [AllowAny()]This is a common beginner-friendly pattern for public read + protected write APIs.
Creating Custom Permissions
Section titled “Creating Custom Permissions”When built-in permissions are not enough, create your own class.
You can implement:
has_permission()for general view-level checkshas_object_permission()for object-level checks
# permissions.pyfrom rest_framework import permissionsfrom rest_framework.permissions import BasePermission
class IsOwner(BasePermission): def has_object_permission(self, request, view, obj): return obj.owner == request.user
class IsAdminOrReadOnly(BasePermission): def has_permission(self, request, view): if request.method in permissions.SAFE_METHODS: return True return request.user and request.user.is_staffHere we created two custom permissions:
IsOwner: only the owner of the object can access it.IsAdminOrReadOnly: everyone can read, but only staff/admin can write.
Model Permissions
Section titled “Model Permissions”DRF can also use Django’s built-in model permissions.
Django Model Permissions
Section titled “Django Model Permissions”DjangoModelPermissions checks a user’s Django model permissions before allowing write actions.
In simple words, this means DRF asks: “Does this user have permission to do this action on this model?”
Common permission checks are:
addchangedelete- (and
view, if you configure GET checks) - It requires authentication for all requests, even
GET
This is useful when you already manage permissions through Django admin, users, and groups.
from rest_framework.permissions import DjangoModelPermissions
class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer permission_classes = [DjangoModelPermissions]Django Model Permissions Or Anon Read Only
Section titled “Django Model Permissions Or Anon Read Only”Use DjangoModelPermissionsOrAnonReadOnly when you want:
- Anonymous users: read-only access
- Authenticated users: model-permission checks for write actions
This permission class is already built into DRF, so you can use it directly.
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnlyfrom rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer permission_classes = [DjangoModelPermissionsOrAnonReadOnly]Custom Model Permissions
Section titled “Custom Model Permissions”Use this flow when you want fully custom access rules:
- Create custom permissions in your model
Metaclass. - Run migrations.
- Assign those permissions to users/groups.
- Use a custom DRF permission class in your view.
1. Create Permission in Model (Meta class)
Section titled “1. Create Permission in Model (Meta class)”# models.pyfrom django.db import models
class Product(models.Model): name = models.CharField(max_length=100) description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=2)
class Meta: permissions = [ ('can_publish_product', 'Can publish product'), ]2. Run Migrations
Section titled “2. Run Migrations”uv run python manage.py makemigrationsuv run python manage.py migrate3. Create Group and Assign Permissions to Users
Section titled “3. Create Group and Assign Permissions to Users”You can do this in two beginner-friendly ways.
Admin panel way:
- Open Django admin and go to
Groups. - Create a group (example:
ProductManagers). - Add permissions to that group (example:
view_product,change_product,can_publish_product). - Open a user and add that user to the group.
Shell way:
from django.contrib.auth.models import Group, Permissionfrom django.contrib.auth import get_user_model
User = get_user_model()
# 1) Create or get groupgroup, _ = Group.objects.get_or_create(name='ProductManagers')
# 2) Add permissions to groupgroup.permissions.add( Permission.objects.get(codename='view_product'), Permission.objects.get(codename='change_product'), Permission.objects.get(codename='can_publish_product'),)
# 3) Add user to groupuser = User.objects.get(username='john_doe')user.groups.add(group)4. Create a Custom DRF Permission Class
Section titled “4. Create a Custom DRF Permission Class”# permissions.pyfrom rest_framework.permissions import BasePermission
class CanPublishProduct(BasePermission): message = 'You do not have permission to publish products.'
def has_permission(self, request, view): if not request.user or not request.user.is_authenticated: return False # Format: app_label.permission_codename return request.user.has_perm('store.can_publish_product')Use it in your viewset:
from rest_framework import viewsetsfrom .permissions import CanPublishProduct
class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer permission_classes = [CanPublishProduct]Control Existing Permissions by HTTP Method
Section titled “Control Existing Permissions by HTTP Method”Use this pattern when you want to reuse Django’s existing model permissions (view, add, change, delete) and apply them per HTTP method.
Simple idea:
GET/HEAD/OPTIONS->view_productPOST->add_productPUT/PATCH->change_productDELETE->delete_product
This gives you fine-grained control without creating new custom permission codenames.
# permissions.py
# it is a generic permission but you can customize the perms_map to fit your needsfrom rest_framework import permissions
class ViewProductPermission(permissions.BasePermission): def has_permission(self, request, view): if not request.user or not request.user.is_authenticated: return False
perms_by_method = { 'GET': 'store.view_product', 'HEAD': 'store.view_product', 'OPTIONS': 'store.view_product', 'POST': 'store.add_product', 'PUT': 'store.change_product', 'PATCH': 'store.change_product', 'DELETE': 'store.delete_product', }
required_perm = perms_by_method.get(request.method) if not required_perm: return False
# Format: app_label.permission_codename return request.user.has_perm(required_perm)