skip to content

In Django, how do you restrict a view to signed-in users with login_required or LoginRequiredMixin, and what happens to an anonymous visitor?

level: juniorimportance: must knowfreq 72%

answer

  1. decorator for functions, mixin for classes
  2. is_authenticated is the test
  3. a redirect, not an error
  4. LOGIN_URL plus a query parameter

basics

~10 s

Decorate a function view with @login_required or put LoginRequiredMixin first in a class-based view's bases. An anonymous request is redirected to settings.LOGIN_URL (default /accounts/login/) with ?next= carrying the original path.

solid answer

~30 s

`django.contrib.auth.decorators.login_required` wraps a function view and checks `request.user.is_authenticated`; `django.contrib.auth.mixins.LoginRequiredMixin` does the same in `dispatch()` for class-based views and goes first in the bases. For an anonymous user both return a 302 to `settings.LOGIN_URL`, default `/accounts/login/` (a named URL pattern also works), appending `?next=/the/original/path/` so the login view can send the user back. Both let you override `login_url` and `redirect_field_name`; the mixin also has `raise_exception = True` to return 403 instead of redirecting. Neither checks `is_active`: the default authentication backend already refuses inactive users at sign-in and on session reload.

code

python · 12 lines
python
from django.test import TestCase
from django.urls import reverse


class DashboardGuardTests(TestCase):
    def test_anonymous_is_redirected_with_next(self):
        response = self.client.get(reverse("dashboard"))
        self.assertRedirects(
            response,
            "/accounts/login/?next=/dashboard/",
            fetch_redirect_response=False,
        )

go deeper

for a junior

Recall which guard fits which view style, the default /accounts/login/ target, and the next parameter that brings the user back.

for a middle

Explain how the redirect URL is built, when next holds a path versus an absolute URL, and the raise_exception switch on the mixin.

for a senior

Point out the limits: opt-in guards leave forgotten views public, is_active is the backend's job, and API endpoints need a 403 or 401 rather than an HTML redirect.

for a principal

Decide between opt-in guards and the deny-by-default middleware for the whole project, and how exemptions are reviewed.

## The two built-in guards Django's `contrib.auth` ships a guard for each view style. Both ask one question, whether `request.user.is_authenticated` is true, and both answer "no" with a **redirect to the login page**, not an error. | Guard | Applies to | Where the check runs | Customisation | |---|---|---|---| | `login_required` | Function views (or `method_decorator` on a class) | A wrapper around the view | `login_url=`, `redirect_field_name=` arguments | | `LoginRequiredMixin` | Class-based views | `dispatch()`, before the handler method | `login_url`, `redirect_field_name`, `raise_exception` attributes | | `LoginRequiredMiddleware` (5.1) | Every view by default | `process_view`, before any view | `login_not_required` to opt out | The middleware is the deny-by-default variant; the decorator and mixin are opt-in per view. ## Using them ```python from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import ListView from reports.models import Report @login_required def dashboard(request): ... class ReportList(LoginRequiredMixin, ListView): model = Report login_url = "staff-login" # a named URL pattern works too ``` The mixin goes first in the bases so its `dispatch()` runs before the view's own logic; the ordering rules themselves belong to the class-based views topic. ## What an anonymous visitor gets 1. The guard sees `request.user.is_authenticated` is false (an `AnonymousUser`). 2. It resolves the login URL: the guard's own `login_url`, else `settings.LOGIN_URL`, whose default is `"/accounts/login/"`. 3. It builds the return address. If the login URL is on the same scheme and host, only the **path and query string** of the current request are used; otherwise the full absolute URL is used. 4. `redirect_to_login()` appends that value under the **redirect field name**, `"next"` by default, and returns an `HttpResponseRedirect`. So `/reports/?year=2026` becomes `/accounts/login/?next=/reports/%3Fyear%3D2026`. After a successful sign-in the login view reads `next`, checks it is safe, and redirects there. ## The knobs and their defaults - **`LOGIN_URL`** defaults to `/accounts/login/`. It accepts a path or a named URL pattern, so renaming the route does not break it. - **`login_url=`** on the decorator or attribute on the mixin overrides it per view. - **`redirect_field_name`** defaults to `"next"`. Change it only if your login view reads a different parameter; passing `None` to the decorator drops the parameter entirely. - **`raise_exception = True`** on the mixin returns a 403 via `PermissionDenied` instead of redirecting, which suits JSON endpoints that a browser redirect would confuse. ## What the guards do not do - **They do not check `is_active`.** The docs say so explicitly. Inactive users are stopped earlier because the default `ModelBackend` refuses them at sign-in and returns no user when the session is reloaded. - **They do not check permissions.** Signed in is not the same as allowed; permission guards are a separate layer that returns 403 for authenticated users. - **They do not protect anything you forget to decorate.** A new view without the decorator or mixin is public. That gap is why Django 5.1 added `LoginRequiredMiddleware` for deny-by-default projects. - **They are not API authentication.** A token-authenticated API does not have a session user at this point, so a browser-style redirect is usually the wrong answer there. ## A common junior mistake Checking `request.user.is_authenticated` inside a template to hide a link is presentation, not protection: the view still serves the page to anyone who types the URL. The guard belongs on the view. ## Async views and URLconf-level guarding Since Django 5.1, `login_required`, `permission_required` and `user_passes_test` also wrap **async** function views: the wrapper awaits `request.auser()` instead of touching `request.user`, so an `async def` view can be decorated exactly like a sync one. Guarding can also happen where routes are declared rather than where views are defined: - `path("reports/", login_required(ReportList.as_view()))` protects one route while the class stays reusable elsewhere, for instance in a public read-only variant. - A third-party view you cannot edit can be wrapped the same way in your own URLconf. - The downside is visibility: a reader of the view class no longer sees that it is protected, so teams usually pick one convention and stick to it. Whichever style you choose, a quick test per protected route, asserting the 302 to the login URL for an anonymous client, catches the forgotten decorator before it ships.

  • How do you apply login_required to a class-based view without the mixin?
    Decorate its `dispatch` with `django.utils.decorators.method_decorator`: `@method_decorator(login_required, name="dispatch")` on the class. You can also wrap the result of `as_view()` in the URLconf, `path("reports/", login_required(ReportList.as_view()))`, which keeps the view class itself unguarded for reuse.
  • When would you set raise_exception = True on LoginRequiredMixin?
    For endpoints called by JavaScript or other programs, where a 302 to an HTML login page is useless. With `raise_exception = True`, `handle_no_permission()` raises `PermissionDenied`, Django returns a 403, and the client can react, for example by prompting for sign-in.

It is like a staffed door that sends visitors without a badge to the reception desk with a slip saying which room they were heading to; reception reads the slip after issuing the badge.

saying these in an interview costs you the question

  • login_required returns a 401 or 403 to anonymous users by default.
  • login_required also rejects users whose is_active flag is False.
  • Hiding a link in the template protects the view behind it.
  • LOGIN_URL must be a hard-coded path; named URL patterns are not accepted.
  • LoginRequiredMixin can go anywhere in the list of base classes.