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:

  1. Retrieves the last N entries from UserPasswordHistory linked to the user
  2. Uses Django's identify_hasher to compare each stored hash with the candidate password
  3. 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() returns PolicyResult(True), flow continues normally
  • If passes() returns PolicyResult(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 through days and optionally invalidates expired passwords via deny_only=False
  • Password Uniqueness Policy (UniquePasswordPolicy, Enterprise only) prevents reuse of the last N passwords stored in UserPasswordHistory
  • Both policies inherit from the base Policy class and implement a passes() method returning PolicyResult
  • Attach policies to flows or prompt stages using PolicyBinding from authentik/policies/models.py
  • Policies access submitted passwords through PLAN_CONTEXT_PROMPT provided 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →