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.userbecomes 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 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.htmltemplates/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_TIMEOUTin 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 = TrueCustom 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 users | Permissions |
|---|---|---|
| Regular User | Customers/end users | Only permissions needed for their own resources (often enforced via object-level checks in code) |
| Support | Support agents | Read-only access to user data, ability to view tickets, limited actions |
| Staff Ops | Operations team | CRUD on operational models, limited financial actions |
| Finance Approver | Finance team | Custom permissions like approve_payout, view invoices, export reports |
| Admin | System administrators | Broad permissions; often also is_superuser for full access |
Important distinction:
is_staffis commonly used to allow access to the Django admin site. It is not a full authorization system by itself.is_superuserbypasses 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=FalseSecure 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_PRELOADSecure 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_requiredfor authenticated-onlypermission_required/PermissionRequiredMixinfor restrictedstaff_member_requiredoris_staffchecks 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