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

2023-01-19

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

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:

Advantages of this approach

Disadvantages / Where it doesn't work

This approach probably is not for you if...

The solution in a nutshell

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:

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:

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!