Introduction

In most Django projects, not every page should be public. Some pages should only be visible to logged-in users, and some actions should only be allowed for specific users such as admins, editors, teachers, or managers.

For example:

This is where login protection and permissions become essential.

In this tutorial, you will learn how to:

By the end, you will know how to secure your Django pages properly and build applications with controlled access.

What You Will Learn

By the end of this tutorial, you will understand:

Prerequisites

Before starting, you should already know:

1. Why Protect Pages?

In a real application, some information should not be public.

Examples:

If you do not protect these pages, anyone can access them just by opening the URL.

So Django provides tools to control:

2. login_required — Protect Function-Based Views

The simplest way to protect a page is with login_required.

Example

from django.contrib.auth.decorators import login_required
from django.shortcuts import render

@login_required
def dashboard(request):
    return render(request, 'dashboard.html')

What it does

3. Set the Login URL

Django needs to know where to send unauthenticated users.

settings.py

LOGIN_URL = 'login'

You can also use a direct path:

LOGIN_URL = '/login/'

If a user tries to open a protected page while not logged in, Django redirects them there.

4. Redirect After Login

You can also define where users go after logging in.

settings.py

LOGIN_REDIRECT_URL = 'dashboard'
LOGOUT_REDIRECT_URL = 'login'

This means:

5. Example with URLs

urls.py

from django.urls import path
from .views import dashboard

urlpatterns = [
    path('dashboard/', dashboard, name='dashboard'),
]

Now dashboard/ is protected.

6. What Happens When a User Is Not Logged In?

Suppose a visitor tries to access:

/dashboard/

If they are not logged in, Django redirects them to:

/login/?next=/dashboard/

The next parameter tells Django where to return the user after successful login.

This is very useful because the user returns to the page they originally wanted.

7. Protecting Class-Based Views

For class-based views, Django uses LoginRequiredMixin.

Example

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView

class DashboardView(LoginRequiredMixin, TemplateView):
    template_name = 'dashboard.html'

Important Rule

Put LoginRequiredMixin before the main view class.

Correct:

class DashboardView(LoginRequiredMixin, TemplateView):

Not:

class DashboardView(TemplateView, LoginRequiredMixin):

8. Protecting List and Detail Views

You can also protect generic views the same way.

Example

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

class PostListView(LoginRequiredMixin, ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'

Now only logged-in users can see the post list.

9. Restricting Access by Staff Status

Some pages should only be visible to staff users.

Django’s User model has built-in fields:

Example

from django.contrib.auth.decorators import user_passes_test
from django.shortcuts import render

def is_staff_user(user):
    return user.is_staff

@user_passes_test(is_staff_user)
def staff_dashboard(request):
    return render(request, 'staff_dashboard.html')

This allows only staff users.

10. Using user_passes_test

user_passes_test lets you define your own rule.

Example: only superusers

from django.contrib.auth.decorators import user_passes_test

def is_admin(user):
    return user.is_superuser

@user_passes_test(is_admin)
def admin_panel(request):
    return render(request, 'admin_panel.html')

11. Django’s Built-In Permission System

Django automatically creates permissions for each model:

For example, for a Post model in an app called blog, Django creates permissions like:

These permissions can be assigned to users or groups.

12. Using permission_required

Django provides the permission_required decorator.

Example

from django.contrib.auth.decorators import permission_required

@permission_required('blog.add_post')
def add_post(request):
    return render(request, 'blog/add_post.html')

This means only users with the permission blog.add_post can access the page.

If they do not have permission, access is denied.

13. Handling Exceptions with permission_required

By default, permission_required redirects to login if the user is not logged in.

You can also raise an error instead:

@permission_required('blog.add_post', raise_exception=True)
def add_post(request):
    return render(request, 'blog/add_post.html')

If the user lacks permission, Django returns a 403 Forbidden error.

14. Checking Permissions Manually in a View

Sometimes you want more custom control.

Example

from django.http import HttpResponseForbidden
from django.shortcuts import render

def add_post(request):
    if not request.user.has_perm('blog.add_post'):
        return HttpResponseForbidden("You do not have permission to add posts.")
    return render(request, 'blog/add_post.html')

This is useful when you want custom messages or custom logic.

15. Using Permissions in Class-Based Views

For CBVs, use PermissionRequiredMixin.

Example

from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import TemplateView

class AddPostView(PermissionRequiredMixin, TemplateView):
    template_name = 'blog/add_post.html'
    permission_required = 'blog.add_post'

Only users with that permission can access the page.

16. Combine Login and Permission Mixins

Often you want both login and permission protection.

Example

from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import TemplateView

class AddPostView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView):
    template_name = 'blog/add_post.html'
    permission_required = 'blog.add_post'

This means:

17. Groups in Django

Django groups let you assign permissions to several users at once.

For example, you may create groups like:

Instead of assigning permissions user by user, you assign permissions to the group, then add users to the group.

18. Creating Groups in the Admin Panel

Go to Django admin:

Now all users in the Editors group inherit those permissions.

This is much easier for real projects.

19. Checking Group Membership

You can check whether a user belongs to a group.

Example

def is_editor(user):
    return user.groups.filter(name='Editors').exists()

Then use it with user_passes_test:

from django.contrib.auth.decorators import user_passes_test

@user_passes_test(is_editor)
def editor_dashboard(request):
    return render(request, 'editor_dashboard.html')

20. Permissions in Templates

Django lets you check permissions directly in templates.

Example

{% if perms.blog.add_post %}
    <a href="{% url 'add_post' %}">Add Post</a>
{% endif %}

This means:

You can also check authentication:

{% if user.is_authenticated %}
    <a href="{% url 'dashboard' %}">Dashboard</a>
{% endif %}

21. Hiding vs Protecting

This is important:

For example, even if a user does not see the “Delete Post” button, they could still manually type the delete URL into the browser.

So always protect both:

22. Restricting by Ownership

Sometimes the rule is not based on staff or permissions, but ownership.

For example:

Function-Based View Example

from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, render
from .models import Post

def edit_post(request, post_id):
    post = get_object_or_404(Post, id=post_id)

    if request.user != post.author:
        return HttpResponseForbidden("You can only edit your own posts.")

    return render(request, 'blog/edit_post.html', {'post': post})

23. Ownership Restriction in Class-Based Views

For CBVs, you can override dispatch() or get_object().

Example

from django.http import HttpResponseForbidden
from django.views.generic import UpdateView
from .models import Post

class PostUpdateView(UpdateView):
    model = Post
    fields = ['title', 'content']
    template_name = 'blog/post_form.html'

    def dispatch(self, request, *args, **kwargs):
        post = self.get_object()
        if request.user != post.author:
            return HttpResponseForbidden("You can only edit your own posts.")
        return super().dispatch(request, *args, **kwargs)

This protects the update page so only the author can edit the post.

24. Full Example: Protected Blog Management

Let us imagine a blog with these rules:

views.py

from django.contrib.auth.decorators import login_required, permission_required
from django.http import HttpResponseForbidden
from django.shortcuts import render, get_object_or_404
from .models import Post

@login_required
def dashboard(request):
    return render(request, 'dashboard.html')

@permission_required('blog.add_post', raise_exception=True)
def add_post(request):
    return render(request, 'blog/add_post.html')

@login_required
def edit_post(request, post_id):
    post = get_object_or_404(Post, id=post_id)

    if request.user != post.author:
        return HttpResponseForbidden("You can only edit your own posts.")

    return render(request, 'blog/edit_post.html', {'post': post})

This is a realistic access-control structure.

25. Custom Permissions

You can define your own permissions inside a model.

Example

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()

    class Meta:
        permissions = [
            ('can_publish_post', 'Can publish post'),
        ]

After migrations, Django creates this custom permission.

You can assign it to users or groups just like built-in permissions.

Then use it like this:

@permission_required('blog.can_publish_post', raise_exception=True)
def publish_post(request):
    ...

26. Common Mistakes

Forgetting to protect the view

Showing or hiding links in templates is not enough.

Using login_required but not setting LOGIN_URL

This may cause redirect confusion.

Mixing authentication and permission logic

Being logged in does not automatically mean the user has permission.

Not using groups

For bigger projects, groups make permission management much easier.

Forgetting ownership checks

A logged-in user should not automatically be able to edit everyone’s content.

27. Best Practices

28. Mini Project Example

Imagine a school platform:

Possible rules:

This is exactly the kind of access control real Django applications need.

29. Summary

In this tutorial, you learned that:

31. Mini Quiz

1. Which decorator protects a function-based view for logged-in users only?

A. user_passes_test
B. permission_required
C. login_required
D. auth_required

2. Which mixin protects a class-based view for logged-in users?

A. UserRequiredMixin
B. LoginRequiredMixin
C. AuthMixin
D. SecureViewMixin

3. Which decorator checks a specific permission?

A. group_required
B. login_required
C. permission_required
D. staff_required

4. What does user.has_perm() check?

A. Whether the user is active
B. Whether the user has a specific permission
C. Whether the user belongs to a group
D. Whether the user is logged out

5. Why are groups useful?

A. They replace models
B. They speed up templates
C. They let you assign permissions to multiple users easily
D. They automatically create dashboards

32. What Comes Next?

The next ideal tutorial is:

Tutorial: Django Sessions and Cookies
Subject: how Django remembers users, stores session data, and manages cookies.