PHPackages                             choks/password-policy-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. choks/password-policy-bundle

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

choks/password-policy-bundle
============================

v2.0(2y ago)166GPL-3.0-or-laterPHPPHP &gt;=8.1CI failing

Since Feb 13Pushed 7mo agoCompare

[ Source](https://github.com/choks87/password-policy-bundle)[ Packagist](https://packagist.org/packages/choks/password-policy-bundle)[ RSS](/packages/choks-password-policy-bundle/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (2)Dependencies (14)Versions (3)Used By (0)

[![CI](https://github.com/choks87/password-policy-bundle/actions/workflows/ci.yml/badge.svg)](https://github.com/choks87/password-policy-bundle/actions/workflows/ci.yml/badge.svg)

What is this?
=============

[](#what-is-this)

Password Policy is a Symfony Bundle where you can validate user passwords against policy.

Pre-Requirements
================

[](#pre-requirements)

- PHP: &gt;=8.1
- Openssl PHP Extension
- Symfony 6 or 7

Installation
============

[](#installation)

Install via composer:

```
composer require choks/password-policy-bundle
```

Add to your bundles:

```
Choks\PasswordPolicy\PasswordPolicy::class => ['all' => true],
```

If you are using doctrine and DBAL storage, when generating schema, a table for storing Password History will be installed automatically. If don't, you will need to [create it manually](#table-for-storing-history).

Usage
=====

[](#usage)

Before we dig, let's explain how it works. First, you can use validation against policy on any object as long as that object implements `Choks\PasswordPolicy\Contract\PasswordPolicySubjectInterface`, which will require to implement `getIdentifier()` so we can distinguish owner of password when saving into Password History and `getPlainPassword()` so we can compare (why plain password?)

Basically there are two ways of validating:

- Manually, by calling service methods
- Automatically, by putting `#[Choks\PasswordPolicy\Atrribute\Listen]` on Doctrine Entity

And two ways of specifying the policy in your application:

- Via bundle configuration (or)
- Via your own Policy Provider

Defining policy
---------------

[](#defining-policy)

### Via Configuration

[](#via-configuration)

```
password_policy:
  policy:
    expiration:
      expires_after:
        unit: 'month' # Possible values are 'day', 'week', 'year' (Enum: Choks\PasswordPolicy\Enum\PeriodUnit)
        value: 1
    character:
      min_length: 8 # Minimum password length, leave null if you don't want to use
      numbers: 1 # At least how many numbers there? Leave null if you don't want to use
      lowercase: 1 # At least how many lowercase characters there? Leave null if you don't want to use
      uppercase: 1 # At least how many uppercase characters there?Leave null if you don't want to use
      special: 1 # At least how many special characters there? Leave null if you don't want to use
    # Password history policy is used when you want your passwords to be validated against previous passwords.
    # By default, History Policy is not used.
    history:
      # Provided password should not be used in 10 previous passwords. Leave null if you don't want to use
      not_used_in_past_n_passwords: 10
      # Period for which we should look in the past. Leave null if you don't want to use
      period:
        unit: 'month' # Possible values are 'day', 'week', 'year' (Enum: Choks\PasswordPolicy\Enum\PeriodUnit)
        value: 1
```

That's it, your configuration is set

### Via your own Policy Provider

[](#via-your-own-policy-provider)

Here is an example of your custom Policy Provider.

```
use Choks\PasswordPolicy\Contract\PolicyInterface;
use Choks\PasswordPolicy\Contract\PolicyProviderInterface;
use Choks\PasswordPolicy\Model\ExpirationPolicy;
use Choks\PasswordPolicy\Model\CharacterPolicy;
use Choks\PasswordPolicy\Model\HistoryPolicy;
use Choks\PasswordPolicy\Model\Policy;

final class MyCustomPolicyProvider implements PolicyProviderInterface
{
    public function getPolicy(UserInterface $user): PolicyInterface
    {
        // Assuming that you have your own way of storing Policy configuration, for example db.
        $policyData = $this->entityManager->getRepository()->yourOwnWayOfFetchingData();

        return new Policy(
            new ExpirationPolicy(/* Use your stored data to construct */),
            new CharacterPolicy(/* Use your stored data to construct */),
            new HistoryPolicy(/* Use your stored data to construct */),
        );
    }
}
```

Next step is to register this as service and then, put it to bundle config:

```
password_policy:
  policy_provider: MyCustomPolicyProvider::class # You can put your own provider here
```

Whenever you try to validate manually or automatically, this provider will be called to get Policy to use.

Checking Policy and manipulating Password History
-------------------------------------------------

[](#checking-policy-and-manipulating-password-history)

### Manually

[](#manually)

#### Checking against policy

[](#checking-against-policy)

You can get or inject Checker Service via ID `password_policy.checker` or `Choks\PasswordPolicy\Contract\PolicyCheckerInterface`. Let's say you are validating `$user` (Remember, $user has to implement `PasswordPolicySubjectInterface`)

```
$checker = $this->getContainer()->get('password_policy.checker')
$violations = $checker->validate($user);

if ($violations->hasErrors()) {
    // Do own stuff, you have violations to check error messages.
}
```

#### Adding to password history (if you use it)

[](#adding-to-password-history-if-you-use-it)

You can get or inject Password History service via ID `password_policy.history` or `Choks\PasswordPolicy\Contract\PasswordHistoryInterface`

```
$history = $this->getContainer()->get('password_policy.history')
$history->add($user); // This will write password into password history.
# ...
# also:

$history->clear(); // This will clear all passwords in history, for all users.
$history->remove($user); // This will clear all passwords in history, for specific User.
```

### Automatic checking

[](#automatic-checking)

Although, I would encourage to manually control flow of checking, it's much easier to let bundle do that for you. By adding `#[Listen]` attribute, you are expecting bundle to automatically:

- When User is being inserted or update (when flushed):
    - Validate password retrieved by `getPlainPassword()` against current policy,
    - Add password to history (using crypt, see complete config reference below) if history policy is used.
- When User is being removed (when flushed):
    - Remove passwords for user in history.

```
use Choks\PasswordPolicy\Contract\PasswordPolicySubjectInterface;
use Doctrine\ORM\Mapping as ORM;
use Choks\PasswordPolicy\Atrribute\PasswordPolicy;

#[PasswordPolicy]
#[ORM\Entity]
class User implements PasswordPolicySubjectInterface
{
    #[ORM\Id]
    #[ORM\Column]
    public int $id;

    public ?string $plainPassword = null;

    public function __construct(int $id, string $plainPassword = null)
    {
        $this->id            = $id;
        $this->plainPassword = $plainPassword;
    }

    public function getIdentifier(): string
    {
        return (string)$this->id;
    }

    public function setPlainPassword(#[\SensitiveParameter] ?string $plainPassword): User
    {
        $this->plainPassword = $plainPassword;

        return $this;
    }

    public function getPlainPassword(): ?string
    {
        return $this->plainPassword;
    }
}
```

At the momnent of saving entity, if validation fails, the `Choks\PasswordPolicy\Exception\PolicyCheckException` will be thrown. If you catch it, you can examine violations via `getViolations()`,

Clearing all password history
-----------------------------

[](#clearing-all-password-history)

In some cases, you want to clear all passwords from history. Probably after update of this bundle, or when you change policy or so. You can do that by executing a command:

```
bin/console password-policy:history:clear
```

Expiration
----------

[](#expiration)

You can set also set expiration policy. If you want to get expired password, or passwords, you can use:

```
  $expirationService = $this->getContainer()->get('password_policy.expiration');
  $password = $expirationService->getExpired($user); // Returns Choks\PasswordPolicy\ValueObject\Password
```

If you want to use it in your services, inject with autowire `Choks\PasswordPolicy\Contract\PasswordExpirationInterface`For more customization, you can also call method processExpired and `Choks\PasswordPolicy\Event\ExpiredPasswordEvent` will be dispatched, so you can listen and catch it, if last password is found to be expired:

```
$password = $expirationService->processExpired($user);
```

Why plain password? Is it safe?
-------------------------------

[](#why-plain-password-is-it-safe)

When using Symfony's `password_hashers` algo, could be and it is usually non-deterministic. What it means is that every hash for same plain password is different. Also, and usually, those hashing algorithms are one direction only, means that it cannot be decrypted/un-hashed.

Hahser also does not give us ability to compare Users plain password with some hashed one, it can only verify hashed user password using `UserPasswordHasherInterface` on User, against plain one.

We need to store encrypted password in history, and in order to do that, bundle is using its own crypt algo (That's why you have `cipher_method` in configuration. You can always choose different one.). When we compare user plain password with password in history, we are decrypting those and compare.

How you will deliver plain password via `getPlainPassword()` it's up to you, but I encourage you not to persist it, and if can use `eraseCredentials()` to unset it.

Note: If `getPlainPassword()` return NULL, every password policy operation will be skipped.

Configuration Reference
=======================

[](#configuration-reference)

```
password_policy:
  policy_provider: ConfigurationPolicyProvider::class # You can put your own provider here
  special_chars: "\"'!@#$%^&*()_+=-`~.,;:[]{}\\|" # Which characters are considered special chars
  trim: true # Should we trim given password?
  salt: '%env(APP_SECRET)%' # Salt used when encrypting passwords
  cipher_method: aes-128-ctr # Check https://www.php.net/manual/en/function.openssl-get-cipher-methods.php

  # This policy is what would be used in your application as policy, if you don't specify your own provider
  policy:
    expiration:
      expires_after:
        unit: 'month' # Possible values are 'day', 'week', 'year' (Enum: Choks\PasswordPolicy\Enum\PeriodUnit)
        value: 1
    character:
      min_length: 8 # Minimum password length (default is null)
      numbers: 1 # At least how many numbers there? (default is null)
      lowercase: 1 # At least how many lowercase characters there? (default is null)
      uppercase: 1 # At least how many uppercase characters there? (default is null)
      special: 1 # At least how many special characters there? (default is null)
    # Password history policy is used when you want your passwords to be validated against previous passwords.
    # By default, History Policy is not used.
    history:
      not_used_in_past_n_passwords: 10 # Provided password should not be used in 10 previous passwords.
      period: # Period for which we should look in the past.
        unit: 'month' # Possible values are 'day', 'week', 'year' (default is null)
        value: 1  (default is null)

  storage: # Only one storage can be defined. Storage is used to store password history
    dbal:
        table: 'password_history' # Name of the table where historic passwords should be stored.
        connection: 'default' # Doctrine DBAL connection name.
    cache:
      adapter: cache.app # your application cache adapter (see Symfony framework cache docs)
      key_prefix: 'password_history' # Prefix used for cache keys
```

Note: `not_used_in_past_n_passwords` and `period` could be used combined or independent (one set, other not). But in o order to use period, both unit and value must be set.

### Table for storing History

[](#table-for-storing-history)

If you are not using doctrine generate schema and in some case your table didn't get created, you can create it manually by this DDL:

```
CREATE TABLE password_history
(
    subject_id    VARCHAR(64)  NOT NULL,
    password      VARCHAR(128) NOT NULL,
    created_at    DATETIME     NOT NULL COMMENT '(DC2Type:datetime_immutable)'
)
    COLLATE = utf8mb4_unicode_ci;

CREATE INDEX IDX_F3521448B8E8428
    ON password_history (created_at);

CREATE INDEX IDX_F352144A76ED395
    ON password_history (subject_id);
```

What is planned to be done in future (not promised)?
====================================================

[](#what-is-planned-to-be-done-in-future-not-promised)

- Custom Policy provider per entity, defined via #\[Listen\]

Contribute
==========

[](#contribute)

Feel free to contribute, at any time. Please provide new tests or tests changed. Also, if you find some bug, open an issue and will try to fix it as soon as possible.

Todo
====

[](#todo)

- Garbage collection for passwords in history (FILO, per User)
- Support schema update without Doctrine ORM, without PostSchema listener

###  Health Score

31

—

LowBetter than 68% of packages

Maintenance43

Moderate activity, may be stable

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~16 days

Total

2

Last Release

803d ago

Major Versions

v1.0 → v2.02024-03-01

### Community

Maintainers

![](https://www.gravatar.com/avatar/ac580ac8e9fb7afc42de276664939a427f746cdc210f7348a41454cdbf0ecfc6?d=identicon)[choks87](/maintainers/choks87)

---

Top Contributors

[![choks87](https://avatars.githubusercontent.com/u/37302106?v=4)](https://github.com/choks87 "choks87 (10 commits)")

###  Code Quality

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/choks-password-policy-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/choks-password-policy-bundle/health.svg)](https://phpackages.com/packages/choks-password-policy-bundle)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.4k5.6M651](/packages/sylius-sylius)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.3M152](/packages/sulu-sulu)[prestashop/prestashop

PrestaShop is an Open Source e-commerce platform, committed to providing the best shopping cart experience for both merchants and customers.

9.0k15.4k](/packages/prestashop-prestashop)[contao/core-bundle

Contao Open Source CMS

1231.6M2.4k](/packages/contao-core-bundle)[ec-cube/ec-cube

EC-CUBE EC open platform.

78527.0k1](/packages/ec-cube-ec-cube)[kimai/kimai

Kimai - Time Tracking

4.6k7.4k1](/packages/kimai-kimai)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
