What do Django's AUTH_PASSWORD_VALIDATORS check in a new project, and which Django code paths actually run them?
answer
- four entries from startproject
- length, similarity, common, numeric
- OPTIONS tune each one
- forms and commands call them
- set_password() does not
basics
~10 sA new project lists four validators: attribute similarity, minimum length (8 by default), common passwords and all-numeric. validate_password() runs them from Django's password forms and the createsuperuser and changepassword commands, not from set_password().
solid answer
~30 s`AUTH_PASSWORD_VALIDATORS` defaults to an empty list in `global_settings.py`, but `startproject` writes four entries: `UserAttributeSimilarityValidator` (rejects passwords too close to `username`, `first_name`, `last_name` or `email`, `max_similarity=0.7`), `MinimumLengthValidator` (`min_length=8`), `CommonPasswordValidator` (a bundled list of common passwords) and `NumericPasswordValidator` (rejects all-digit passwords). Each entry is a dict with `NAME` and optional `OPTIONS` passed to the constructor. They run only when code calls `password_validation.validate_password()`: Django's `UserCreationForm`, `SetPasswordForm`, `PasswordChangeForm`, `AdminPasswordChangeForm`, and the `createsuperuser` and `changepassword` commands do. `set_password()`, `create_user()` and your own serializers do not, so any other write path must call `validate_password(password, user)` itself.
code
python · 23 lines# settings.py
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 12},
},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# any write path outside Django's forms
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
def create_member(email, password):
user = get_user_model()(username=email, email=email)
validate_password(password, user) # raises ValidationError listing every failure
user.set_password(password)
user.save()
return usergo deeper
Recall the four validators startproject adds and that MinimumLengthValidator's default is 8 characters.
Explain the NAME and OPTIONS format, how validate_password() collects all errors, and which forms and commands call it.
Audit every password write path, especially API sign-up and admin tooling, for places that call set_password() or create_user() without validate_password().
Decide which rules belong in AUTH_PASSWORD_VALIDATORS versus a breached-password service or MFA, weighing user friction against the attacks you actually face.
## Two defaults that disagree It is easy to get this wrong in an interview because there are two different "defaults": | Where | Value | |---|---| | `django/conf/global_settings.py` | `AUTH_PASSWORD_VALIDATORS = []` - no validation at all | | `settings.py` written by `startproject` | four validators, listed below | A project generated by `django-admin startproject` therefore validates passwords, while a settings file written by hand, or one where someone deleted the block, does not. ## The four built-in validators | Validator | Rejects | Options | |---|---|---| | `UserAttributeSimilarityValidator` | passwords too similar to user attributes | `user_attributes` (default `username`, `first_name`, `last_name`, `email`), `max_similarity` (default `0.7`) | | `MinimumLengthValidator` | passwords shorter than the minimum | `min_length` (default `8`) | | `CommonPasswordValidator` | passwords found in a bundled gzip list of common passwords | `password_list_path` | | `NumericPasswordValidator` | passwords made only of digits | none | Each entry in the setting is a dict: `NAME` is the dotted path, and `OPTIONS`, if present, is passed as keyword arguments to the class. A misspelt `NAME` raises `ImproperlyConfigured` when the validators are first loaded. ## What validate_password() does `django.contrib.auth.password_validation.validate_password(password, user=None)` instantiates the configured validators, calls each one's `validate(password, user)`, collects every `ValidationError`, and raises them together, so a user sees all problems at once. Passing `user` matters: the similarity validator needs it to compare against the attributes. A validator class may also implement `password_changed(password, user)`, which `AbstractBaseUser.save()` calls after `set_password()` was used, useful for a password-history validator; and `get_help_text()`, which forms show under the field. ## Which code paths call it Django's own password-setting surfaces all go through it: - `BaseUserCreationForm` / `UserCreationForm` - sign-up and admin user creation; - `SetPasswordForm` - the reset confirmation step; - `PasswordChangeForm` - a signed-in user changing their password; - `AdminPasswordChangeForm` - staff setting a password in the admin; - `manage.py createsuperuser`, which offers to bypass a failure interactively, and `manage.py changepassword`, which re-prompts and aborts after repeated failures. What does **not** call it: 1. `user.set_password()` and `make_password()` - they only hash; 2. `UserManager.create_user()` and `create_superuser()` called from code; 3. data migrations, fixtures and management commands you write; 4. API endpoints built with a serializer or schema, unless the code calls `validate_password()` explicitly. That gap is the classic interview trap: a team adds a JSON sign-up endpoint that calls `create_user()` and quietly accepts `123`. ## Writing your own validator A validator is any class with `validate(self, password, user=None)` that raises `ValidationError` with a `code`, plus `get_help_text()`. Add it to the setting like the built-ins; no base class is required. Keep validators cheap and deterministic, because they run on every form submission that sets a password. ## Error messages and help text Each built-in validator raises `ValidationError` with a stable `code`, which makes failures easy to test and to map in an API: - `MinimumLengthValidator` uses `password_too_short`; - `NumericPasswordValidator` uses `password_entirely_numeric`; - the similarity and common-password validators use their own codes in the same style. `password_validators_help_texts()` returns the list of help texts for the configured validators, which Django's password forms render under the field so users see the rules before they type. Custom validators should implement `get_help_text()` for the same reason. ## Configuration example Raising the minimum length for a service whose users are staff with password managers is a one-line `OPTIONS` change, shown in the code example. Validators never re-check existing passwords; they apply the next time a password is set.
- Why pass the user object to validate_password() when the user is not saved yet?`UserAttributeSimilarityValidator` compares the password with the user's `username`, `first_name`, `last_name` and `email`. An unsaved instance already carries those values, so building it first lets the validator reject a password like the member's own email address. Without `user`, that validator has nothing to compare and silently passes.
saying these in an interview costs you the question
- set_password() refuses passwords that fail AUTH_PASSWORD_VALIDATORS
- Django enforces the four validators even when the setting is removed
- MinimumLengthValidator defaults to 12 characters
- Validators re-check existing passwords when the setting changes
- validate_password() stops at the first failing validator