Skip to content

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.

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/

  1. Install Djoser:

    Terminal window
    uv add djoser djangorestframework-simplejwt
  2. Add Djoser to your INSTALLED_APPS in settings.py:

    INSTALLED_APPS = [
    # other apps
    'djoser',
    'rest_framework_simplejwt',
    ]
  3. Include Djoser’s URLs in your project’s urls.py:

    from django.urls import path, include
    urlpatterns = [
    # other urls
    path('auth/', include('djoser.urls')),
    path('auth/', include('djoser.urls.jwt')), # for JWT authentication
    ]
  4. Configure DRF to use JWT authentication in settings.py:

    REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    }
  5. Configure Simple JWT settings in settings.py:

    You can customize the token lifetime, rotation, and other settings as needed. Here’s an example configuration:

    from datetime import timedelta
    SIMPLE_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,
    }
  6. Run migrations to create the necessary database tables:

    Terminal window
    uv run python manage.py makemigrations
    uv run python manage.py migrate

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.py
from 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.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser): # You can add additional fields here if needed
pass
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.py
from rest_framework import serializers
from .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.py
from rest_framework import viewsets
from .models import Profile
from .serializers import ProfileSerializer
from 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 context

JWT 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 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
  1. 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
  2. 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)
  3. POST /api/auth/jwt/verify/

    Description: Verify token validity

    • Request

      {
      "token": "access_token"
      }
    • Response

      • 200 OK (valid)
      • 401 Unauthorized (invalid)
    • Access

      • Public
  4. GET /api/auth/users/

    Description: List all users

    • Response

      [
      {
      "id": 1,
      "email": "user@example.com"
      }
      ]
    • Access

      • Admin only (must restrict)
  5. POST /api/auth/users/

    Description: Register new user

    • Request

      {
      "email": "user@example.com",
      "password": "password"
      }
    • Response

      {
      "id": 1,
      "email": "user@example.com"
      }
    • Access

      • Public
  6. GET /api/auth/users/{id}/

    Description: Get user by ID

    • Response

      {
      "id": 1,
      "email": "user@example.com"
      }
    • Access

      • Admin or Owner
  7. PUT /api/auth/users/{id}/

    Description: Fully update user

    • Request

      {
      "email": "new@example.com"
      }
    • Access

      • Admin or Owner
  8. PATCH /api/auth/users/{id}/

    Description: Partially update user

    • Request

      {
      "email": "updated@example.com"
      }
    • Access

      • Admin or Owner
  9. DELETE /api/auth/users/{id}/

    Description: Delete user

    • Response

      • 204 No Content
    • Access

      • Admin or Owner
  10. 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)
  11. 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
  12. PUT /api/auth/users/me/

    Description: Fully update current user

    • Request

      {
      "email": "new@example.com"
      }
    • Access

      • Authenticated user
  13. PATCH /api/auth/users/me/

    Description: Partially update current user

    • Request

      {
      "email": "updated@example.com"
      }
    • Access

      • Authenticated user
  14. DELETE /api/auth/users/me/

    Description: Delete own account

    • Response

      • 204 No Content
    • Access

      • Authenticated user
  15. POST /api/auth/users/resend_activation/

    Description: Resend activation email

    • Request

      {
      "email": "user@example.com"
      }
    • Response

      • 204 No Content
    • Access

      • Public
  16. POST /api/auth/users/reset_email/

    Description: Request email change

    • Request

      {
      "email": "new@example.com"
      }
    • Response

      • 204 No Content
    • Access

      • Authenticated user
  17. 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)
  18. POST /api/auth/users/reset_password/

    Description: Request password reset

    • Request

      {
      "email": "user@example.com"
      }
    • Response

      • 204 No Content
    • Access

      • Public
  19. 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)
  20. POST /api/auth/users/set_email/

    Description: Change email (logged-in user)

    • Request

      {
      "email": "new@example.com"
      }
    • Access

      • Authenticated user
  21. 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.py
from django.urls import path, include
urlpatterns = [
# other urls
path('auth/', include('djoser.urls')),
path('auth/', include('djoser.urls.jwt')), # for JWT authentication
]

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.

Terminal window
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.

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.

Terminal window
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"
}
  • access token : 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.
  • refresh token : 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.

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.

Terminal window
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"
}

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.

Terminal window
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

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.py
from django.db import models
from 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.py
from rest_framework import serializers
from .models import Customer
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = [
'user',
# other fields
]
# views.py
from rest_framework import viewsets
from .models import Customer
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .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 action
from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets
from .models import Customer
from 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=False indicates that this action is not for a specific instance (i.e., it does not require a primary key in the URL). if we set detail=True then 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 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 optionally view).
  • DjangoObjectPermissions: Uses object-level permissions (permission per object).
  • Custom permissions: You can build your own permission rules by extending BasePermission.

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.

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 IsAuthenticated
from 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, AllowAny
from 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.

When built-in permissions are not enough, create your own class.

You can implement:

  • has_permission() for general view-level checks
  • has_object_permission() for object-level checks
# permissions.py
from rest_framework import permissions
from 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_staff

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

DRF can also use Django’s built-in 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:

  • add
  • change
  • delete
  • (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 DjangoModelPermissionsOrAnonReadOnly
from rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]

Use this flow when you want fully custom access rules:

  1. Create custom permissions in your model Meta class.
  2. Run migrations.
  3. Assign those permissions to users/groups.
  4. 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.py
from 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'),
]
Terminal window
uv run python manage.py makemigrations
uv run python manage.py migrate

3. 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:

  1. Open Django admin and go to Groups.
  2. Create a group (example: ProductManagers).
  3. Add permissions to that group (example: view_product, change_product, can_publish_product).
  4. Open a user and add that user to the group.

Shell way:

from django.contrib.auth.models import Group, Permission
from django.contrib.auth import get_user_model
User = get_user_model()
# 1) Create or get group
group, _ = Group.objects.get_or_create(name='ProductManagers')
# 2) Add permissions to group
group.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 group
user = User.objects.get(username='john_doe')
user.groups.add(group)
# permissions.py
from 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 viewsets
from .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_product
  • POST -> add_product
  • PUT/PATCH -> change_product
  • DELETE -> 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 needs
from 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)