Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
828440e
Implement password policy with hook
JolienTrog Aug 26, 2025
3e7a2cf
fix code sniffer violations
JolienTrog Aug 26, 2025
ed78365
fix code sniffer violations
JolienTrog Aug 26, 2025
bb96ba7
fix code sniffer violations
JolienTrog Aug 26, 2025
174c59c
Update application/forms/Account/ChangePasswordForm.php
JolienTrog Aug 26, 2025
93c6479
Update application/forms/Account/ChangePasswordForm.php
JolienTrog Aug 26, 2025
4ab7f2f
add constructor for password policy objekt
JolienTrog Aug 26, 2025
9175c4d
add constructor to PasswordValidator and codereview changes
JolienTrog Aug 27, 2025
1215b94
fix codesniffer
JolienTrog Aug 27, 2025
0a44791
Implement 'None Password Policy'-class for consistency
JolienTrog Aug 28, 2025
8f49073
fix codesniffer
JolienTrog Aug 28, 2025
43d6483
fix if no password policy is set in config.ini
JolienTrog Aug 28, 2025
02c067e
fix codesniffer
JolienTrog Aug 28, 2025
740837f
changes codereview
JolienTrog Aug 28, 2025
64d7036
codereview
JolienTrog Aug 28, 2025
b3a1a31
codereview
JolienTrog Aug 28, 2025
d2d91d2
codereview
JolienTrog Aug 28, 2025
2a10671
Change Class Default in CommonPasswordPolicy
JolienTrog Sep 1, 2025
d7acd7c
Unittest for Classes Password Policy
JolienTrog Sep 2, 2025
33bb7ba
codereview: Improve type hints and return types
JolienTrog Sep 2, 2025
9d8cbcd
codereview
JolienTrog Sep 10, 2025
2682e0f
add documantation for general and custom password policy
JolienTrog Sep 10, 2025
91f20c8
codereview
JolienTrog Sep 11, 2025
ba6972d
codereview change description methods typ to return string or null
JolienTrog Sep 11, 2025
6b2ffd0
implement Password Policy Helper Class to reduce code duplication
JolienTrog Sep 29, 2025
71090ae
fix failed unittest
JolienTrog Oct 6, 2025
d7e36e3
Handle invalid password policy config
JolienTrog Oct 14, 2025
76bacb6
Fix code sniffer violation
JolienTrog Oct 14, 2025
29b74f1
Apply code review suggestions
JolienTrog Nov 6, 2025
47acabf
Rearrange helper, split hook and interface
JolienTrog Nov 21, 2025
409137e
Adjust validation to accept oldPassword
JolienTrog Jan 26, 2026
81e6859
Adjust Test with second value
JolienTrog Jan 26, 2026
7193bab
code-review
JolienTrog Jan 30, 2026
4dabe41
Update application/forms/Config/General/PasswordPolicyConfigForm.php
JolienTrog Mar 20, 2026
c29a90a
Update doc/05-Authentication.md
JolienTrog Mar 20, 2026
74e549d
Update test/php/library/Icinga/Application/AnyPasswordPolicyTest.php
JolienTrog Mar 20, 2026
ca5f843
Update library/Icinga/Authentication/PasswordPolicyValidator.php
JolienTrog Mar 20, 2026
812646e
Update library/Icinga/Authentication/PasswordPolicyValidator.php
JolienTrog Mar 20, 2026
315570f
Update library/Icinga/Authentication/PasswordPolicyValidator.php
JolienTrog Mar 20, 2026
b022b18
Update doc/05-Authentication.md
JolienTrog Mar 20, 2026
408f1e5
Update doc/05-Authentication.md
JolienTrog Mar 20, 2026
1380632
Update doc/05-Authentication.md
JolienTrog Mar 20, 2026
37e7ed6
Update library/Icinga/Application/ProvidedHook/AnyPasswordPolicy.php
JolienTrog Mar 20, 2026
5f229e1
Update library/Icinga/Application/ProvidedHook/CommonPasswordPolicy.php
JolienTrog Mar 20, 2026
a71c4d8
Update library/Icinga/Authentication/PasswordPolicy.php
JolienTrog Mar 20, 2026
2c197eb
Update library/Icinga/Authentication/PasswordPolicyHelper.php
JolienTrog Mar 20, 2026
abef7e6
Apply suggestions from code review
JolienTrog Mar 20, 2026
d063739
Update ChangePasswordForm.php
JolienTrog Mar 20, 2026
5ed97f1
Update UserForm.php
JolienTrog Mar 20, 2026
42abffc
Update 05-Authentication.md
JolienTrog Mar 20, 2026
b8a2364
code-review changes
JolienTrog Mar 23, 2026
69d2d04
code sniffer changes
JolienTrog Mar 23, 2026
1ec7a38
code sniffer changes
JolienTrog Mar 23, 2026
a3bc5d9
Implement Password Policy in setup Guide
JolienTrog Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions application/forms/Account/ChangePasswordForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

Comment thread
Al2Klimov marked this conversation as resolved.
namespace Icinga\Forms\Account;

use Icinga\Authentication\PasswordPolicyHelper;
use Icinga\Authentication\User\DbUserBackend;
use Icinga\Data\Filter\Filter;
use Icinga\User;
Expand Down Expand Up @@ -46,10 +47,12 @@ public function createElements(array $formData)
'password',
'new_password',
array(
'label' => $this->translate('New Password'),
'required' => true
'label' => $this->translate('New Password'),
'required' => true
)
);
PasswordPolicyHelper::applyPasswordPolicy($this, 'new_password', 'old_password');

$this->addElement(
'password',
'new_password_confirmation',
Expand Down
39 changes: 39 additions & 0 deletions application/forms/Config/General/PasswordPolicyConfigForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

Comment thread
lippserd marked this conversation as resolved.
namespace Icinga\Forms\Config\General;

use Icinga\Application\Hook\PasswordPolicyHook;
use Icinga\Authentication\PasswordPolicyHelper;
use Icinga\Web\Form;

/**
* Configuration form for password policy selection
*
* This form is not used directly but as subform for the {@link GeneralConfigForm}.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
class PasswordPolicyConfigForm extends Form
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There already was some discussion with @lippserd to move this form into a new Security section in IW2.
We should consider converting this form into an ipl-web based form instead during this process.

{
public function init(): void
{
$this->setName('form_config_general_password_policy');
}

public function createElements(array $formData): static
{
$this->addElement(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see the description of the password policy somewhere in this form.
We display it in every place where a password field exists, why not where we actually select the policy?

'select',
sprintf('%s_%s', PasswordPolicyHelper::CONFIG_SECTION, PasswordPolicyHelper::CONFIG_KEY),
[
Comment thread
Al2Klimov marked this conversation as resolved.
'description' => $this->translate('Enforce password requirements for new passwords'),
'label' => $this->translate('Password Policy'),
'value' => PasswordPolicyHelper::DEFAULT_PASSWORD_POLICY,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having an invalid value set in the config (class not found or not an instance of PasswordPolicy) causes IW2 to display Common.
I would expect some sort of error message here.

'multiOptions' => PasswordPolicyHook::all()
]
);

Comment thread
lippserd marked this conversation as resolved.
return $this;
}
}
3 changes: 3 additions & 0 deletions application/forms/Config/GeneralConfigForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Icinga\Forms\Config\General\DefaultAuthenticationDomainConfigForm;
use Icinga\Forms\Config\General\LoggingConfigForm;
use Icinga\Forms\Config\General\ThemingConfigForm;
use Icinga\Forms\Config\General\PasswordPolicyConfigForm;
use Icinga\Forms\ConfigForm;

/**
Expand All @@ -32,9 +33,11 @@ public function createElements(array $formData)
$loggingConfigForm = new LoggingConfigForm();
$themingConfigForm = new ThemingConfigForm();
$domainConfigForm = new DefaultAuthenticationDomainConfigForm();
$passwordPolicyConfigForm = new PasswordPolicyConfigForm();
Comment thread
Al2Klimov marked this conversation as resolved.
$this->addSubForm($appConfigForm->create($formData));
$this->addSubForm($loggingConfigForm->create($formData));
$this->addSubForm($themingConfigForm->create($formData));
$this->addSubForm($domainConfigForm->create($formData));
$this->addSubForm($passwordPolicyConfigForm->create($formData));
}
}
5 changes: 5 additions & 0 deletions application/forms/Config/User/UserForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
namespace Icinga\Forms\Config\User;

use Icinga\Application\Hook\ConfigFormEventsHook;
use Icinga\Application\Logger;
use Icinga\Authentication\PasswordPolicyHelper;
use Icinga\Data\Filter\Filter;
use Icinga\Forms\RepositoryForm;
use Icinga\Web\Form\Element\Note;
use Icinga\Web\Notification;
use Throwable;

class UserForm extends RepositoryForm
{
Expand Down Expand Up @@ -42,6 +46,7 @@ protected function createInsertElements(array $formData)
'label' => $this->translate('Password')
)
);
PasswordPolicyHelper::applyPasswordPolicy($this, 'password');

$this->setTitle($this->translate('Add a new user'));
$this->setSubmitLabel($this->translate('Add'));
Expand Down
102 changes: 102 additions & 0 deletions doc/05-Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,108 @@ resource = icingaweb-mysql
Please read [this chapter](20-Advanced-Topics.md#advanced-topics-authentication-tips-manual-user-database-auth)
in order to manually create users directly inside the database.

### Password Policy <a id="authentication-password-policy"></a>
Comment thread
lippserd marked this conversation as resolved.

Icinga Web supports password policies when using database authentication.
You can configure this under **Configuration > Application > General**.

By default, no password policy is enforced (`Any`).
Icinga Web provides a built-in policy called `Common` with the following requirements:

* Minimum length of 12 characters
* At least one number
* At least one special character
* At least one uppercase letter
* At least one lowercase letter

#### Custom Password Policy <a id="authentication-custom-password-policy"></a>

You can create custom password policies by developing a module with a provided hook.

**Create Module Structure**
Comment thread
JolienTrog marked this conversation as resolved.

```bash
mkdir -p /usr/share/icingaweb2/modules/mypasswordpolicy/library/Mypasswordpolicy/ProvidedHook
cd /usr/share/icingaweb2/modules/mypasswordpolicy
```

Create `module.info`:
Comment thread
JolienTrog marked this conversation as resolved.

```ini
Module: mypasswordpolicy
Name: My Password Policy
Comment thread
lippserd marked this conversation as resolved.
Version: 1.0.0
Description: Custom password policy implementation
```

**Implement the Hook**

Icinga Web provides the `PasswordPolicyHook` with predefined methods
that simplify the extension of custom password policies.

Create `library/Mypasswordpolicy/ProvidedHook/PasswordPolicy.php`:

```php
<?php

namespace Icinga\Module\Mypasswordpolicy\ProvidedHook;

use Icinga\Application\Hook\PasswordPolicyHook;

class PasswordPolicy extends PasswordPolicyHook
Comment thread
lippserd marked this conversation as resolved.
{

public function getName(): string
{
return 'My Custom Policy';
}

public function getDescription(): string
{
return 'Custom password requirements: 8+ chars, 1 number';
}

public function validate(string $newPassword, ?string $oldPassword = null): array;
{
$violations = [];

if (strlen($newPassword) < 8) {
$violations[] = 'Password must be at least 8 characters';
}

if (! preg_match('/[0-9]/', $newPassword)) {
$violations[] = 'Password must contain at least one number';
}

if ($oldPassword !== null && hash_equals($oldPassword, $newPassword)) {
$violations[] = 'New password must be different from the old password';
}

return $violations;
}
}
?>
```

**Register the Hook**

Create `run.php`:
Comment thread
JolienTrog marked this conversation as resolved.

```php
<?php

use Icinga\Module\Mypasswordpolicy\ProvidedHook\MyPasswordPolicy;
Mypasswordpolicy::register();
?>
```


Enable the module:
Comment thread
JolienTrog marked this conversation as resolved.
```shell
icingacli module enable mypasswordpolicy
```

The custom policy will now appear in **Configuration > Application > General** under Password Policy.

## Groups <a id="authentication-configuration-groups"></a>

Expand Down
14 changes: 9 additions & 5 deletions library/Icinga/Application/ApplicationBootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
use DirectoryIterator;
use ErrorException;
use Exception;
use Icinga\Application\ProvidedHook\DbMigration;
use ipl\I18n\GettextTranslator;
use ipl\I18n\StaticTranslator;
use LogicException;
use Icinga\Application\Modules\Manager as ModuleManager;
use Icinga\Application\ProvidedHook\DbMigration;
use Icinga\Authentication\User\UserBackend;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError;
use Icinga\Exception\IcingaException;
use Icinga\Exception\NotReadableError;
use ipl\I18n\GettextTranslator;
use ipl\I18n\StaticTranslator;
use LogicException;
use Icinga\Application\ProvidedHook\AnyPasswordPolicy;
use Icinga\Application\ProvidedHook\CommonPasswordPolicy;

/**
* This class bootstraps a thin Icinga application layer
Expand Down Expand Up @@ -740,6 +742,8 @@ public function hasLocales()
protected function registerApplicationHooks(): self
{
Hook::register('DbMigration', DbMigration::class, DbMigration::class);
CommonPasswordPolicy::register();
AnyPasswordPolicy::register();

return $this;
}
Expand Down
42 changes: 42 additions & 0 deletions library/Icinga/Application/Hook/PasswordPolicyHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/* Icinga Web 2 | (c) 2025 Icinga GmbH | GPLv2+ */
Comment thread
JolienTrog marked this conversation as resolved.

namespace Icinga\Application\Hook;

use Icinga\Application\Hook;
use Icinga\Authentication\PasswordPolicy;

/**
* Base class for hookable password policies
*/
abstract class PasswordPolicyHook implements PasswordPolicy
{
/** @var string Hook name *(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** @var string Hook name *(
/** @var string Hook name */

This will not parse correctly!

protected const HOOK_NAME = 'PasswordPolicy';

/**
* Register password policy
*
* @return void
*/
public static function register(): void
{
Hook::register(self::HOOK_NAME, static::class, static::class);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Wait until Introduce \Icinga\Application\Hook\Essentials #5474 is merged
  2. Use that trait


/**
* Return all registered password policies sorted by name
*
* @return array<string, string>
*/
public static function all(): array
{
$passwordPolicies = [];
foreach (Hook::all('PasswordPolicy') as $class => $policy) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (Hook::all('PasswordPolicy') as $class => $policy) {
foreach (Hook::all(self::HOOK_NAME) as $class => $policy) {

We have the constant, why not use it?

$passwordPolicies[$class] = $policy->getName();
}
asort($passwordPolicies);

return $passwordPolicies;
}
}
33 changes: 33 additions & 0 deletions library/Icinga/Application/ProvidedHook/AnyPasswordPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Application\ProvidedHook;

use Icinga\Application\Hook\PasswordPolicyHook;
use ipl\I18n\Translation;

/**
* Policy to allow any password
*/
class AnyPasswordPolicy extends PasswordPolicyHook
{
use Translation;

public function getName(): string
{
// Policy is named 'None' to indicate that no password policy is enforced and any password is accepted.
return $this->translate('None');
}

public function getDescription(): ?string
{
return null;
}

public function validate(string $newPassword, ?string $oldPassword = null): array
{
return [];
}
}
64 changes: 64 additions & 0 deletions library/Icinga/Application/ProvidedHook/CommonPasswordPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Application\ProvidedHook;

use Icinga\Application\Hook\PasswordPolicyHook;
use ipl\I18n\Translation;

/**
* Common implementation of a password policy
*
* Enforces:
* - Minimum length of 12 characters
* - At least one number
* - At least one special character
* - At least one uppercase letter
* - At least one lowercase letter
*/
class CommonPasswordPolicy extends PasswordPolicyHook
{
use Translation;

public function getName(): string
{
return $this->translate('Common');
}

public function getDescription(): ?string
{
return $this->translate(
'Password requirements: minimum 12 characters, ' .
'at least 1 number, 1 special character, uppercase and lowercase letters.'
);
}

public function validate(string $newPassword, ?string $oldPassword = null): array
{
$violations = [];

if (mb_strlen($newPassword) < 12) {
$violations[] = $this->translate('Password must be at least 12 characters long');
}

if (! preg_match('/[0-9]/', $newPassword)) {
$violations[] = $this->translate('Password must contain at least one number');
}

if (! preg_match('/[^a-zA-Z0-9]/', $newPassword)) {
$violations[] = $this->translate('Password must contain at least one special character');
}

if (! preg_match('/[A-Z]/', $newPassword)) {
$violations[] = $this->translate('Password must contain at least one uppercase letter');
}

if (! preg_match('/[a-z]/', $newPassword)) {
$violations[] = $this->translate('Password must contain at least one lowercase letter');
}

return $violations;
}
}
Loading
Loading