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:

  • a dashboard should be accessible only after login
  • an admin page should be restricted to staff users
  • an edit page should be available only to the author of a post
  • some users may view data, while others may add, edit, or delete it

This is where login protection and permissions become essential.

In this tutorial, you will learn how to:

  • protect pages so only authenticated users can access them
  • restrict views based on user roles
  • use Django’s built-in permission system
  • work with groups and permissions
  • control access in both function-based views and class-based views

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:

  • how to use login_required
  • how to redirect unauthenticated users
  • how to protect class-based views
  • how Django permissions work
  • how to use permission_required
  • how to check permissions in templates
  • how to use groups
  • how to restrict pages by ownership or role

Prerequisites

Before starting, you should already know:

  • Django authentication basics
  • users, login, and logout
  • views and templates
  • class-based views basics
  • Django models

1. Why Protect Pages?

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

Examples:

  • user profile dashboard
  • order history
  • teacher area
  • management pages
  • content editing pages
  • private documents

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

So Django provides tools to control:

  • who can enter a page
  • what they are allowed to do

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

  • if the user is logged in, the page is shown
  • if the user is not logged in, Django redirects them to the login page

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:

  • after login → go to dashboard
  • after logout → go to login page

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:

  • is_authenticated
  • is_staff
  • is_superuser

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:

  • add_modelname
  • change_modelname
  • delete_modelname
  • view_modelname

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

  • blog.add_post
  • blog.change_post
  • blog.delete_post
  • blog.view_post

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:

  • user must be logged in
  • user must have the blog.add_post permission

17. Groups in Django

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

For example, you may create groups like:

  • Editors
  • Teachers
  • Managers
  • Moderators

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:

  • open Groups
  • create a new group, for example Editors
  • assign permissions such as:
    • blog.add_post
    • blog.change_post
    • blog.delete_post
  • add users to the group

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:

  • if the user has the permission blog.add_post
  • show the “Add Post” link

You can also check authentication:

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

21. Hiding vs Protecting

This is important:

  • hiding a button in the template is not enough
  • you must also protect the view itself

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:

  • the template display
  • the view access

22. Restricting by Ownership

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

For example:

  • only the author can edit their own post
  • only the owner can view their private file
  • only the student can view their own grades

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:

  • everyone can view public posts
  • only logged-in users can access dashboard
  • only editors can add posts
  • only the author can edit their own posts

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

  • protect private pages with login_required
  • use PermissionRequiredMixin or permission_required for restricted actions
  • use groups for role-based access
  • check ownership when needed
  • hide unauthorized buttons in templates, but also secure the view
  • keep permission logic clear and simple
  • use Django admin to manage permissions and groups easily

28. Mini Project Example

Imagine a school platform:

  • students can only see their own dashboard
  • teachers can add grades
  • managers can access reports
  • admins can manage all data

Possible rules:

  • login_required for student dashboard
  • permission_required('school.add_grade') for teachers
  • group Managers for reports
  • ownership checks for personal data

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

29. Summary

In this tutorial, you learned that:

  • login_required protects pages for logged-in users
  • LoginRequiredMixin protects class-based views
  • Django has a built-in permission system
  • permission_required and PermissionRequiredMixin restrict access by permission
  • groups make it easier to manage permissions
  • template checks help hide UI elements
  • ownership checks protect personal content
  • proper access control is essential in real projects

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.