Filter concepts by levelShowing all levels.

Django · Authentication

Authentication

The User model, login/logout and backends, password management, and staff/superuser/groups/permissions

  • Decide on a custom user model earlyswapping AUTH_USER_MODEL after the first migrate is not a safe operation
  • authenticate() checks; login() establishes the sessiontwo separate functions — never call login() on an unverified value
  • set_password() hashes; never assign .password directlya direct assignment stores exactly what was given, hashed or not
  • is_superuser bypasses every permission checkis_staff only gates admin-site access, not what a user may do there

1.The User model, and swapping it for a custom one

django.contrib.auth.models.User is the default user model — username, email, password (hashed), first_name/last_name, is_staff/is_superuser/is_active, and groups/permissions. AUTH_USER_MODEL in settings.py points at whichever user model is actually in use; swapping it to a custom model (extending AbstractUser for a mostly-default shape, or AbstractBaseUser for a from-scratch one, e.g. email-only login) must happen BEFORE the first migration, since every other app's ForeignKey(User) actually resolves through this setting.

Key features

  • AUTH_USER_MODEL (default "auth.User") is the single source of truth for which model is "the" user model — every internal Django reference goes through it, never a hardcoded import
  • get_user_model() returns the actual active user model at runtime — this is the correct way to reference it in code, not `from django.contrib.auth.models import User`
  • A ForeignKey to the user model should be written as settings.AUTH_USER_MODEL (a string, avoiding a circular import) — not a direct import of User, which breaks the moment AUTH_USER_MODEL is swapped
  • AbstractUser extends the default fields/behavior (username, email, is_staff, etc.) — appropriate when the default shape is close to what's needed, just extended
  • AbstractBaseUser provides only password hashing and the authentication API surface, no fields at all — appropriate for a from-scratch shape (e.g. email-only login, no username field)
  • Swapping AUTH_USER_MODEL after the first migrate has already run against the default User is NOT a supported, low-risk operation — Django's own docs recommend deciding this before any migrations exist

AbstractUser vs AbstractBaseUser

AbstractUser vs AbstractBaseUser
Base classStarts withUse when
AbstractUserAll default User fields (username, email, names, flags)The default shape is right, just needs an extra field or two
AbstractBaseUserOnly password hashing + auth API, no other fieldsA genuinely different shape — e.g. email as the only login identifier
Every internal reference goes through AUTH_USER_MODEL

settings.py

AUTH_USER_MODEL

"accounts.User"

Django internals

get_user_model()

ForeignKey(settings.AUTH_USER_MODEL)

accounts/models.py

class User(AbstractUser)

  • settings.py
    • AUTH_USER_MODEL — "accounts.User"
  • Django internals
    • get_user_model()
    • ForeignKey(settings.AUTH_USER_MODEL)
  • accounts/models.py
    • class User(AbstractUser)

Command

python
# settings.py
AUTH_USER_MODEL = "accounts.User"

# accounts/models.py
class User(AbstractUser):
    ...

AUTH_USER_MODEL + get_user_model() is the indirection Django uses everywhere internally — never import User directly. Decide on a custom user model (AbstractUser for default-shape-plus-fields, AbstractBaseUser for a from-scratch shape) BEFORE the first migration; swapping later is not a safe, casual operation.

2.Login, logout, and authentication backends

authenticate(request, username=..., password=...) checks credentials against every backend listed in AUTHENTICATION_BACKENDS (default: just ModelBackend, which checks username+password against the user model) and returns a User or None — it does NOT log anyone in. login(request, user) is the separate step that actually establishes the session, storing the user's id and (since Django tracks it) which backend authenticated them. logout(request) clears the session. Session authentication means the ongoing, per-request "who is this" comes from request.session, populated by AuthenticationMiddleware reading the session cookie — not from re-checking a password on every request.

Key features

  • authenticate(request, **credentials) checks credentials against AUTHENTICATION_BACKENDS and returns a User instance or None — it never touches the session
  • login(request, user) is the separate call that actually establishes the session — always call it only after authenticate() (or equivalent verification) succeeds, never on an unverified user object
  • AUTHENTICATION_BACKENDS defaults to ["django.contrib.auth.backends.ModelBackend"] — a list, tried in order, so multiple authentication mechanisms (SSO, API keys, legacy hashes) can coexist
  • logout(request) clears the entire session, not just the auth-related keys — any other session data is lost too, unless explicitly preserved beforehand
  • ModelBackend authenticates against USERNAME_FIELD (username by default, email if a custom user model sets USERNAME_FIELD = "email") — "username or email login" in practice means either a custom backend or setting USERNAME_FIELD, not a special authenticate() flag
  • request.user is only populated because AuthenticationMiddleware runs on every request, reading the session established at login — removing that middleware means request.user is never set

authenticate() vs login() vs logout()

authenticate() vs login() vs logout()
FunctionDoes
authenticate(request, **credentials)checks credentials against AUTHENTICATION_BACKENDS, returns User or None — no session change
login(request, user)establishes the session for an already-verified user
logout(request)clears the entire session
authenticate() checks credentials; login() is the separate step that starts the session
Browser
login_view
authenticate()
Session
  1. 1POST username + password
  2. 2checks AUTHENTICATION_BACKENDS
  3. 3User or None
  4. 4login(request, user) — only if not None
  1. Browser → login_view: POST username + password
  2. login_view → authenticate(): checks AUTHENTICATION_BACKENDS
  3. authenticate() → login_view: User or None
  4. login_view → Session: login(request, user) — only if not None

Command

python
user = authenticate(request, username=..., password=...)
if user is not None:
    login(request, user)
...
logout(request)

authenticate() checks credentials and returns a user or None — it never touches the session. login(request, user) is the separate step that establishes it; only ever call it on a value authenticate() (or equivalent) actually verified. AUTHENTICATION_BACKENDS is a list tried in order, letting multiple auth mechanisms coexist. request.user only exists because AuthenticationMiddleware reads the session on every request.

3.Password hashing/validation/reset, and staff/superuser/groups/permissions

Django never stores a plain password — set_password()/check_password() run it through PASSWORD_HASHERS (PBKDF2 by default, a slow, salted algorithm designed to resist brute-forcing). AUTH_PASSWORD_VALIDATORS runs a separate set of rules (minimum length, not too similar to the user's own attributes, not a common password, not all-numeric) at signup/change time, not at login. Password reset uses a signed, time-limited, single-use token (PasswordResetTokenGenerator) emailed to the user, so a reset link works without ever exposing or requiring the old password. is_staff gates admin-site access; is_superuser bypasses ALL permission checks; Groups bundle permissions for reuse across many users; individual Permissions (model-level, or custom ones) are the actual unit django.contrib.auth checks against.

Key features

  • set_password(raw) hashes and stores; check_password(raw) verifies against the stored hash — a User is never saved with a plaintext password in either direction
  • PASSWORD_HASHERS is an ordered list — the first entry hashes NEW passwords, but Django can still verify against a hash produced by any hasher later in the list, supporting a gradual algorithm migration
  • AUTH_PASSWORD_VALIDATORS run at password set/change time (forms, not the User model itself) — a directly-created User via the ORM without going through a form/serializer bypasses them entirely
  • PasswordResetTokenGenerator tokens are signed and encode a timestamp — they expire after PASSWORD_RESET_TIMEOUT (default 259200 seconds / 3 days) and are invalidated by certain user field changes (e.g. the password itself, or last_login in Django 5.x), making them naturally single-use
  • is_superuser bypasses EVERY permission check, including custom ones evaluated via has_perm() — it does not just imply "all default permissions", it skips the check outright
  • A Group is just a named bundle of Permission objects — user.has_perm() checks both permissions assigned directly to the user AND permissions inherited via any Group they belong to

The four access-control layers

The four access-control layers
LayerControls
is_staffcan log into /admin/ at all
is_superuserbypasses every permission check entirely
Groupsreusable named bundles of permissions, assigned to many users at once
Permissionsthe actual unit checked by has_perm() — model-level (add/change/delete/view) or custom
Four access-control layers, coarsest to most granular

is_superuser

bypasses every permission check entirely

is_staff

can log into /admin/ at all

Groups

reusable named bundles of permissions

Permissions

the actual unit has_perm() checks

  1. is_superuser — bypasses every permission check entirely
  2. is_staff — can log into /admin/ at all
  3. Groups — reusable named bundles of permissions
  4. Permissions — the actual unit has_perm() checks

Command

python
user.set_password(raw_password)
user.check_password(raw_password)
user.has_perm("app_label.codename")

set_password()/check_password() are the only supported way to write/verify a password — never assign user.password directly. AUTH_PASSWORD_VALIDATORS run at set/change time via validate_password(), not automatically on every save(). is_staff gates admin access; is_superuser bypasses ALL permission checks; Groups are reusable, named permission bundles; has_perm() is the actual unit checked, direct or via a group.

Remember

  • AUTH_USER_MODEL + get_user_model() is the indirection every Django internal uses — never import User directly, and decide on a custom model before the first migration.
  • authenticate() returns a user or None without touching the session; login(request, user) is the separate step that establishes it — only ever call it on an already-verified value.
  • set_password()/check_password() are the only supported way to write/verify a password; AUTH_PASSWORD_VALIDATORS run at set/change time via validate_password(), not on every save().
  • is_staff gates admin access, is_superuser bypasses every permission check, Groups bundle reusable Permissions, and has_perm() is the actual unit checked — direct or via a group.

Pro tip

  • PASSWORD_HASHERS is an ordered list — Django can verify against any hasher later in the list even while only using the first to hash new passwords, supporting a gradual algorithm migration.
  • Password reset tokens are signed and time-limited (PASSWORD_RESET_TIMEOUT, default 3 days) rather than a stored value — the link itself is the temporary proof of access, never the old password.

Next up

  • Authorization
  • Sessions and Cookies
  • Middleware

Sources

  • User authentication in Django — docs.djangoproject.com/en/5.2/topics/auth/
  • Every claim checked against Django 5.2 LTS docs · checked 2026-08-24

Spotted a mistake on this page? Tell us.