# How to Enforce Password Expiry and Uniqueness with Authentik's Password Policies

> Learn how to enforce password expiry and uniqueness with Authentik's powerful password policies. Secure your accounts and prevent password reuse today.

- Repository: [Authentik Security/authentik](https://github.com/goauthentik/authentik)
- Tags: how-to-guide
- Published: 2026-08-14

---

**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`](https://github.com/goauthentik/authentik/blob/main/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

```typescript
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`](https://github.com/goauthentik/authentik/blob/main/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`](https://github.com/goauthentik/authentik/blob/main/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

```typescript
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`](https://github.com/goauthentik/authentik/blob/main/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`](https://github.com/goauthentik/authentik/blob/main/authentik/policies/models.py)) to connect your policy to the execution path.

### Binding a Policy to a Flow

```python
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`](https://github.com/goauthentik/authentik/blob/main/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`](https://github.com/goauthentik/authentik/blob/main/authentik/policies/expiry/models.py) | Implements `PasswordExpiryPolicy` with `days` and `deny_only` fields |
| Expiry API | [`authentik/policies/expiry/api.py`](https://github.com/goauthentik/authentik/blob/main/authentik/policies/expiry/api.py) | REST endpoints for CRUD operations |
| Uniqueness Model (Enterprise) | [`authentik/enterprise/policies/unique_password/models.py`](https://github.com/goauthentik/authentik/blob/main/authentik/enterprise/policies/unique_password/models.py) | Implements `UniquePasswordPolicy` and `UserPasswordHistory` |
| Uniqueness API (Enterprise) | [`authentik/enterprise/policies/unique_password/api.py`](https://github.com/goauthentik/authentik/blob/main/authentik/enterprise/policies/unique_password/api.py) | REST endpoints for enterprise policy |
| Policy Base Class | [`authentik/policies/models.py`](https://github.com/goauthentik/authentik/blob/main/authentik/policies/models.py) | Defines `Policy` abstract base and `PolicyBinding` for attachments |
| Prompt Stage Context | [`authentik/stages/prompt/stage.py`](https://github.com/goauthentik/authentik/blob/main/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`](https://github.com/goauthentik/authentik/blob/main/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.