Kolja Sam Pluemer

Dead simple authentication with Django: Global password, one user, custom usernames

A guide on how to protect a Django webapp with a password while allowing custom user names

19.01.2023

While building an MVP recently, I found myself faced with a curious set of authentication requirements:

  • My app had to be password-protected (only people in the know should have access)
  • Users must be able to attach their name to the actions they are doing
  • There was no time or motivation for a full-blown authentication system

I struggled a bit with designing and implementing this, so on the off-chance future me or anyone else needs this, here is a quick write-up. We are going to use Python/Django.

What can this be used for?

In case you are confused what I am on about, here are two examples where this approach may be useful:

  • You have a webapp just for your friends, where it still matters who did what. Think chat room, greeting card designer or pen and paper tool.
  • Process automation in a small business, where employees log specific actions. Think “Eric logs machine XB3 as ‘needs cleaning’” “Anne confirms machine XB3 is cleaned”.

Advantages of this approach

  • Implementation speed: What I am about to showcase takes one or two hours of dev time max, if you know Django.
  • UX: Users basically have to do nothing.
  • Limited complexity: You don’t have to deal with all the bullshit that comes with proper user accounts: Password resets, confirmation emails, GDPR concerns. If it has user accounts it’s not a side-project, it’s an unpaid job.

Disadvantages / Where it doesn’t work

This approach probably is not for you if…

  • …the app has a large user base.
  • …user accounts are complex and have many dependencies in the database.
  • …impersonation is a concern.
  • …it is absolutely crucial that user names are consistent and predictable.

The solution in a nutshell

  • A single superuser is created, and all authentication runs via that account.
  • To get access to the app, one must fill in a simple login form
  • The password field must match with the password of the superuser, otherwise the login is rejected
  • The ‘username’ can be freely chosen and is saved in a cookie
  • This username cookie is used to log who does what in the backend
  • If the cookie is deleted or expired, we logout the user and redirect to the login form

General considerations

Before we get to the code, here a few general musings that might be useful even if your use case is dissimilar to mine:

Try to avoid JavaScript

When starting this project, I first experimented with localStorage and the like - being a client technology, JavaScript is required here. We are talking form.preventDefault(); and the like. However, this just creates headaches in the long run: Failure when the script doesn’t load, security gaps and an explosion of states.

If you use Django’s session and cookie framework, you don’t need no JS. Very convenient.

Use standards whenever possible

I briefly considered doing some, eh, Bad Things. Frustrated with every documentation and tutorial railroading me towards a super complex full-blow auth system, I considered just string-matching an input against a hardcoded password or something. However, shit like that is not only insecure, but also unnecessary: you can beat the given Django tools into submission. Built-in capabalities such as @login_required() are super powerful yet easy to use; so you use them as much as possible.

Implementation

Enough talk, here is what I did. It’s basically just two pages; login and the ‘main’ page that you are hiding from the public eye. You’ll need to create these two pages and make them accessible in urls.py. Also run python manage.py createsuperuser and take note of username and password you use.

Login form

Whenever I go the custom route in Django, I get annoyed with the effects rippling to everything from urls.py to forms.py to my template, so I just went for a custom page with a straight HTML form, bypassing all the usual stuff. You don’t have to do it like that, of course. Feel free to customize the given Django auth pages or do a custom form instead. Anyways, the template (in it’s most basic form):

    
 <form action="/" method="POST">
    {% csrf_token %}
    <input name="name" value="{{ name }}" />
    <input type="password" name="password" />
    <button type="submit">Submit</button>
  </form>
    

Of course you would be well advised to use labels and such, but this isn’t an accessibility tutorial. Before I explain, here is the logic (views.py):


from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout

def my_login_view(request):
    if request.method == 'GET':
        if request.user.is_authenticated:
            return redirect('protected_main_page')
        name = request.COOKIES.get('name') or ''
        return render(request, 'my_login_view.html', {'name': name})

    if request.method == 'POST':
        # the username is hardcoded and corresponds to the superuser we created
        username = 'admin'
        password = request.POST['password']
        user = authenticate(request, username=username, password=password)

        if user is not None:
            login(request, user)
            response = redirect('protected_main_page')
            # Name is not relevant for auth as such!
            name = request.POST['name']
            response.set_cookie('name', name)
            return response
        else:
            return redirect('/')

So what happens here? Let’s go step by step:

  • If it’s a GET request, that means we just loaded the page (as opposed to clicking Submit)
  • If the user is already authenticated, they don’t want to hang out at our login form. We redirect them to the main part of the app instead.
  • Otherwise, we do want to render our form. But first, we check if the chosen name of the user is still saved as a cookie. If so, we pass that to the template and prefill it in the form - see value="" in the template.
  • If it is a POST, the user has submitted the form
  • First, we check whether the password is valid - for that, use the name you have given your superuser instead of admin. We use the built-in function authenticate to confirm that the combination of the hard-coded username and the password the user has put in is indeed valid.
  • If yes, we use the login() function to actually create a valid authentication session
  • Before we redirect the now authenticated user, we get the name from the form and save it in a cookie. Now we can refer to the name of the user later! Don’t worry about the weird syntax with response, that’s just how you set cookies in Django.
  • If authenticate failed (returning user=None) we just redirect to the login again. Should probably add a message here or something so that the user knows what’s up.

Main page

The main page of the app is super simple. Well, probably it isn’t, because that is where the magic happens. However, the auth part is simple. You basically have a function like this in your views.py:

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

@login_required(login_url='/')
def protected_main_page(request):
    name = request.COOKIES.get('name')
    if name is None:
        logout(request)
        return redirect('/')

    return render(request, 'pages/protected_main_page.html', {'name': name})

We do three things here:

  • use the @login_required decorator to only allow access to the chosen few who are actually logged in
  • get the name (the one the user chose) from the cookie and send it with the template (so you can display it or whatever)
  • automatically sign out the user when the cookie is not found, because we don’t want anonymous users

Other settings

To make all the redirects work and access some useful functionality I recommend having this in your settings.py (feel free to change the values):

LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"

…and this in your urls.py’s urlpatterns=[]:

path("auth/", include("django.contrib.auth.urls")),

Now you can do stuff like adding a logout-link in any template:

<a href="{% url 'logout' %}" class="link">Logout</a>

…and it just works.

That’s all, have fun with this and feel free to message me when anything goes wrong. Cheerz!