Free Ebook cover Django Fundamentals: From First App to a Complete Backend

Django Fundamentals: From First App to a Complete Backend

New course

12 pages

Authentication and Authorization in Django Backend Applications

Capítulo 8

Estimated reading time: 10 minutes

+ Exercise

Authentication vs. Authorization (and why Django separates them)

Authentication answers: “Who is this user?” (login, sessions, password checks). Authorization answers: “What is this user allowed to do?” (permissions, groups, staff-only endpoints). Django’s built-in django.contrib.auth provides both, and you typically combine them: authenticate a user, then enforce access rules per view/endpoint.

Core building blocks you will use

  • User model: represents accounts. You can use Django’s default user model or a custom one (not covered here).
  • Session authentication: after login, Django stores the user’s ID in the session; request.user becomes the authenticated user.
  • Permissions: strings like app_label.add_model, app_label.change_model, app_label.delete_model, app_label.view_model, plus any custom permissions you define.
  • Groups: named collections of permissions (roles are usually implemented as groups).
  • Decorators/mixins: login_required, permission_required, LoginRequiredMixin, PermissionRequiredMixin.

Enabling Django’s auth system (project settings checklist)

Most projects already have these enabled. Verify the essentials:

# settings.py (snippets) INSTALLED_APPS = [     # ...     'django.contrib.auth',     'django.contrib.contenttypes',     'django.contrib.sessions',     'django.contrib.messages',     # ... ] MIDDLEWARE = [     # ...     'django.contrib.sessions.middleware.SessionMiddleware',     'django.contrib.auth.middleware.AuthenticationMiddleware',     'django.contrib.messages.middleware.MessageMiddleware',     # ... ]

These pieces ensure sessions work, request.user is populated, and login/logout can use Django’s built-in views.

Login and logout with Django’s built-in views

Django ships ready-to-use authentication views. For a backend application, these are often sufficient and more secure than rolling your own.

Step 1: Add auth URLs

# project/urls.py from django.contrib import admin from django.urls import path, include  urlpatterns = [     path('admin/', admin.site.urls),     path('accounts/', include('django.contrib.auth.urls')), ]

This provides routes such as:

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

  • /accounts/login/
  • /accounts/logout/
  • /accounts/password_change/
  • /accounts/password_reset/ (email-based reset flow)

Step 2: Provide templates (minimal)

Django’s auth views look for templates under registration/. Create at least:

  • templates/registration/login.html
  • templates/registration/logged_out.html (optional)

Keep the login template simple; Django will pass a form named form and a next parameter for redirects.

Step 3: Configure redirect behavior

# settings.py LOGIN_URL = '/accounts/login/' LOGIN_REDIRECT_URL = '/dashboard/' LOGOUT_REDIRECT_URL = '/accounts/login/'

Secure pattern: prefer redirecting to a known internal URL after login/logout. Django’s built-in login view validates the next parameter to avoid open redirects (it must be a safe URL on your host), but you should still keep your redirect defaults explicit.

Password management basics (change vs. reset)

Password change (authenticated users)

Use /accounts/password_change/ and /accounts/password_change/done/. This is for users who are already logged in and know their current password.

Secure pattern: require re-authentication for highly sensitive actions (e.g., changing email, exporting data). A common approach is to ask for the current password again in that specific view, even if the user is logged in.

Password reset (unauthenticated users)

Use /accounts/password_reset/ flow. This sends a reset link via email.

Minimal email configuration example:

# settings.py EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.yourprovider.com' EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_USER = 'no-reply@example.com' EMAIL_HOST_PASSWORD = '...' DEFAULT_FROM_EMAIL = 'no-reply@example.com'

Secure patterns:

  • Do not reveal whether an email exists in the system. Django’s reset view is designed to avoid account enumeration by showing the same “email sent” message.
  • Use HTTPS in production so reset links and session cookies are protected in transit.
  • Keep reset tokens short-lived (Django uses PASSWORD_RESET_TIMEOUT in seconds).

Protecting backend views with login_required

For backend features (dashboards, internal tools, user settings), the first line of defense is requiring authentication.

Function-based views

# app/views.py from django.contrib.auth.decorators import login_required from django.http import HttpResponse  @login_required def dashboard(request):     return HttpResponse('Private dashboard')

Class-based views

# app/views.py from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import TemplateView  class DashboardView(LoginRequiredMixin, TemplateView):     template_name = 'dashboard.html'

Secure pattern: apply protection at the view boundary (decorator/mixin) rather than relying on template logic to hide links. Hiding UI elements is not access control.

Authorization with permissions (permission_required / PermissionRequiredMixin)

Once a user is authenticated, you typically restrict actions by permission. Django automatically creates model permissions (add, change, delete, view) for each model.

Checking a permission in a function-based view

from django.contrib.auth.decorators import permission_required from django.http import HttpResponse  @permission_required('billing.view_invoice', raise_exception=True) def invoice_list(request):     return HttpResponse('Invoices')

With raise_exception=True, unauthorized users get a 403 Forbidden instead of being redirected to login (useful when the user is logged in but lacks permission).

Checking permissions in class-based views

from django.contrib.auth.mixins import PermissionRequiredMixin from django.views.generic import ListView from .models import Invoice  class InvoiceListView(PermissionRequiredMixin, ListView):     model = Invoice     permission_required = 'billing.view_invoice'     raise_exception = True

Custom permissions for non-CRUD actions

For actions like “approve payout” or “view audit log,” define explicit permissions.

# app/models.py class Payout(models.Model):     # fields...      class Meta:         permissions = [             ('approve_payout', 'Can approve payouts'),             ('view_audit_log', 'Can view audit log'),         ]

After adding permissions, create and apply migrations so Django registers them.

Groups and role design (staff vs. regular users)

Django provides Group as a practical way to implement roles. A structured approach is:

  • Define a small set of roles (groups) that match business responsibilities.
  • Map each role to a set of permissions.
  • Assign users to roles; avoid per-user permission sprawl unless necessary.

Recommended role model

Role (Group)Typical usersPermissions
Regular UserCustomers/end usersOnly permissions needed for their own resources (often enforced via object-level checks in code)
SupportSupport agentsRead-only access to user data, ability to view tickets, limited actions
Staff OpsOperations teamCRUD on operational models, limited financial actions
Finance ApproverFinance teamCustom permissions like approve_payout, view invoices, export reports
AdminSystem administratorsBroad permissions; often also is_superuser for full access

Important distinction:

  • is_staff is commonly used to allow access to the Django admin site. It is not a full authorization system by itself.
  • is_superuser bypasses permission checks. Use sparingly.

Creating groups and assigning permissions (repeatable via a data migration)

Instead of manually clicking in the admin, you can codify roles using a data migration so environments stay consistent.

# app/migrations/0002_create_roles.py from django.db import migrations  def create_roles(apps, schema_editor):     Group = apps.get_model('auth', 'Group')     Permission = apps.get_model('auth', 'Permission')      support, _ = Group.objects.get_or_create(name='Support')     finance, _ = Group.objects.get_or_create(name='Finance Approver')      # Example: allow Support to view users (auth app)     perms = Permission.objects.filter(codename__in=['view_user'])     support.permissions.set(perms)      # Example: allow Finance to approve payouts (your app)     approve = Permission.objects.get(codename='approve_payout')     finance.permissions.add(approve)  class Migration(migrations.Migration):     dependencies = [         ('app', '0001_initial'),         ('auth', '0012_alter_user_first_name_max_length'),     ]     operations = [migrations.RunPython(create_roles)]

Secure pattern: keep role definitions in code (migration/management command) to prevent “it works on staging but not production” permission drift.

Handling unauthorized access correctly (401 vs 403, redirects, and UX)

Unauthenticated users

For pages that require login, redirect to LOGIN_URL with a next parameter. login_required does this automatically.

Authenticated but forbidden

Return 403 Forbidden. Use raise_exception=True on permission checks, or raise django.core.exceptions.PermissionDenied yourself.

from django.core.exceptions import PermissionDenied  def staff_only_view(request):     if not request.user.is_authenticated:         # let login_required handle this in real code         raise PermissionDenied     if not request.user.is_staff:         raise PermissionDenied     # ...

Custom 403 page

Provide a friendly error page without leaking sensitive details:

# settings.py (ensure templates/403.html exists) # Django will render 403.html for PermissionDenied in DEBUG=False

Secure pattern: do not include internal object IDs, stack traces, or permission names in error messages shown to end users.

Restricting admin-like endpoints (beyond the Django admin)

Many backends have “internal tools” endpoints (bulk operations, exports, reprocessing jobs). Treat these as high-risk and lock them down explicitly.

Option A: staff-only gate

from django.contrib.admin.views.decorators import staff_member_required  @staff_member_required def internal_metrics(request):     ...

staff_member_required requires is_active and is_staff, and redirects to the admin login by default. This is useful for internal pages meant for staff.

Option B: permission-only gate (recommended for fine control)

from django.contrib.auth.decorators import permission_required  @permission_required('ops.view_metrics', raise_exception=True) def internal_metrics(request):     ...

This avoids granting broad staff access when only a subset of staff should see the endpoint.

Option C: combine staff + permission for sensitive operations

from django.core.exceptions import PermissionDenied  def payout_approve(request, payout_id):     if not request.user.is_authenticated:         raise PermissionDenied     if not request.user.is_staff:         raise PermissionDenied     if not request.user.has_perm('billing.approve_payout'):         raise PermissionDenied     ...

Secure pattern: for “dangerous” endpoints, require both a trusted user category (staff) and a specific permission, so accidental staff assignment doesn’t grant critical powers.

Object-level access control (own data vs. others’ data)

Django’s built-in permissions are model-level (global). Many backend rules are object-level: “users can only view their own orders.” Implement this by filtering querysets and validating ownership in the view.

Example: restrict a detail view to the owner

from django.shortcuts import get_object_or_404 from django.core.exceptions import PermissionDenied from .models import Order  def order_detail(request, pk):     order = get_object_or_404(Order, pk=pk)     if order.user_id != request.user.id and not request.user.has_perm('shop.view_all_orders'):         raise PermissionDenied     ...

Secure pattern: enforce ownership checks server-side even if the UI never links to other users’ objects.

Common security settings for auth-backed backends

These settings harden session-based authentication:

# settings.py SESSION_COOKIE_SECURE = True          # HTTPS only CSRF_COOKIE_SECURE = True             # HTTPS only SESSION_COOKIE_HTTPONLY = True        # JS can't read session cookie CSRF_COOKIE_HTTPONLY = False          # typically left False for CSRF frameworks SECURE_BROWSER_XSS_FILTER = True       # legacy; modern browsers vary SECURE_CONTENT_TYPE_NOSNIFF = True  # Consider also: SECURE_HSTS_SECONDS, SECURE_HSTS_INCLUDE_SUBDOMAINS, SECURE_HSTS_PRELOAD

Secure pattern: treat cookies and CSRF as part of authentication. If you use session auth for backend pages, keep CSRF protection enabled on state-changing requests.

Practical workflow: designing roles and mapping them to permissions

Step 1: List backend features and classify them

  • Public: no login required (rare in backend apps)
  • Authenticated: any logged-in user
  • Privileged: staff only (internal)
  • Restricted: only specific roles (permissions)
  • Critical: dual-gated (staff + permission) and audited

Step 2: Define permissions per feature

Prefer explicit permissions for sensitive actions (approve, export, impersonate, refund). Use model permissions for CRUD where appropriate.

Step 3: Create groups (roles) and assign permissions

Codify in a migration or management command. Keep roles few and stable.

Step 4: Enforce at the view boundary

  • login_required for authenticated-only
  • permission_required/PermissionRequiredMixin for restricted
  • staff_member_required or is_staff checks for staff-only tooling
  • Object-level checks for “own data” rules

Step 5: Decide behavior for unauthorized access

  • Redirect to login for unauthenticated
  • Return 403 for authenticated-but-forbidden
  • Use a safe 403 template; log details server-side

Now answer the exercise about the content:

In a Django backend, how should access be handled when a user is logged in but does not have the required permission for a view?

You are right! Congratulations, now go to the next page

You missed! Try again.

If the user is authenticated but lacks authorization, the correct behavior is to deny access with 403 Forbidden, e.g., via raise_exception=True or raising PermissionDenied. Redirecting to login is meant for unauthenticated users, and hiding UI elements is not access control.

Next chapter

Settings and Environment Configuration for Development and Production

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.