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.
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
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:
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:
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.
# 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
# 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_requiredwhen 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 forlist/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.