We have the bare minimum of CRUD operations working for the core project management section of the app, and while we’re nowhere near production quality, I feel like it’s about time I actually deploy it to a server so that I can access it from my laptop and phone from anywhere and start dogfooding, so to speak. Deploying early means I have the opportunity to catch and fix any deployment showstoppers before the codebase gets too unruly.

First thing I need to get working is auth, since deployment means the app is technically public-facing, which means we should put some access control in place so that randos can’t create/update/read/delete our test project management data while we’re using it. The goal is to eventually have this be a multi-user system with every user having private projects/tasks anyway.

We don’t currently have auth (or any kind of user model at all) because when I started, I was still getting my bearings with Django and I didn’t want to complicate things while trying to get it to do something that wasn’t in the tutorial. This may have been in error! Read on for details.

Basic auth principles

(skip this section if you already know about authentication and authorisation)

When I say “auth”, I mean “authentication and authorisation”. Authentication is the business of figuring out who you’re talking to; authorisation is the business of figuring out (and enforcing) the authority (permissions) someone has. The two go hand-in-hand. For web apps, this typically looks like:

  • A user is authenticated on login. We’re can be pretty sure the agent logging in is a given user that has previously registered an account because they provide the correct password that matches that user’s username (or similar). (Passwords can be stolen, sure – we can get more into the threat model later.)
  • When a user successfully logs in, they get given a session token that they can then present to the web app on subsequent actions that says that they have permission to perform a given action on behalf of the user that they’re “logged in” as.
    • The reason we use tokens is because there are lots of different small actions a user has permission to do that (basically) only they have permission to do (e.g. read or update or create or delete projects/tasks that they own) and getting them to provide their password every time they want to do such an action would get old really quickly. Providing a token that signals that the user has already been authenticated is more convenient.
  • This access token can be set to expire after some amount of time and should be invalidated when the user logs out.

It follows that we need a user model, which is going to get tied in to our existing models (e.g. task lists – every task list has an owner, and only that owner has permission to read and modify that task list [1]) and mechanisms for authentication (login & registration) and authorisation (access/session tokens) that interface with this new database of users.

Note: we need cryptography to implement this system – access tokens should be unforgeable and shouldn’t reveal e.g. the user’s password, it shouldn’t be possible to eavesdrop on the network traffic and read (and replay) the access token, etc. We don’t want to implement any of this stuff ourselves because we’ll get it wrong.

Fortunately, Django seems to have us covered.

Django’s auth system

We’re looking at django.contrib.auth.

Main barrier to getting this working is the whole user model thing. I reckon a user should just be able to log in with an email and password. I also want to have a “nickname” to informally refer to the user throughout the app; this doesn’t have to be unique. So a first pass of the model should be:

  • email: primary key, required
  • password hash: required
  • nickname: required

The existing User model has a username field, which is unique and is the primary key. I guess we could just copy the email address into it, but that seems like a bit of a hack. I don’t know for certain whether this model will be OK/elegant down the line, especially when (if?) we integrate OAuth 2 (e.g. “Sign in with Google”), and since migrating to a custom user model will be very painful once we get actual production data, I’m going to switch to a custom user model now. (“If you’re starting a new project, it’s highly recommended to set up a custom user model, even if the default User model is sufficient for you. This model behaves identically to the default user model, but you’ll be able to customize it in the future if the need arises.”)

Rambling/scratch

Need to make a manager for the user model. Was confused about how to create a user – how does the UserManager class know that it’s associated with the User class? Downloaded the Django source code and had a look around. Was interesting; the Manager class inherits from BaseManager.from_queryset(QuerySet) in db/models/manager.py. from_queryset() dynamically creates a class with all the QuerySet methods. Lots of clever OOP stuff going on under the hood I guess.

Anyway, the following seems to work:

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils import timezone

class UserManager(BaseUserManager):
    def create_user(self, email, nickname, password=None, **extra_fields):
        if not email:
            raise ValueError("Users must have an email address")
        user = User(
            email=self.normalize_email(email),
            nickname=nickname,
            **extra_fields
        )
        # will set to unusable password if password is None
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, nickname, password=None, **extra_fields):
        # override is_staff, is_superuser
        extra_fields["is_staff"] = True
        extra_fields["is_superuser"] = True

        return self.create_user(email, nickname, password, **extra_fields)

class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    nickname = models.CharField(max_length=50)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    # is_superuser provided by mixin
    date_joined = models.DateTimeField(default=timezone.now)

    USERNAME_FIELD = "email"
    EMAIL_FIELD = "email"
    REQUIRED_FIELDS = ["nickname"]

    objects = UserManager();

    def __str__(self):
        return f"{self.email} (\"{self.nickname}\")"

    def get_full_name(self):
        return self.email

    def get_short_name(self):
        return self.nickname

Update: self.model is a field of type class, not a method, and it’s documented on the Managers page.

Another thing to note is that Manager methods can access self.model to get the model class to which they’re attached.

So it could be used instead of the User() instantiation above.

Online tutorials for doing this stuff typically seem to put the custom user model in an app called accounts. I believe this is because a bunch of account-related URLs (e.g. LOGIN_URL) default to URLs starting with /account, but then a lot of the templates default to looking in places that start with /registration. I already created and registered an app called registration, so I’ll stick to that and hope nothing breaks.

Also had to make a UserAdmin thingo. I’m not going to bother with making the forms fancy for now:

from django.contrib import admin
# from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User

class UserAdmin(admin.ModelAdmin):
    # i'll get around to this later
    pass

admin.site.register(User, UserAdmin)

Can just delete the migrations for all the apps and the sqlite database and start over to set AUTH_USER_MODEL. Thank goodness for not being in production.

We can create a superuser via manage.py and log in to our admin site.

Upshot

Ok, cool, we have a basic auth model. Next steps: logging in and out, and preventing access to the main app functionality (CRUD) unless the user is logged in.

Postscript

Django provides easy ways of doing common webapp things, but using them requires asking yourself exactly what you need to achieve in your own webapp. At this stage of my development journey, I would prefer using something more primitive, as right now I keep having to decide whether the “easy” defaults are what I actually want or need anyway, and trace through exactly what those easy defaults are so I understand what’s going on. The MVC model is starting to sink into my brain though and I’m hoping once I get far enough in I’ll be happy I used Django.

Writing this devlog was somewhat annoying. Either I’m too verbose, and I’m writing paragraphs to explain what I’m doing to some hypothetical reader who has no relevant background, which is tedious and not very helpful for myself personally, or I’m just jotting things down as I go that make little sense to anyone other than myself, which is unsatisfying. I think I should be leaning towards the latter, though. I will say the act of writing has been keeping me on track somewhat.

Footnotes

[1] Although we might eventually want a more complicated access control system so that we can have task lists that are shared between multiple users.