Overview
Signing in with django-allauth's API mode, two-factor authentication, and djoser's account endpoints.
HyperSaaS uses two libraries for authentication:
| Library | Purpose |
|---|---|
| django-allauth | Signing 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 |
| djoser | REST 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:
| Limit | Default |
|---|---|
| Failed sign-ins for one account | 5 per 5 minutes |
| Failed sign-ins from one IP | 10 per minute |
| Wrong two-factor codes | Count 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:
| Setting | Default | Covers |
|---|---|---|
THROTTLE_AUTH_ATTEMPTS | 10/minute | djoser'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",
]