How to Enforce Password Expiry and Uniqueness with Authentik's Password Policies
Authentik provides two dedicated password policy types—PasswordExpiryPolicy and UniquePasswordPolicy—that you attach to authentication flows or prompt stages to automatically enforce password-age limits and prevent password reuse.
Authentik's policy framework lets you enforce enterprise-grade password security without custom code. The open-source edition includes password expiry controls, while password uniqueness enforcement requires an enterprise license. Both policies integrate directly into authentication flows and evaluate automatically when users create or update credentials.
Password Expiry Policy in Authentik
The Password Expiry Policy invalidates passwords after a configurable number of days. You can optionally force immediate password resets or simply deny login until the user updates their credentials.
Core Implementation
The policy lives in authentik/policies/expiry/models.py where the PasswordExpiryPolicy model defines two critical fields:
| Field | Type | Purpose |
|---|---|---|
days |
Integer | Maximum password age before enforcement triggers |
deny_only |
Boolean | If True, blocks login; if False, marks password unusable and forces reset |
When evaluated, the passes() method compares request.user.password_change_date against the current date. If elapsed days exceed the threshold, the policy returns a PolicyResult with passing=False and an appropriate error message. When deny_only=False, the code calls user.set_unusable_password() to invalidate the current credential.
Creating a Password Expiry Policy via API
import { PolicyApi, PasswordExpiryPolicy } from "@goauthentik/api";
const api = new PolicyApi(/* …auth config… */);
async function createExpiryPolicy() {
const policy: PasswordExpiryPolicy = await api.policiesPasswordExpiryCreate({
name: "Standard password expiry",
days: 90, // expire after 90 days
deny_only: false, // invalidate password, force reset
});
console.log("Created expiry policy:", policy.id);
}
The REST endpoints are exposed through authentik/policies/expiry/api.py, which uses Django REST Framework to provide full CRUD operations.
Password Uniqueness Policy (Enterprise)
The UniquePasswordPolicy prevents users from reusing their last N passwords. This requires an Authentik Enterprise license and operates on a dedicated password history table.
Core Implementation
Located in authentik/enterprise/policies/unique_password/models.py, this policy stores configuration in UniquePasswordPolicy and historical hashes in UserPasswordHistory. The num_historical_passwords field configures how many previous passwords to check against.
The passes() method:
- Retrieves the last N entries from
UserPasswordHistorylinked to the user - Uses Django's
identify_hasherto compare each stored hash with the candidate password - Returns
PolicyResult(False, …)immediately on any match
Creating a Password Uniqueness Policy via API
import { PolicyApi, UniquePasswordPolicy } from "@goauthentik/api";
async function createUniquenessPolicy() {
const policy: UniquePasswordPolicy = await api.policiesUniquePasswordCreate({
name: "Disallow last 5 passwords",
num_historical_passwords: 5,
});
console.log("Created uniqueness policy:", policy.id);
}
API endpoints are defined in authentik/enterprise/policies/unique_password/api.py.
Attaching Policies to Authentication Flows
Policies only enforce when bound to flows or stages. Use Policy Binding (PolicyBinding in authentik/policies/models.py) to connect your policy to the execution path.
Binding a Policy to a Flow
from goauthentik.client import AuthentikClient
client = AuthentikClient(
base_url="https://auth.example.com",
token="YOUR_TOKEN"
)
client.policies.policybinding_create(
data={
"policy": policy_id, # UUID of your expiry or uniqueness policy
"flow": flow_id, # UUID of the target authentication flow
"order": 1, # Evaluation order among bindings
"negate": False, # Set True to invert the policy result
}
)
Binding to a Prompt Stage
For password change flows, attach policies directly to the Prompt Stage that collects the new password. The stage provides PLAN_CONTEXT_PROMPT (implemented in authentik/stages/prompt/stage.py) which policies access to retrieve the submitted password value.
When evaluation occurs:
- If
passes()returnsPolicyResult(True), flow continues normally - If
passes()returnsPolicyResult(False, "error message"), flow aborts and displays the error to the user
How Policies Receive Password Context
Both policies rely on the request context populated by prompt stages. The PLAN_CONTEXT_PROMPT dictionary contains key-value pairs from submitted form data, including the password field. Policies extract this value during passes() execution and perform their validation logic against it.
This architecture means policies are stage-agnostic—they work with any prompt stage that collects password data, whether in enrollment, recovery, or password change flows.
Key Configuration Files and Modules
| Component | File Path | Role |
|---|---|---|
| Password Expiry Model | authentik/policies/expiry/models.py |
Implements PasswordExpiryPolicy with days and deny_only fields |
| Expiry API | authentik/policies/expiry/api.py |
REST endpoints for CRUD operations |
| Uniqueness Model (Enterprise) | authentik/enterprise/policies/unique_password/models.py |
Implements UniquePasswordPolicy and UserPasswordHistory |
| Uniqueness API (Enterprise) | authentik/enterprise/policies/unique_password/api.py |
REST endpoints for enterprise policy |
| Policy Base Class | authentik/policies/models.py |
Defines Policy abstract base and PolicyBinding for attachments |
| Prompt Stage Context | authentik/stages/prompt/stage.py |
Populates PLAN_CONTEXT_PROMPT for policy evaluation |
Summary
- Password Expiry Policy (
PasswordExpiryPolicy) enforces maximum password age throughdaysand optionally invalidates expired passwords viadeny_only=False - Password Uniqueness Policy (
UniquePasswordPolicy, Enterprise only) prevents reuse of the last N passwords stored inUserPasswordHistory - Both policies inherit from the base
Policyclass and implement apasses()method returningPolicyResult - Attach policies to flows or prompt stages using
PolicyBindingfromauthentik/policies/models.py - Policies access submitted passwords through
PLAN_CONTEXT_PROMPTprovided by prompt stages
Frequently Asked Questions
How do I know if my password expiry policy is working?
Check the policy execution logs in the Authentik Admin UI under Events > Logs. Failed policy evaluations appear with the configured error message. You can also test by artificially setting days=0 and attempting login—this should immediately trigger enforcement.
Can I use password uniqueness without an Enterprise license?
No. The UniquePasswordPolicy and UserPasswordHistory models reside in authentik/enterprise/policies/unique_password/ and require an active Enterprise license. The open-source PasswordExpiryPolicy provides password aging controls as the community alternative.
What happens when a user tries to reuse a historical password?
The UniquePasswordPolicy.passes() method hashes the candidate password and compares it against stored historical hashes using Django's password hasher identification. On any match, it returns PolicyResult(False, …) with a configurable denial message, and the flow prevents password update until the user provides a unique value.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →