Skip to main content

Django: Authentication and Permissions in Django REST Framework (DRF)

Django: Authentication and Permissions in Django REST Framework (DRF)

Authentication and permissions in Django REST Framework (DRF) are critical for securing RESTful APIs, ensuring that only authorized users can access or modify resources. Integrated with Django’s Model-View-Template (MVT) architecture, DRF provides robust mechanisms for user authentication (verifying identity) and permissions (controlling access). This tutorial explores authentication and permissions in DRF, covering setup, configuration, and practical applications for securing APIs in applications like blogs, e-commerce platforms, or dashboards.


01. Why Authentication and Permissions in DRF?

Authentication identifies who a user is (e.g., via tokens, sessions), while permissions determine what they can do (e.g., read, write). DRF’s flexible authentication and permission systems protect sensitive data and restrict actions, making them essential for secure API development. For example, a blog API might allow anyone to read posts but restrict post creation to authenticated authors.

Example: Basic DRF Authentication Setup

# Install DRF and JWT
pip install djangorestframework djangorestframework-simplejwt
# myproject/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt',  # JWT authentication
    'myapp',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('myapp.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Output:

JWT authentication configured; token endpoints at http://127.0.0.1:8000/api/token/.

Explanation:

  • rest_framework_simplejwt - Enables JSON Web Token (JWT) authentication.
  • DEFAULT_AUTHENTICATION_CLASSES - Sets JWT as the default authentication method.
  • DEFAULT_PERMISSION_CLASSES - Restricts access to authenticated users by default.

02. Key Authentication and Permission Concepts

DRF provides a variety of authentication and permission classes to secure APIs. The table below summarizes key concepts and their roles:

Concept Description Use Case
Authentication Verifies user identity Token-based access, session login
Permission Controls access to resources Restrict CRUD operations
JWT Authentication Uses tokens for stateless authentication Secure API for mobile/web clients
Custom Permissions Define specific access rules Role-based or object-level access


2.1 JWT Authentication

Example: JWT-Protected API

# myapp/models.py
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

# myapp/serializers.py
from rest_framework import serializers
from .models import Article

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ['id', 'title', 'content', 'author', 'created_at']
        read_only_fields = ['author', 'created_at']

# myapp/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticated]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet

router = DefaultRouter()
router.register(r'articles', ArticleViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Output:

API at http://127.0.0.1:8000/api/articles/ requires JWT token for access.

Explanation:

  • IsAuthenticated - Ensures only users with valid JWT tokens can access the API.
  • perform_create - Sets the article’s author to the authenticated user.
  • Token obtained via /api/token/ must be included in the Authorization: Bearer header.

2.2 Built-in Permission Classes

Example: Read-Only for Unauthenticated Users

# myapp/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

Output:

GET http://127.0.0.1:8000/api/articles/ allowed for all; POST requires JWT token.

Explanation:

  • IsAuthenticatedOrReadOnly - Allows unauthenticated users to read (GET) but requires authentication for write actions (POST, PUT, DELETE).
  • Ideal for public-facing APIs with restricted modifications.

2.3 Custom Permission Classes

Example: Author-Only Edit Permission

# myapp/permissions.py
from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        # Allow GET, HEAD, OPTIONS for all
        if request.method in permissions.SAFE_METHODS:
            return True
        # Allow write actions only if user is the author
        return obj.author == request.user

# myapp/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Article
from .serializers import ArticleSerializer
from .permissions import IsAuthorOrReadOnly

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

Output:

Only article authors can edit/delete at http://127.0.0.1:8000/api/articles/{id}/.

Explanation:

  • IsAuthorOrReadOnly - Custom permission checks if the user is the article’s author for write actions.
  • has_object_permission - Applies per-object access control.
  • Combined with IsAuthenticated to ensure only authenticated users can attempt writes.

2.4 Combining Authentication and Permissions

Example: Secure Task API

# myapp/models.py
from django.db import models

class Task(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

# myapp/serializers.py
from rest_framework import serializers
from .models import Task

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ['id', 'title', 'description', 'owner', 'created_at']
        read_only_fields = ['owner', 'created_at']

# myapp/permissions.py
from rest_framework import permissions

class IsOwner(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user

# myapp/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Task
from .serializers import TaskSerializer
from .permissions import IsOwner

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated, IsOwner]

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

    def get_queryset(self):
        # Only return tasks owned by the user
        return Task.objects.filter(owner=self.request.user)

# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

router = DefaultRouter()
router.register(r'tasks', TaskViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Output:

Tasks at http://127.0.0.1:8000/api/tasks/ accessible only to their owners with JWT token.

Explanation:

  • IsAuthenticated - Requires JWT token.
  • IsOwner - Ensures users can only access their own tasks.
  • get_queryset - Filters tasks to the authenticated user.

2.5 Incorrect Authentication/Permission Usage

Example: Unprotected API

# myapp/views.py (Incorrect)
from rest_framework import viewsets
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    # No permission_classes defined

Output:

Anyone can access http://127.0.0.1:8000/api/articles/, risking unauthorized actions.

Explanation:

  • Omitting permission_classes allows unrestricted access.
  • Solution: Add IsAuthenticated or custom permissions.

03. Effective Usage

3.1 Recommended Practices

  • Use JWT authentication for stateless APIs, combine with permission classes, and implement custom permissions for fine-grained control.

Example: Comprehensive Secure API

# myproject/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt',
    'myapp',
]
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

# myapp/models.py
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

# myapp/serializers.py
from rest_framework import serializers
from .models import Article

class ArticleSerializer(serializers.ModelSerializer):
    author_username = serializers.ReadOnlyField(source='author.username')

    class Meta:
        model = Article
        fields = ['id', 'title', 'content', 'author', 'author_username', 'created_at']
        read_only_fields = ['author', 'created_at']

# myapp/permissions.py
from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.author == request.user

# myapp/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Article
from .serializers import ArticleSerializer
from .permissions import IsAuthorOrReadOnly

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet

router = DefaultRouter()
router.register(r'articles', ArticleViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('myapp.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Output:

Secure API at http://127.0.0.1:8000/api/articles/ with JWT and author-only edits.
  • JWT ensures stateless authentication.
  • IsAuthenticatedOrReadOnly - Allows public reads.
  • IsAuthorOrReadOnly - Restricts edits to authors.

3.2 Practices to Avoid

  • Avoid unprotected endpoints or overly permissive permissions.

Example: Overly Permissive Permission

# myapp/views.py (Incorrect)
from rest_framework import viewsets
from rest_framework.permissions import AllowAny
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [AllowAny]  # Incorrect: No restrictions

Output:

Anyone can modify articles, risking data integrity.
  • AllowAny - Removes all access controls.
  • Solution: Use IsAuthenticated or custom permissions.

04. Common Use Cases

4.1 Secure Blog API

Allow public reads but restrict writes to authenticated authors.

Example: Blog API

# myapp/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Article
from .serializers import ArticleSerializer
from .permissions import IsAuthorOrReadOnly

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

Output:

Public reads, author-only writes at http://127.0.0.1:8000/api/articles/.

Explanation:

  • Combines permissions for flexible access control.
  • JWT ensures secure authentication.

4.2 Task Management API

Restrict task access to their owners with JWT authentication.

Example: Task API

# myapp/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Task
from .serializers import TaskSerializer
from .permissions import IsOwner

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated, IsOwner]

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

    def get_queryset(self):
        return Task.objects.filter(owner=self.request.user)

Output:

User-specific tasks at http://127.0.0.1:8000/api/tasks/ with JWT token.

Explanation:

  • IsOwner - Ensures object-level access control.
  • Queryset filtering enhances security.

Conclusion

DRF’s authentication and permission systems, integrated with Django’s Model-View-Template architecture, provide a secure framework for API development. Key takeaways:

  • Use JWT for stateless authentication.
  • Apply built-in permissions like IsAuthenticatedOrReadOnly.
  • Create custom permissions for fine-grained control.
  • Avoid unprotected endpoints or permissive settings.

With DRF authentication and permissions, you can build secure, role-based APIs for modern applications!

Comments