~/TechPurAI
~/tutorials/django-from-scratch/user-authentication
intermediate·part 16 of 22·2 min read

User authentication: login, logout, and signup

Updated Aug 15, 2026Python · Django

request.user has been referenced since part 13 as if logging in already worked. It hasn't — django.contrib.auth has been in INSTALLED_APPS since part 1 doing the User model and session work, but nothing so far has actually let someone log in through a page.

Login and logout, almost for free

python
# blogsite/urls.py
from django.contrib.auth import views as auth_views

urlpatterns = [
    # ...
    path("accounts/login/", auth_views.LoginView.as_view(), name="login"),
    path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
]

LoginView and LogoutView are built into django.contrib.auth — no view code of your own needed, only a template for the login form:

django
{# registration/login.html #}
{% extends "blog/base.html" %}
{% block content %}
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Log in</button>
  </form>
{% endblock %}

LoginView specifically looks for registration/login.html — that exact path is a Django convention, not a name you get to pick. Django searches every installed app's templates/ folder for it, so this can live in blog/templates/registration/login.html alongside the blog/ folder used everywhere else.

Signup

python
# blog/views.py
from django.contrib.auth.forms import UserCreationForm
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy

class SignUpView(CreateView):
    form_class = UserCreationForm
    template_name = "registration/signup.html"
    success_url = reverse_lazy("login")
python
# blog/urls.py
path("accounts/signup/", views.SignUpView.as_view(), name="signup"),

There's no built-in SignUpView the way there's a LoginView — UserCreationForm, another django.contrib.auth shortcut, handles the username/password/password-confirmation fields and validation (matching passwords, minimum complexity), and CreateView from part 13 does the rest. signup.html is the same {% csrf_token %} {{ form.as_p }} shape as the login template.

What request.user actually is

python
def some_view(request):
    request.user            # a User instance, or AnonymousUser if not logged in
    request.user.is_authenticated   # True or False — always safe to check, on either type

request.user is never None — it's always a real object, either the logged-in User or Django's AnonymousUser, which exists specifically so request.user.is_authenticated is always safe to call without a null check first. AnonymousUser.is_authenticated is hardcoded False; a real User's is hardcoded True.

Showing login state in the base template

django
{# blog/templates/blog/base.html — inside <header> #}
{% if user.is_authenticated %}
  <span>{{ user.username }}</span>
  <form method="post" action="{% url 'logout' %}">{% csrf_token %}<button>Log out</button></form>
{% else %}
  <a href="{% url 'login' %}">Log in</a>
  <a href="{% url 'signup' %}">Sign up</a>
{% endif %}

user (not request.user) is available directly in every template — Django's auth context processor puts it there automatically, on every request, so nothing needs to be passed in manually from every single view.

Common mistake

Logging out with a plain link (<a href="/logout/">) instead of a form. Django's LogoutView only accepts POST by default for the same reason a delete link had to become a form in part 15 — a state-changing action reachable by GET is reachable by anything that follows a link, not just a person choosing to click it.

Next: actually restricting PostCreateView and friends to logged-in users — right now, anyone can reach /post/new/ regardless of request.user.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous15. Deleting objects: DeleteView and confirmation templatesnext →17. Restricting access: login_required and LoginRequiredMixin