Skip to main content

Command Palette

Search for a command to run...

Build a basic login system using Django

Updated
11 min read
Build a basic login system using Django
K
On kevincoder.co.za, I write about my journey as a developer working across Django, Go, and everything in between, from large-scale systems to small, useful tools. Programming has been my passion for over 15 years now. I love learning new skills and am thrilled I can share these with you! A big part of my career was built on knowledge shared by others; in the main, open-source projects, forums, and communities like Stack Overflow. This blog is my way of contributing back and sharing what I’ve learned along the way.

Many know and love Django admin (myself included). The problem is that it can be somewhat rigid, and well, let's be honest looks like it’s from 2010. Nevertheless, in Django, it’s stupidly simple to implement your own auth by extending the framework’s built-in views and templates.

In this article, let’s look at a quick-fire way of implementing an auth system in a matter of minutes.

A quick Django primer

This article is designed for developers who are familiar with Django, but just in case you are a complete beginner or just “rusty,” let’s go through some basics:

Virtual Environment:

To get started with Django development, you need a virtual environment. A virtual environment is simply an isolated Python installation that won’t interfere with the global Python installed on your system.

Virtual environments are especially useful on Linux machines. Since most Linux distros use Python for various core libraries, by installing your project packages at the system level, you could potentially break your whole desktop environment!

To set up a virtual environment for our project, run this in your terminal:

mkdir django_projects
cd django_projects
python -m venv .venv
source .venv/bin/activate

To exit the virtual environment, just type “deactivate” in your terminal, and to go back, simply run the last command as per above, ensuring you’re in the correct folder.

Now let’s install Django and create our project:

pip install django
django-admin startproject authsystem
cd authsystem

When you create a new Django project, you should see this folder structure:

.
├── authsystem
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

Very barebone and minimal, such is the beauty of Django! Inside your project, you will find a “manage.py” command, similar to the first “django-admin” command. This will allow you to perform various console tasks. The only difference is that “manage.py” is scoped to your project and will be aware of your project’s settings and apps.

Let’s create an application called “accounts”

python manage.py startapp accounts

Django has a very clean architecture in that you have projects and apps:

  • Projects: Are a collection of apps, usually your website project, or a library or framework that you're developing. They help organize related code and resources under a single umbrella, making it easier to manage dependencies, configuration, and deployments.

  • Apps: Are like small libraries, they should have one major purpose. Example: an "accounts" app manages everything related to user accounts, a “documents” app manages everything related to parsing and storing documents and so forth.

An app generally consists of:

├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py
  • admin.py: We won’t use the admin panel in this tutorial, but basically registering models here will create a CRUD interface in the Django admin panel for that model.

  • migrations: Any logic that needs to alter your database structure.

  • models.py: A local mapping of database tables using Django’s powerful ORM.

  • views.py: These are actually “controllers” when thinking in “MVC” terms. The “V,” by the way, in Django is actually the controller, and “T” is the templates, thus, Django is an “MVT” framework.

Django will not automatically register “apps” you generate; you’ll need to register the app manually in order to use this app within the project. Luckily, this is fairly easy to do, just add “accounts” to your settings.py file as follows:

# Update authsystem/settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "accounts"            # <---- the app we created.
]

While we’re in the settings file, let’s tell Django where to find our HTML templates.

First, let’s create the template folder. You should be at the project root at the same level as “authsystem” and “accounts”:

mkdir templates

Next, let’s update our settings file:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [f"{BASE_DIR}/templates"], # <---- Add template folder here.
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

Django is very flexible; you can place templates just about anywhere, including inside each individual app’s folder. Since the goal of this project is to be a single webapp, we are not building a library or shareable components, thus we’ll just use one templates folder at the project root.

Getting started

Great! Now that you have a project and an accounts app. Let’s first run migrations:

python manage.py migrate

By default, Django will use “sqlite” as its database, which makes local development much easier, as you don’t have to configure anything in order to get a database up and running. Since Django is ORM-based, it’s usually easy to just swap out the database type in production to something like PostgreSQL.

You will notice a whole bunch of migrations are actually run, even though we’ve not added a single model or database table???

  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

These are migrations for some Django admin features, including various tables for auth, such as permissions, groups, and a user table to store registered users. Sensible defaults you’ll use in nearly every project, so it’s a welcome addition almost always.

To get started with our custom login system, let’s add a few routes to authsystem/urls.py as follows:

💡 A cleaner way to add urls, is to use the “accounts” urls.py file and then import the “urlpatterns” in the main urls.py. I’m putting everything in the project’s urls.py for simplicity.

from django.contrib import admin
from django.urls import path

from accounts.views import account_login, account_logout, account_register,dashboard

urlpatterns = [
    path("login/", account_login, name="login"),
    path("register/", account_register, name="register"),
    path("logout/", account_logout, name="logout"),
    path("dashboard/", dashboard, name="dashboard"),
]

Your IDE or code editor might complain that these functions don’t exist, just ignore those for now. We’ll add them in a bit.

Building the login page

Before we get to the actual view functions, let me just list out all the imports we're going to need here so it’s easier for you to follow along instead of constantly repeating these:

from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import redirect, render

from .forms import RegisterForm

Now that we have the routes in place, let’s build the login view. In your “accounts/views.py” file, add the following:

def account_login(request):
    if request.user.is_authenticated:
        return redirect("dashboard")

    if request.method == "POST":
        username = request.POST.get("username")
        password = request.POST.get("password")
        user = authenticate(request, username=username, password=password)

        if user is not None:
            login(request, user)
            return redirect("dashboard")
        else:
            messages.error(request, "Invalid username or password.")

    return render(request, "accounts/login.html")

ℹ️ Notice I am prefixing some of these views with “account_”. This is because we import some functions from Django named “login”, “logout” and so on, thus, doing so just prevents clashing with those function names.

We first use request.user_is_authenticated to check if the user is logged in, and if they are, just redirect to the user dashboard page. Otherwise, we take 1 of 2 actions:

  • If the request is a POST request, it means that the user filled in their username and password. We should therefore validate those credentials. We use the auth helper function provided by Django authenticate , which will take care of hashing the password and comparing it with the password stored in the DB.

  • If it’s a GET request, just show the login form.

Let’s create a folder to store all of our auth templates:

mkdir -p templates/accounts
touch templates/accounts/login.html
touch templates/accounts/register.html
touch templates/accounts/dashboard.html

Next, paste this into the “login.html” template

{% if messages %}
<div class="mb-4">
    {% for message in messages %}
    <div>
        {{ message }}
    </div>
    {% endfor %}
</div>
{% endif %}

<form action="{% url 'login' %}" method="POST">
{% csrf_token %}

<label>Email Address</label> <br />
<input type="text" id="username" name="username" required /> <br />  

<label>Password</label> <br />
<input type="password" id="password" name="password" required /> <br />  
<input type="submit" value="Login" />
</form>

This is a very ugly, unstyled form, but I kept it simple so you can see what’s going on. Notice we also print messages; this is another useful Django feature. It’s basically a flash message where you can pass success/failure messages between views and between the view and the template.

Handy for showing error messages when the user doesn’t fill in something or fills in incorrect information.

Also note: we include {% csrf_token %} which is a security mechanism to protect your forms from “Cross-Site Request Forgery” attacks. Since this is a simple 2-field form, I didn’t create a form in “forms.py”, but usually, with Django, you would create a form class when dealing with user input.

Building the registration view

For this one, we are capturing quite a few fields, therefore, we’ll use Django forms as well to manage the form handling better. Let’s start off by creating a “forms.py” in the “accounts” folder:

touch accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

class RegisterForm(UserCreationForm):
    email = forms.EmailField(
        required=True,
        widget=forms.EmailInput()
    )
    first_name = forms.CharField(
        max_length=50,
        required=True,
        widget=forms.TextInput()
    )
    last_name = forms.CharField(
        max_length=50,
        required=True,
        widget=forms.TextInput()
    )

    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email', 'password1', 'password2')

    def save(self, commit=True):
        user = super().save(commit=False)
        user.username = self.cleaned_data.get('email')
        user.email = self.cleaned_data.get('email')
        user.first_name = self.cleaned_data.get('first_name')
        user.last_name = self.cleaned_data.get('last_name')

        if commit:
            user.save()
        return user

Django provides a UserCreationForm class that will take care of validation and hashing of the password, so we don’t have to reinvent the wheel here. We override the save method to ensure that the username is set to the user’s email address. This “username” field comes from an old school way of doing things when usernames were a thing; nowadays, most just use the email as a username.

Also, overriding save allows you to do more customizations, such as:

  • Setting a User profile. We’re using the default Django user model, but your application may need to store additional information such as birthdate, cellphone number, address, etc… In such a case, simply create a new model “UserProfile” and use the: models.OneToOneField relationship to link these models. Thereafter, you can store whatever extra information is needed via the save method.

  • Link a user to a Team. Great for SaaS type applications.

⚠️ It’s worth mentioning that Django’s username field has a max-length of 150 characters. This should be generally fine for most email addresses, however, you can always alter the column length using a migration. To get an idea on how, have a look in: .venv/lib/python3.12/site-packages/django/contrib/auth/migrations

Next, let’s build our register view, in views.py:

def account_register(request):
    if request.user.is_authenticated:
        return redirect("dashboard")

    if request.method == "POST":
        form = RegisterForm(request.POST)
        if form.is_valid():
            try:
                user = form.save()
                messages.success(
                    request, "Account created successfully. You can now log in."
                )
                return redirect("login")
            except Exception as e:
                messages.error(request, f"Error creating account: {e}")
    else:
        form = RegisterForm()

    return render(request, "accounts/register.html", {"form": form})

This is very similar to the login view, except we use a form this time. Finally, you need to paste this HTML in templates/accounts/register.html

<div>
  <h2>Register</h2>

  {% if messages %}
  <div>
    {% for message in messages %}
    <div>
      {{ message }}
    </div>
    {% endfor %}
  </div>
  {% endif %}

  <form  action="{% url 'register' %}" method="post" />
    {% csrf_token %}

    <div>
      <label for="{{ form.first_name.id_for_label }}">First Name</label>
      {{ form.first_name }}
      {% if form.first_name.errors %}
        <div>{{ form.first_name.errors }}</div>
      {% endif %}
    </div>

    <div>
      <label for="{{ form.last_name.id_for_label }}">Last Name</label>
      {{ form.last_name }}
      {% if form.last_name.errors %}
        <div>{{ form.last_name.errors }}</div>
      {% endif %}
    </div>

    <div>
      <label for="{{ form.email.id_for_label }}">Email</label>
      {{ form.email }}
      {% if form.email.errors %}
        <div>{{ form.email.errors }}</div>
      {% endif %}
    </div>

    <div>
      <label for="{{ form.password1.id_for_label }}">Password</label>
      {{ form.password1 }}
      {% if form.password1.errors %}
        <div>{{ form.password1.errors }}</div>
      {% endif %}
    </div>

    <div>
      <label for="{{ form.password2.id_for_label }}">Confirm Password</label>
      {{ form.password2 }}
      {% if form.password2.errors %}
        <div>{{ form.password2.errors }}</div>
      {% endif %}
    </div>

    <button type="submit">Register</button>
  </form>

  <div>
    <p>Already have an account? <a href="{% url 'login' %}">Log in</a></p>
  </div>
</div>

The Logout view

Logout is the simplest view of the 3, you simply just destroy the session and redirect to the login page:

def account_logout(request):
    logout(request)
    return redirect("login")

Restricting access

Now that you have a basic login system in place. We need a place for the user to be redirected to after login. Let’s create the dashboard view as follows in accounts/views.py

def dashboard(request):
    return render(request, "accounts/dashboard.html", {})

And some simple HTML in templates/accounts/dashboard.html:

<h1> Welcome {{request.user.first_name}} </h1>

One problem, though, if you visit /dashboard without logging in, you can still access this page! To restrict this page such that only logged-in users can access it, we simply need to import and use the @login_required decorator as follows:

... other imports here ....
from django.contrib.auth.decorators import login_required

@login_required(login_url="/login")
def dashboard(request):
   return render(request, "accounts/dashboard.html", {})

Now, you should get redirected to the login page. Easy right?

Conclusion

The Django auth system is really powerful and robust. This article was merely a gentle introduction, there’s so much more you can do, especially with authorization, decorators, and more.

Hopefully, this gives you a good enough start to build out your own auth system. Even though in the modern era, you probably could get some sort of boilerplate or use a 3rdy party auth service, it’s still very handy to understand the fundamentals of how auth systems work.

More from this blog

Kevin Coder | tutorials, thought experiments & tech ramblings

37 posts