Chapter 18 · Authentication, sessions and messages
18.11 Requiring a login: login_required, permission_required and LoginRequiredMiddleware
login_required and permission_required are made from user_passes_test, which tests the request’s user before the view runs and answers a failure with a redirect from redirect_to_login, the address asked for carried in "next" by default. LoginRequiredMixin, PermissionRequiredMixin and UserPassesTestMixin test in dispatch and answer a logged-in user who fails with 403, not a redirect; LoginRequiredMiddleware requires a login of every view that login_not_required does not mark.
A view that serves only some users must decide before it does any work. A browser with no logged-in user is ordinarily sent to the login page with the address it asked for, for the login view to send it back (The login and password views: LoginView, LogoutView and PasswordChangeView); a logged-in user who lacks a right is answered 403, or sent to log in as someone else. The decision is made around a function view by a decorator, inside dispatch by a mixin, or for every view by a middleware.
user_passes_test
user_passes_test takes a test, a callable given a user that answers true or false, and returns a decorator, which picks its wrapper by whether the view is a coroutine function. Around an ordinary view the wrapper calls the test with HttpRequest.user, through async_to_sync if the test is a coroutine function; such a test runs in an event loop, where reading a user not yet loaded can query the database, which Django refuses with SynchronousOnlyOperation. Around a coroutine view it awaits HttpRequest and then the test, an ordinary test through sync_to_async (Asynchronous views). A true answer calls the view, a false one returns the redirect, and an exception the test raises goes out through the wrapper.
The decorator also puts the login URL and field name it was given on the wrapper, as "login_url" and "redirect_field_name", for LoginRequiredMiddleware, and the wrapper takes the view’s name and attributes, so a mark left by an inner decorator survives (What the handler asks of a view).
The redirect: redirect_to_login
The login URL is the decorator’s argument or LOGIN_URL, "/ by default, passed through resolve_url, so either may be a path or a URL pattern’s name (The shortcuts). The address to come back to is the request’s whole URL, from HttpRequest, cut to its path and query string by HttpRequest when the login URL has no scheme or the request’s, and no host or the request’s.
redirect_to_login keeps any query string the login URL has, sets the address under the field’s name, "next" unless the decorator names another, and encodes the query with "/" left as it is; an empty field name, or None, adds nothing. It returns an HttpResponseRedirect. The office’s crew list is under login_required(login_url="https://:
a browser with no session
GET /berths/?ship=Beagle 302 Location: /accounts/login/?next=/berths/%3Fship%3DBeagle
GET /crew/ 302 Location: https://accounts.harbour.example/login/?next=http%3A//harbour.example/crew/
login_required and login_not_required
login_required is user_passes_test with a test that reads the user’s is_authenticated, written bare above a view or called with a login URL and a field’s name; it does not look at is_active.
login_not_required wraps nothing. It sets the view’s attribute "login_required" to false and returns the same function. Only LoginRequiredMiddleware reads it.
permission_required
permission_required takes one permission’s name or an iterable of names, and builds for user_passes_test a test of the view’s kind, which asks PermissionsMixin, or PermissionsMixin, whether the user has all of them (Permissions: Permission, groups and has_perm). It asks an anonymous user too, as a comment notes. When the answer is no, the test raises PermissionDenied if the decorator was given raise_exception=True, which the handler turns into a 403 (How an exception becomes a response); otherwise it returns false, and the user is sent to the login page, logged in or not.
So a logged-in user lacking it is asked to log in again, and under raise_exception=True a stranger never sees the login page. The documentation’s remedy is login_required written above the raising decorator, which also avoids a loop when LoginView sends a logged-in user back. The office’s edit_ship raises; its rename view, under permission_required("fleet, does not:
GET /ships/1/edit/ permission_required("fleet.change_ship", raise_exception=True)
tomas 403
no session 403
GET /ships/1/rename/ permission_required("fleet.change_ship")
nora 200
tomas 302 Location: /accounts/login/?next=/ships/1/rename/
no session 302 Location: /accounts/login/?next=/ships/1/rename/
The mixins and AccessMixin.handle_no_permission
Each mixin tests in dispatch and calls super().dispatch only when the test passes, so it stands before View among the bases (View.as_view and dispatch). LoginRequiredMixin tests is_authenticated. PermissionRequiredMixin asks has_perms for the names that PermissionRequiredMixin gives, and the latter raises ImproperlyConfigured when PermissionRequiredMixin is left None. UserPassesTestMixin calls, with no argument, the method UserPassesTestMixin returns, UserPassesTestMixin by default.
A failure goes to AccessMixin, which differs from the decorators in one decision: it raises PermissionDenied, with AccessMixin as its message, when AccessMixin is true or the user is logged in. Only an anonymous user is redirected, to an address worked out as the decorator’s is; AccessMixin raises ImproperlyConfigured when neither AccessMixin nor LOGIN_URL gives a URL. So tomas, whom the rename view sends to log in, gets 403 from the office’s manifest, a view with PermissionRequiredMixin and the same permission:
GET /manifest/ PermissionRequiredMixin, "fleet.change_ship"
tomas 403
no session 302 Location: /accounts/login/?next=/manifest/
LoginRequiredMiddleware
LoginRequiredMiddleware is a MiddlewareMixin with one hook, LoginRequiredMiddleware, which the handler calls with the view before calling it (Building the middleware chain). The hook lets the request through when the view’s attribute "login_required" is false, or when the user is authenticated; otherwise it returns the redirect, and the view is not called. LoginRequiredMiddleware works the redirect out as the decorator does, from the view’s "login_url" or LOGIN_URL (with neither, LoginRequiredMiddleware raises ImproperlyConfigured), and the view’s "redirect_field_name" or LoginRequiredMiddleware, "next".
Under the middleware, login_required carries its login URL and field’s name to it, and a stranger is redirected before the decorator’s wrapper is reached. A mixin carries nothing to it: its login URL and field’s name are attributes of the class, and the middleware reads the function View.as_view returns. 5.1’s release notes say the middleware does not support setting either through LoginRequiredMixin. The office’s roster has LoginRequiredMixin with a login URL of "/crew/login/"; its tide table has no decorator:
a browser with no session, without LoginRequiredMiddleware
GET /roster/ 302 Location: /crew/login/?next=/roster/
with LoginRequiredMiddleware after AuthenticationMiddleware
GET /tide/ 302 Location: /accounts/login/?next=/tide/
GET /roster/ 302 Location: /accounts/login/?next=/roster/
GET /accounts/login/ 200
GET /accounts/password_reset/ 200
POST /accounts/logout/ 302 Location: /accounts/login/?next=/accounts/logout/
A class-based view is marked through method_decorator on dispatch, which copies onto dispatch the attributes its decorator sets, and View.as_view copies those onto the function it returns. So LoginView and the four views of the password reset are login_not_required; LogoutView is not, and the password-change views carry login_required. The admin marks its login view, and gives the site-wide views that AdminSite wraps its login page as their login URL (The admin’s URLs: AdminSite and AdminSite).
The middleware reads HttpRequest.user, so it must come after AuthenticationMiddleware. AuthConfig.ready registers check_middleware, which reports "auth.E013" when LoginRequiredMiddleware or a subclass is in MIDDLEWARE and AuthenticationMiddleware is missing or comes after it (The system checks).