The recording for this lesson has not been published yet.
In the tutorial you logged into /admin/ and the little "Log in" link on your
blog pointed there too. That was not a shortcut for beginners: the admin uses exactly the
same django.contrib.auth app that you are about to expose on your own pages.
Nothing new needs installing.
Two of the entries in the default settings do all the work:
# mysite/settings.py (already there since startproject)
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
...
]
MIDDLEWARE = [
...
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
...
]
request.user is not magic, it is two middlewares in a fixed order.
SessionMiddleware reads the sessionid cookie and loads the
session dictionary; AuthenticationMiddleware looks for the key
_auth_user_id in it and fetches that user from the database.
Two consequences worth remembering. First, request.user is
never None - for a visitor who is not signed in it is an
AnonymousUser, so always test request.user.is_authenticated.
Second, the cookie only carries a session key; the user id lives on the server.
Django ships a URLconf with the whole set of auth views. Include it once:
# mysite/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("accounts/", include("accounts.urls")),
path("", include("blog.urls")),
]
That single include() gives you these routes and names:
| URL | URL name | Template it expects |
|---|---|---|
accounts/login/ | login | registration/login.html |
accounts/logout/ | logout | registration/logged_out.html |
accounts/password_change/ | password_change | registration/password_change_form.html |
accounts/password_reset/ | password_reset | four templates - lesson 4.3 |
Note what is not in the list: registration. Django has no signup view, because every project wants different fields. You write it yourself in section 6.
The auth views look for registration/login.html through the normal
template loaders. Put them in templates/registration/ and make sure
TEMPLATES[0]["DIRS"] contains BASE_DIR / "templates".
LoginView renders whatever you give it. Extend the
base.html you already have:
<!-- templates/registration/login.html -->
{% extends "base.html" %}
{% block content %}
<h1>Log in</h1>
{% if form.errors %}
<p class="text-danger">Your username and password did not match. Please try again.</p>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Log in</button>
<input type="hidden" name="next" value="{{ next }}">
</form>
<p><a href="{% url 'password_reset' %}">Forgot your password?</a></p>
<p>No account yet? <a href="{% url 'signup' %}">Sign up</a>.</p>
{% endblock %}
And the navigation, which now knows who is looking at it:
<!-- templates/base.html -->
{% if user.is_authenticated %}
<span>Hello, {{ user.username }}</span>
<a href="{% url 'blog:post_new' %}">New post</a>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit">Log out</button>
</form>
{% else %}
<a href="{% url 'login' %}">Log in</a>
{% endif %}
Since Django 5.0, LogoutView refuses GET requests. A plain
<a href="/accounts/logout/"> returns 405. Use a small form with
{% csrf_token %}, as above - it also stops
prefetching browsers and images from logging your users out.
# mysite/settings.py
LOGIN_URL = "login" # where @login_required sends anonymous visitors
LOGIN_REDIRECT_URL = "blog:post_list" # after a successful login
LOGOUT_REDIRECT_URL = "blog:post_list" # after logout
Without LOGIN_REDIRECT_URL Django uses /accounts/profile/, which
does not exist in your project - a 404 straight after a successful login is a classic first
bug. All three settings accept a URL name or a path.
If a single view needs a different destination, override it there rather than in settings:
# accounts/urls.py
from django.contrib.auth.views import LoginView
from django.urls import path
from . import views
urlpatterns = [
path("signup/", views.SignUpView.as_view(), name="signup"),
path(
"staff-login/",
LoginView.as_view(
template_name="accounts/staff_login.html",
next_page="admin:index",
redirect_authenticated_user=True,
),
name="staff_login",
),
]
UserCreationForm gives you a username field and two password fields, runs
Django's password validators and hashes the password on save. Point it at your own user
model from lesson 4.1 and add the fields you care about:
# accounts/forms.py
from django.contrib.auth.forms import UserCreationForm
from .models import User
class SignUpForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = User
fields = ("username", "email")
# accounts/views.py
from django.contrib.auth import login
from django.urls import reverse_lazy
from django.views.generic import CreateView
from .forms import SignUpForm
class SignUpView(CreateView):
form_class = SignUpForm
template_name = "registration/signup.html"
success_url = reverse_lazy("blog:post_list")
def form_valid(self, form):
response = super().form_valid(form)
login(self.request, self.object) # sign the new user in immediately
return response
The function-view version, if you prefer the style you learned in the tutorial:
def signup(request):
if request.method == "POST":
form = SignUpForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect("blog:post_list")
else:
form = SignUpForm()
return render(request, "registration/signup.html", {"form": form})
Turn on the validators you want in settings - the defaults are already sensible:
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 10}},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
User(password="hunter2") stores the plain text and nobody can log in.
Use the form, or user.set_password(raw) followed by
user.save(), or User.objects.create_user(...).
When @login_required intercepts a request it sends the visitor to
/accounts/login/?next=/post/3/edit/. After a successful login,
LoginView reads that next value and returns the user to the page
they were actually trying to reach.
# blog/views.py
from django.contrib.auth.decorators import login_required
@login_required
def post_new(request):
...
This is a redirect controlled by the URL, so it is an obvious attack surface: a phishing
link could contain ?next=https://evil.example.com/. Django protects you by
validating the target with url_has_allowed_host_and_scheme() and silently
falling back to LOGIN_REDIRECT_URL when the host is not yours. Two rules
follow:
ALLOWED_HOSTS, because the check uses
it.next yourself, validate it the same way - do not call
redirect(request.GET["next"]):
from django.utils.http import url_has_allowed_host_and_scheme
target = request.POST.get("next", "")
if url_has_allowed_host_and_scheme(target, allowed_hosts={request.get_host()}):
return redirect(target)
return redirect("blog:post_list")
A quick test that documents the whole flow:
# accounts/tests.py
from django.test import TestCase
from django.urls import reverse
class SignUpTests(TestCase):
def test_signup_logs_the_user_in(self):
response = self.client.post(
reverse("signup"),
{"username": "ada", "email": "ada@example.com",
"password1": "correct-horse-9", "password2": "correct-horse-9"},
)
self.assertRedirects(response, reverse("blog:post_list"))
self.assertTrue(response.wsgi_request.user.is_authenticated)
def test_new_post_requires_login(self):
url = reverse("blog:post_new")
self.assertRedirects(self.client.get(url), f"/accounts/login/?next={url}")
django.contrib.auth.urls and write
templates/registration/login.html and
logged_out.html on top of your base.html.LOGIN_URL, LOGIN_REDIRECT_URL and
LOGOUT_REDIRECT_URL, then verify each one in the browser./accounts/logout/ now answers 405.SignUpView with UserCreationForm that logs the new user
in, and try a password like password1234 to see the validators speak up.post_new with @login_required, log out, visit the
URL and watch the next parameter bring you back after login. Then try
?next=https://example.com/ and observe that Django ignores it.