Django Authentication Tutorial: Login, Logout, Signup, Password Change, and Password Reset
Tags: django, authentication, login, signup
Published: 2024-01-27 01:50:09

In this tutorial, we'll learn how to configure a complete [user authentication system](https://docs.djangoproject.com/en/5.0/topics/auth/) in Django consisting of login, logout, signup, and password reset.

The complete source code is available [on Github](https://github.com/wsvincent/django-auth-tutorial) if you need a reference.

## Table of Contents

- [Initial Setup](#initial-setup)
- [Log In Page](#log-in-page)
- [Create A Homepage](#create-a-homepage)
- [Log Out Button](#log-out-button)
- [Sign Up Page](#sign-up-page)
- [Password Change](#password-change)
- [Password Reset](#password-reset)
- [Conclusion](#conclusion)

<a id="initial-setup"></a>
## Initial Setup
The first step is to create a dedicated directory for our code, which we'll place on the Desktop in a folder called `django_auth`. 

```
# Windows
$ cd onedrive\desktop\code
$ mkdir django_auth
$ cd django_auth

# macOS
$ cd ~/desktop/code
$ mkdir django_auth
$ cd django_auth
```

Next, activate a new virtual environment called `.venv`.

```
# Windows
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install django~=5.0

# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install django~=5.0
```

Then install Django, create a new Django project called `django_project`, run `migrate` to initialize the new database, and execute `runserver` to start up the local web server provided by Django.

```
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py migrate
(.venv) $ python manage.py runserver
```

Navigating to `http://127.0.0.1:8000`, you'll see the Django welcome screen.

![Django welcome page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tdxn9hpuok019bd9i5jt.png)

The Django `contrib` module provides built-in apps to help with development. In the `django_project/settings.py` file under `INSTALLED_APPS`, you can see that `auth` is listed and available to us.

```python
# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",  # Yoohoo!!!!
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
```

To use the `auth` app, we need to add it to our project-level `django_project/urls.py` file. At the top, import `include` and create a new URL path at `accounts/`. You can choose a different URL path, but using `accounts/` is a standard practice and requires less customization later.

```python
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include  # new

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")),  # new
]
```

The `auth` app we've now included provides us with multiple [authentication views](https://docs.djangoproject.com/en/5.0/topics/auth/default/#module-django.contrib.auth.views) and URLs for handling login, logout, password change, password reset, etc. It notably _does not_ include a view and URL for signup, so we have to configure that ourselves.

```
accounts/login/ [name='login']
accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']
```

<a id="log-in-page"></a>
## Log In Page
Let's make our login page! By default, Django will look within a templates folder called `registration` for auth templates. The login template is called `login.html`.

Create a new project-level directory called `templates` and a directory called `registration` within it.

```
(.venv) $ mkdir templates
(.venv) $ mkdir templates/registration
```

Then create a `templates/registration/login.html` file with your text editor and include the following code:

```html
<!-- templates/registration/login.html -->
<h2>Log In</h2>
<form method="post">
  {% csrf_token %}
  {{ form }}
  <button type="submit">Log In</button>
</form>
```

This code is a standard Django form using `POST` to send data and `{% csrf_token %}` tags for security concerns, namely to prevent a CSRF Attack. The form's contents are displayed with `{{ form }}`, and then we add a "submit" button.

Next, update the `settings.py` file to tell Django to look for a `templates` folder at the project level. Update the `DIRS` setting within `TEMPLATES` with the following one-line change.

```python
# django_project/settings.py
TEMPLATES = [
    {
        ...
        "DIRS": [BASE_DIR / "templates"],  # new
        ...
    },
]
```

Our login functionality now works, but we should specify _where_ to redirect the user upon a successful login using the `LOGIN_REDIRECT_URL` setting. At the bottom of the `settings.py` file, add the following to redirect the user to the homepage. 

```python
# django_project/settings.py
LOGIN_REDIRECT_URL = "/"  # new
```

If you start the Django server again with `python manage.py runserver` and navigate to our login page at `http://127.0.0.1:8000/accounts/login/`, you'll see the following.

![Login page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t3fektnsgrt6scs2y7kw.png)

We can only log in if we have a user account. And since adding a signup form is yet to come, the most straightforward approach is to make a superuser account from the command line. Quit the server with `Control+c` and then run the command `python manage.py createsuperuser.` Answer the prompts and note that your password will not appear on the screen when typing for security reasons.

```
(.venv) > python manage.py createsuperuser
Username (leave blank to use 'wsv'):
Email address: will@learndjango.com
Password:
Password (again):
Superuser created successfully.
```

Now start the server again with `python manage.py runserver` and refresh the page at `http://127.0.0.1:8000/accounts/login/`. Enter the login info for your just-created superuser.

![Homepage error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ibn4jdq1j2b5yomt3anw.png)

Our login worked because it redirected us to the homepage, but we still need to create that homepage, so we see the error _Page not found_. Let's fix that!

<a id="create-a-homepage"></a>
## Create A Homepage
We want a simple homepage displaying one message to logged-in users and another to logged-out users. Create two new files with your text editor: `templates/base.html` and `templates/home.html.` Note that these files exist within the `templates` folder but _not_ within `templates/registration/,` where Django auth looks by default for user auth templates.

Add the following code to each:

```html
<!-- templates/base.html -->
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>{% block title %}Django Auth Tutorial{% endblock %}</title>
</head>

<body>
  <main>
    {% block content %}
    {% endblock %}
  </main>
</body>

</html>
```

```html
<!-- templates/home.html -->
{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
{% if user.is_authenticated %}
Hi {{ user.username }}!
{% else %}
<p>You are not logged in</p>
<a href="{% url 'login' %}">Log In</a>
{% endif %}
{% endblock %}
```

While we're at it, we can update `login.html` too to extend our new `base.html` file:

```html
<!-- templates/registration/login.html -->
{% extends "base.html" %}

{% block title %}Login{% endblock %}

{% block content %}
<h2>Log In</h2>
<form method="post">
  {% csrf_token %}
  {{ form }}
  <button type="submit">Log In</button>
</form>
{% endblock %}
```

Now, update the `django_project/urls.py` file so we can display the homepage. Import `TemplateView` on the third line and then add a URL pattern for it at the path, `"  "`.

```python
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.views.generic.base import TemplateView  # new

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")),
    path("", TemplateView.as_view(template_name="home.html"), name="home"),  # new
]
```

And we're done. If you start the Django server again with `python manage.py runserver` and navigate to the homepage at `http://127.0.0.1:8000/` you'll see the following:

![Homepage logged in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lljr1os07i7ftirpmept.png)

It worked! But how do we log out? The only option currently is to go into the admin panel at `http://127.0.0.1:8000/admin/` and click the "Log Out" link in the upper right corner.

![Admin page logout link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rzhmku8ipxb0mhrft86p.png)

The "Logout" link will log us out, as seen by the redirect page:

![Admin page logged out](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yeytuic4j7ujvwnbrg86.png)

If you go to the homepage again at `http://127.0.0.1:8000/` and refresh the page, it is visible that we are logged out.

![Home page logged out](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/70g75n4k8f6uf4oph6ee.png)

<a id="log-out-button"></a>
## Log Out Button
One of the changes to Django 5.0, as noted in the [release notes](https://docs.djangoproject.com/en/5.0/releases/5.0/), is the removal support for logging out via `GET` requests. In previous versions of Django, you could add a logout link like `<a href=" {% url 'logout' %}">Log Out</a>` to a template file. But now a `POST` request via a form is required.

Let's demonstrate that now by adding a "Logout" button to the homepage. We can add a logout button to the `home.html` file under the `{{ user.username }}!` section.

```html
<!-- templates/home.html-->
{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
{% if user.is_authenticated %}
Hi {{ user.username }}!
<form action="{% url 'logout' %}" method="post">
  {% csrf_token %}
  <button type="submit">Log Out</button>
</form>
{% else %}
<p>You are not logged in</p>
<a href="{% url 'login' %}">Log In</a>
{% endif %}
{% endblock %}
```

Then update `settings.py` with our redirect link, `LOGOUT_REDIRECT_URL.` Add it right next to our login redirect so the bottom of the `settings.py` file should look as follows:

```python
# django_project/settings.py
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"  # new
```

Now that we have a homepage view, we should use that instead of our current hardcoded approach. What's the URL name of our homepage? It's `home,` which we named in our `django_project/urls.py` file:

```python
# django_project/urls.py
    ...
    path("", TemplateView.as_view(template_name="home.html"), name="home"),
    ...
```

So we can replace `"/"` with `home` at the bottom of the `settings.py` file:

```python
# django_project/settings.py
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"
```

If you log in and revisit the homepage, you'll be redirected to the new homepage with a "logout" link for logged-in users.

![Homepage logout button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifewe0p0jlzj6w6ssmjw.png)

Clicking it takes you to the logged-out homepage with a "Log In" link.

![Homepage login link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hcr9bhvwp668te2o2uny.png)

If you want a "Log Out" link instead of a button, there are two user experience patterns. You could add custom CSS to the form or create a dedicated logout page containing the form and a message like, "Are you sure you want to log out?"

<a id="sign-up-page"></a>
## Sign Up Page
Now that we have sorted out logging in and logging out, it is time to add a signup page to our basic Django site. If you recall, Django __does not__ provide a built-in view or URL for this, so we must code up the form and the page ourselves.

To begin, stop the local webserver with `Control+c` and create a dedicated app called `accounts`, which we'll use for our custom account logic.

```
(.venv) $ python manage.py startapp accounts
```

Make sure to add the new app to the `INSTALLED_APPS` setting in the `django_project/settings.py` file:

```python
# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "accounts",  # new
]
```

Then add a URL path in `django_project/urls.py` that is __above__ our included Django `auth` app. The order is important here because Django looks for URL patterns from top-to-bottom. We want to maintain the pattern of having our user authentication logic at `accounts/` but ensure that the signup page loads first.

```python
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.views.generic.base import TemplateView

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("accounts.urls")),  # new
    path("accounts/", include("django.contrib.auth.urls")),
    path("", TemplateView.as_view(template_name="home.html"), name="home"),
]
```

Next, create a new file called `accounts/urls.py` with your text editor and add the following code.

```python
# accounts/urls.py
from django.urls import path

from .views import SignUpView


urlpatterns = [
    path("signup/", SignUpView.as_view(), name="signup"),
]
```

Now for the `accounts/views.py` file:

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


class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    success_url = reverse_lazy("login")
    template_name = "registration/signup.html"
```

At the top we import [UserCreationForm](https://docs.djangoproject.com/en/5.0/topics/auth/default/#django.contrib.auth.forms.UserCreationForm), [reverse_lazy](https://docs.djangoproject.com/en/5.0/ref/urlresolvers/#reverse-lazy), and the generic class-based view [CreateView](https://docs.djangoproject.com/en/5.0/ref/class-based-views/generic-editing/#django.views.generic.edit.CreateView).

We are creating a new class called `SignUpView` that extends `CreateView`, sets the form as `UserCreationForm`, and uses the _not-yet-created_ template `signup.html`. Note that we use `reverse_lazy` to redirect users to the login page upon successful registration rather than `reverse`, because _for all generic class-based views_, the URLs are not loaded when the file is imported, so we have to use the lazy form of reverse to load them later when we are sure they're available.

Ok, now for the final step. Create a new template, `templates/registration/signup.html`, and populate it with this code that looks almost exactly like what we used for `login.html.`

```html
<!-- templates/registration/signup.html -->
{% extends "base.html" %}

{% block title %}Sign Up{% endblock %}

{% block content %}
<h2>Sign up</h2>
<form method="post">
  {% csrf_token %}
  {{ form }}
  <button type="submit">Sign Up</button>
</form>
{% endblock %}
```

We're done! To confirm it all works, spin up our local server with `python manage.py runserver` and navigate to `http://127.0.0.1:8000/accounts/signup/`.

![Django signup page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r49pumt28bxlabwag9iw.png)

The extra text with tips on usernames and passwords comes from Django. We can customize that, too, but it requires a little more work and is beyond the scope of this tutorial.

Sign up for a new account and hit the "Sign up" button. You will be redirected to the login page, `http://127.0.0.1:8000/accounts/login/`, where you can log in with your new account.

And then, after a successful login, you'll be redirected to the homepage with a personalized "Hi username!" greeting.

![Homepage for user testuser](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l59n253nvx86rcn26484.png)

One of Django's most powerful features is its built-in admin, which we can use to view and edit our existing users. If you navigate to the admin page at `http://127.0.0.1:8000/admin`, a warning will indicate you are currently logged in to a non-superuser account.

![Admin login page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ol22mumbhklj9x7zer7.png)

Log in with your superuser account and click on "Users."

![Admin page logout link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6k239ena8td7u8g7el3.png)

You can see the two users for our Django project, the superuser and the regular user, created via the signup form.

![Admin users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94pnijowg4d65jc1gfwt.png)

It is possible to customize the Django admin in many ways, but for now, we can see the basic information. Clicking on an individual `username` opens up a change user page where you can edit user information.

<a id="password-change"></a>
## Password Change
Django provides a default implementation of password change functionality. To try it out, log out of your superuser account and log in with your regular user.

The default "Password change" page is located at `http://127.0.0.1:8000/accounts/password_change/`.

![Password change page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0cmcd7xfj14aq9iobz8l.png)

Enter your old password and then a new one twice. Click the "Change My Password" button, and you will be redirected to the "Password change successful" page.

![Password change successful](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejeplmlvwe6ebt5g2hlg.png)

If you want to customize these two password change pages to match the look and feel of your website, it is only necessary to override the existing templates. Django already provides us with the views and URLs. To do this, create two new template files in the `registration` directory:

- `templates/registration/password_change_form.html`
- `templates/registration/password_change_done.html`

We will not fully customize the look and feel of a Django website in this tutorial. However, I illustrate how it is possible in my book, [Django for Beginners](https://djangoforbeginners.com/).

Let's add a link to the password change page in our `home.html` template. I have also added `<p>` tags for some spacing. 

```html
<!-- templates/home.html -->
{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
{% if user.is_authenticated %}
<p>Hi {{ user.username }}!</p>
<p><a href="{% url 'password_change' %}">Password Change</a></p>
<form action="{% url 'logout' %}" method="post">
  {% csrf_token %}
  <button type="submit">Log Out</button>
</form>
{% else %}
<p>You are not logged in</p>
<a href="{% url 'login' %}">Log In</a>
{% endif %}
{% endblock %}
```

Ensure the webserver is running and refresh the homepage to see the updated one.

![Homepage with password change link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8d4px458u0nbkpgarkfz.png)

<a id="password-reset"></a>
## Password Reset
A password reset page is useful when a user forgets their log in information: a user can enter in their email address and receive a cryptographically secure email with a one-time link to a password reset page. This is typically available to logged-out users. Django has built-in functionality for this that only requires a small amount of configuration.

Let's add a link to the default password reset page that will be available to logged-out users.

```html
<!-- templates/home.html -->
{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
{% if user.is_authenticated %}
<p>Hi {{ user.username }}!</p>
<p><a href="{% url 'password_change' %}">Password Change</a></p>
<form action="{% url 'logout' %}" method="post">
  {% csrf_token %}
  <button type="submit">Log Out</button>
</form>
{% else %}
<p>You are not logged in</p>
<p><a href="{% url 'password_reset' %}">Password Reset</a></p>
<p><a href="{% url 'login' %}">Log In</a></p>
{% endif %}
{% endblock %}
```

Click the "Log Out" button on the current homepage.

![Homepage with password reset link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iima83ct2hgmzl5f5qo0.png)

Then click on the link for "Password Reset."

![Password reset page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i79mcxg1xhuim1r2mmyc.png)

The default template is ugly and styled to match the admin but is functional. We want to try it out, but there's one problem: our regular user account _does not have an email address_ associated with it. The default Django [UserCreationForm](https://docs.djangoproject.com/en/5.0/topics/auth/default/#django.contrib.auth.forms.UserCreationForm) we extended for our signup form does not have email included! It is possible to update this so that an email form is included, but doing so is beyond the scope of this tutorial.

Nonetheless, there is an easy fix. Log in to the admin, click on `Users,` and select the `username` for your regular user account to bring up the change user page where you can add an email. 

![Admin change user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fa9f4930rbarkomassrv.png)

Make sure to click the "Save" button at the bottom of the page. Then click the "Log Out" button in the upper right-hand corner of the admin or back on the homepage.

Django defaults to an [SMTP](https://docs.djangoproject.com/en/5.0/ref/settings/#email-backend) email backend that requires some configuration. To test the password reset flow locally, we can update the `django_project/settings.py` file to output emails to the console instead. Add this one line to the bottom of the file.

```python
# django_project/settings.py
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # new
```

Finally, we can try the Password Reset page again at `http://127.0.0.1:8000/accounts/password_reset/`. Enter the email address for your regular user account and click the "Change My Password" button. It will redirect you to the password reset sent page.

![Password reset sent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nyrqd9n2jn6s89s27sss.png)

For security reasons, Django will not provide any notification whether you entered an email that exists in the database or not. But if you look in your terminal/console now, you can see the contents of the email outputted there.

```
Content-Type: text/plain; charset= "utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject: Password reset on 127.0.0.1:8000
From: webmaster@localhost
To: testuser@email.com
Date: Fri, 26 Jan 2024 20:04:32 -0000
Message-ID: <170629947287.95157.11297370611002899863@1.0.0.127.in-addr.arpa>


You're receiving this email because you requested a password reset for your user account at 127.0.0.1:8000.

Please go to the following page and choose a new password:

http://127.0.0.1:8000/accounts/reset/Mg/c1fefk-40c35272edd84fcb73f64d2b4ebd0e8f/

Your username, in case you've forgotten: testuser

Thanks for using our site!

The 127.0.0.1:8000 team



-------------------------------------------------------------------------------
```

Copy the unique URL from your console into your web browser. It will cryptographically confirm your identity and take you to the Password Reset Confirmation page at `http://127.0.0.1:8000/accounts/reset/Mg/set-password/`.

![Password Reset Confirmation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39tlfijdixrsct7htwr2.png)

Enter in a new password and click the "Change my password" button. It will redirect you to the Password reset complete page.

![Password Reset Complete](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hghmk1sc1gzesh5t61jc.png)

To confirm everything worked correctly, navigate to the homepage and log in to your account with the new password.

If you want to customize the templates involved with password reset, they are located at the following locations; you need to create new template files to override them.

- `templates/registration/password_reset_confirm.html`
- `templates/registration/password_reset_form.html`
- `templates/registration/password_reset_done.html`

<a id="conclusion"></a>
## Conclusion
We've implemented a robust user authentication flow for our web app with login, logout, signup, password change, and password reset. Congrats!

The source code is available [on Github](https://github.com/wsvincent/django-auth-tutorial) if you need a reference or spot any issues with this tutorial. Please give the repo a star if you found it helpful.

Django requires more configuration than other web frameworks, but it allows for complete customization. That is the tradeoff involved. If everything worked magically, changing the look, feel, and functionality would be harder based on custom needs. 

What else might we want to add? If you want to learn how to customize user authentication fully, please check out my book, [Django for Beginners](https://djangoforbeginners.com), which covers this and many other projects while incrementally working through building multiple Django projects from scratch.