Django Authentication and Permissions

Django's built-in auth, @login_required, and DRF permission classes with a complete example.

Django's built-in authentication

Django ships with a full authentication system in django.contrib.auth — a User model, session-based login, and view decorators/mixins for protecting a view — already installed in INSTALLED_APPS in a fresh project.

Python
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import redirect, render

def login_view(request):
    if request.method == "POST":
        user = authenticate(
            request,
            username=request.POST["username"],
            password=request.POST["password"],
        )
        if user is not None:
            login(request, user)   # attaches the user to request.session
            return redirect("post_list")
        return render(request, "blog/login.html", {"error": "Invalid credentials"})
    return render(request, "blog/login.html")

def logout_view(request):
    logout(request)
    return redirect("post_list")

authenticate() checks a username/password pair against the configured authentication backend (by default, the User model and its hashed password) and returns a User instance or None. login() is what actually creates the session — it's a separate call so you get a chance to run additional logic (like checking is_active) in between.

Protecting views: @login_required

Python
from django.contrib.auth.decorators import login_required

@login_required
def create_post(request):
    ...

@login_required redirects an anonymous request to the configured login URL (LOGIN_URL in settings, default /accounts/login/) instead of running the view at all, and — once the user does log in — sends them back to the originally requested page automatically via a ?next= query parameter. The class-based equivalent is LoginRequiredMixin:

Python
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView
from .models import Post

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    fields = ["title", "body", "published"]
    success_url = "/"

Permissions

Beyond "logged in or not," Django's User model supports a has_perm() check tied to per-model permissions Django creates automatically (add_post, change_post, delete_post, view_post for a Post model) — and a @permission_required decorator that pairs with @login_required:

Python
from django.contrib.auth.decorators import login_required, permission_required

@login_required
@permission_required("blog.delete_post", raise_exception=True)
def delete_post(request, post_id):
    ...

raise_exception=True returns a 403 Forbidden for a logged-in user who lacks the permission, rather than silently redirecting them to a login page they don't need.

DRF permission classes

DRF layers its own permission system on top for API views — checked per-request against request.user and request.auth:

Permission class Behavior
AllowAny No restriction at all — the default if nothing else is configured
IsAuthenticated Requires any logged-in user (session or token)
IsAuthenticatedOrReadOnly Anyone can GET; only an authenticated user can POST/PUT/DELETE
IsAdminUser Requires request.user.is_staff
A custom BasePermission subclass Any project-specific rule, like "only the object's own author"

A complete example: only a post's own author can edit or delete it, but anyone can read it.

Python
# blog/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:   # GET, HEAD, OPTIONS
            return True
        return obj.author_id == request.user.id
Python
# blog/views.py
from rest_framework import viewsets, permissions
from .models import Post
from .serializers import PostSerializer
from .permissions import IsAuthorOrReadOnly

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]

has_object_permission() runs only once DRF already has a specific object in hand (retrieve, update, destroy) — for a list/create request there's no object yet, so a class-level check like IsAuthenticatedOrReadOnly (via has_permission()) runs first; both class-level and object-level checks must pass together, and DRF evaluates every class in permission_classes in order, rejecting the request at the first one that fails.

Common mistakes

  • Protecting a view with only @login_required when the real requirement is a specific permission — any logged-in user (not just the intended ones) can reach it.
  • Forgetting that has_object_permission() never runs for list/create — there's no object yet to check against, so object-level rules alone leave those actions unprotected; combine them with a class-level permission.
  • Confusing Django's session-based login()/logout() (for browser-facing views) with DRF's token/permission system (for API clients) and mixing the two incompletely — a session-authenticated browser request and a token-authenticated API request are validated through entirely different mechanisms.

Interview questions

Q: What's the difference between @login_required and @permission_required in Django? @login_required only checks that a request is associated with an authenticated user, redirecting anonymous requests to the login page. @permission_required("app.perm_name") additionally checks that the authenticated user holds a specific permission (from Django's automatically generated per-model permissions, or a custom one) — failing that check returns a redirect by default, or a 403 if raise_exception=True is passed.

Q: How do DRF's class-level and object-level permission checks differ, and why does that distinction matter? A class-level check (has_permission(), used by classes like IsAuthenticated) runs before DRF has fetched any specific object — appropriate for list and create, where there's nothing yet to check ownership against. An object-level check (has_object_permission()) runs only once a specific object is in hand, for actions like retrieve, update, and destroy — a rule like "only the object's author can edit it" has to live here, since it needs the actual object to compare against request.user.