Skip to content

Custom User Model

Authentication

How do I manage signup and login in Django?

Authentication is pretty complex. Thankfully, third-party packages are available to make this more manageable:

  1. django-allauth
  2. Authlib

I use allauth in this boilerplate so users are able to sign up via:

Authentication Mode Description
Email Email signup via email address and password
Social Signup via Google and Github

Maybe in the future, I can explore:

  1. login via mobile number
  2. magic email links (e.g. Slack)

User

Once a user is authenticated, a table in the database gets populated. What table? If I didn't modify the Django project, it would be the table represented by auth.User. The following setting however overrides the default:

Override default 'auth.User'
AUTH_USER_MODEL = "profiles.User" # (1)
  1. This overrides the default 'auth.User'. See reference

    From the docs:

    Some kinds of projects may have authentication requirements for which Django’s built-in User model is not always appropriate. For instance, on some sites it makes more sense to use an email address as your identification token instead of a username.

    The rationale for overriding the default can be found in a warning:

    You cannot change the AUTH_USER_MODEL setting during the lifetime of a project (i.e. once you have made and migrated models that depend on it) without serious effort. It is intended to be set at the project start, and the model it refers to must be available in the first migration of the app that it lives in...

    Elaborating further:

    Changing AUTH_USER_MODEL after you’ve created database tables is significantly more difficult since it affects foreign keys and many-to-many relationships, for example.

    This change can’t be done automatically and requires manually fixing your schema, moving your data from the old user table, and possibly manually reapplying some migrations...

Profile

I don't modify the new overriden User at all, preferring to create a dedicated Profile model / table that details the authenticated User.

profiles/models.py: the Profile Model
from django.contrib.auth.models import AbstractUser

class User(AbstractUser): # AbstractUser: username, email, first_name, last_name
    ... # deliberately empty, just an override (just in case)

class Profile(TimeStampedModel):
    ...
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True, null=True)
    image = models.ImageField(storage=select_storage, blank=True, null=True)
    ...

The user, more often than not, is asked on signup to agree to the site's terms and conditions prior to proceeding.

For purposes of compliance (I'm a lawyer), I'd like to be able to monitor consent and later on be able to make changes to the terms and conditions (and secure later consent for those changes as well). How do I go about this? I create another set of models in the pages app that map agreements to consenting users:

/src/pages/models.py: Consent
class Agreement(TimeStampedModel, TitleDescriptionModel):
    class Category(models.TextChoices):
        TERMS = ("terms", _("Terms of Service"))
        PRIVACY = ("privacy", _("Privacy Policy"))
    ... # fields related to the agreement

class UserConsent(TimeStampedModel):
    class Mode(models.TextChoices):
        SIGNUP = ("signup", _("Account Signup"))
        SOCIAL = ("social", _("Social Signup"))
        PROMPT = ("prompt", _("Logged-In Prompt"))
        BANNER = ("banner", _("Banner Pop-Up"))

    agreement = models.ForeignKey(Agreement, on_delete=models.CASCADE)
    user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    mode = models.CharField(
        max_length=7, choices=Mode.choices, default=Mode.SIGNUP
    )

Putting the User, Profile, and UserConsent together, I override django-allauth adapters for both email and social signup processes. This is what the overriden social adapter looks like:

/src/profiles/adapters.py
class ConsentSocialAdapter(DefaultSocialAccountAdapter):
    def save_user(self, request, sociallogin, form=ConsentSocialForm):
        u = super().save_user(request, sociallogin, form)
        consent = UserConsent.objects.create(
            user=u,
            mode=UserConsent.Mode.SOCIAL,
            agreement=Agreement.bind.updated_ver("terms"),
        )
        profile, _ = Profile.objects.get_or_create(user=u)
        profile = consent.user.profile  # type: ignore
        profile.first_name, profile.last_name = (u.first_name, u.last_name)
        profile.save(update_fields=["first_name", "last_name"])
        if not profile.image:  # profile's image field not yet populated
            if url := u.get_social_url():  # type: ignore
                background_store_img_url(url, profile.image, profile.im_key)
        return u

In sum, if you want to sign up, regardless of the mode, consent to the terms before registration.

User-Adjustable Profile Settings

Screenshot of the user settings page with various tabs

Screenshot of the user settings page with various tabs

The boilerplate saves time by pre-styling the otherwise vanilla UI templates of django-allauth. The user dashboard can be accessed by creating an account and logging in. In this dashboard, the following areas can be set:

Area Description
Personal Data name / bio fields
Email django-allauth-driven; enables addition and removal of email adds associated with the account
Password django-allauth-driven; sets password, for social login accounts, and enables changing it if the password already set
Social Login django-allauth-driven; connect, disconnect social accounts
Account Settings deletion, user avatar

Settings on the User Model

Separate settings for auth, email, social

Authentication
1
2
3
4
5
6
7
8
9
<root>
├── src/
    ├── config/ # project named config
        ├── settings/
            ├── __init__.py # switch env: dev | test | prod
            ├── _auth.py # auth, email, social
            ├── _settings.py # base settings
    ├── static/
...

There are a lot of settings related to authentication. So much so that I think it deserves its own settings file rather than being lumped together with everything else.

So for this boilerplate, I separate settings/_auth.py from the base settings/_settings.py. This makes it easier for me to make changes to a file devoted specifically to the user.