HyperSaaS
BackendAuthentication

Overview

Signing in with django-allauth's API mode, two-factor authentication, and djoser's account endpoints.

HyperSaaS uses two libraries for authentication:

LibraryPurpose
django-allauthSigning in (its API mode, at /_allauth/app/v1/), two-factor authentication, the API's tokens, and changing the password. Also the Django admin's sign-in when DJANGO_ADMIN_FORCE_ALLAUTH=True
djoserREST endpoints for sign-up, activation, password reset, email change and account deletion

The Next.js frontend signs people in through allauth: email and password, then a code from an authenticator app if they've turned on two-factor authentication. allauth's own sign-up, email verification and password reset endpoints are closed, because djoser handles those flows. There is no server-rendered sign-up either: allauth's pages are closed unless you set DJANGO_ACCOUNT_ALLOW_REGISTRATION=True, because an account created there would skip the API's activation flow.

Authentication Method

The REST API accepts allauth's JWTs only:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "allauth.headless.contrib.rest_framework.authentication.JWTTokenAuthentication",
    ),
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",
    ),
}

Send the access token as Authorization: Bearer <access_token>. Each token belongs to a sign-in (an allauth session), checked on every request, so signing out or changing the password ends its tokens at once. See Tokens for lifetimes and refresh.

Who can do what is decided by permissions, from the signed-in user and the workspace in the URL; the token only says who you are.

Custom User Model

HyperSaaS uses email as the identifier instead of a username:

class User(AbstractUser):
    name = models.CharField(max_length=255, blank=True)
    email = models.EmailField(unique=True)

    USERNAME_FIELD = "email"

    class Meta:
        constraints = [
            models.UniqueConstraint(Lower("email"), name="users_user_email_unique_ignoring_case"),
        ]

Emails are stored lowercase and looked up case-insensitively, so Ana@Example.com and ana@example.com are the same account.

allauth also keeps a record of each account's email (EmailAddress), and only lets a verified address turn on two-factor authentication. Activating an account and confirming an email change record the address as verified; when the email changes, the old address stops signing in.

Rate Limits

Signing in is limited by allauth, per account and per IP:

LimitDefault
Failed sign-ins for one account5 per 5 minutes
Failed sign-ins from one IP10 per minute
Wrong two-factor codesCount against the account's limit

Over a limit, allauth answers 400 with the code too_many_login_attempts, and the right password is refused too until the window passes. The Next.js server makes these calls for every user, so it passes the browser's IP on in an X-Client-IP header; without it, every sign-in would count against the server's own IP.

djoser's account endpoints (sign-up, activation, password and email resets) are limited per account, by the email or uid in the request:

SettingDefaultCovers
THROTTLE_AUTH_ATTEMPTS10/minutedjoser's account endpoints

Over that limit, the API returns 429. Password reset and email reset answer the same whether or not the address is registered, so they can't be used to find out who has an account.

Password Hashing

HyperSaaS uses Argon2 as the primary password hasher:

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]

On this page