CVE-2023-27580: Use of Password Hash With Insufficient Computational Effort (CWE-916)
value: CodeIgniter Shield provides authentication and authorization for the CodeIgniter 4 PHP framework. An improper implementation was found in the password storage process. All hashed passwords stored in Shield v1.0.0-beta.3 or earlier are easier to crack than expected due to the vulnerability. Therefore, they should be removed as soon as possible. If an attacker gets (1) the user's hashed password by Shield, and (2) the hashed password (SHA-384 hash without salt) from somewhere, the attacker may easily crack the user's password. Upgrade to Shield v1.0.0-beta.4 or later to fix this issue. After upgrading, all users’ hashed passwords should be updated (saved to the database). There are no known workarounds.
CWE description: The product generates a hash for a password, but it uses a scheme that does not provide a sufficient level of computational effort that would make password cracking attacks infeasible or expensive.
CVSS v3 base score: 5.9
Published: 2023-03-13T18:15Z
Repository: https://github.com/codeigniter4/shield
Language: PHP
Files changed: README.md, UPGRADING.md, docs/guides/strengthen_password.md, docs/index.md, mkdocs.yml, src/Authentication/Authenticators/Session.php, src/Authentication/Passwords.php, src/Authentication/Passwords/ValidationRules.php, src/Config/Auth.php, src/Controllers/LoginController.php, src/Controllers/RegisterController.php, src/Language/de/Auth.php, src/Language/en/Auth.php, src/Language/es/Auth.php, src/Language/fa/Auth.php, src/Language/fr/Auth.php, src/Language/id/Auth.php, src/Language/it/Auth.php, src/Language/ja/Auth.php, src/Language/pt-BR/Auth.php, src/Language/sk/Auth.php, src/Language/tr/Auth.php, tests/Authentication/Authenticators/SessionAuthenticatorTest.php, tests/Controllers/LoginTest.php, tests/Controllers/RegisterTest.php, tests/Unit/PasswordsTest.php
Commit date: 2023-03-10 23:01:18 -0600
Commit message: Merge pull request from GHSA-c5vj-f36q-p9vg

* feat: copy the current methods as depreacted

* fix: password shucking

* feat: support old dangerous passwords

* docs: add Upgrade Guide

* refactor: fix variable name typo

Co-authored-by: MGatner <mgatner@icloud.com>

* refactor: fix variable name typo

* fix: $options is missing for password_needs_rehash()

* docs: add OWASP Cheat Sheet in README

* docs: add explanation in UPGRADING.md

* docs: replace Config/Auth.php with app/Config/Auth.php

* docs: add How to Strengthen the Password

* docs: UPGRADING.md

* docs: add note in "Minimum Password Length"

* docs: $supportOldDangerousPassword will be removed in v1.0.0

* feat: add validation rule for max password length

PASSWORD_BCRYPT -> 72 bytes
Others -> 255 characters

* docs: update docs

* feat: add new lang item to new lang files

* feat: add errorPasswordTooLongBytes to Language/pt-BR

* refactor: ChangeIfElseValueAssignToEarlyReturnRector

---------

Co-authored-by: MGatner <mgatner@icloud.com>
Diff: diff --git a/README.md b/README.md
index d428cc9..3b148eb 100644
--- a/README.md
+++ b/README.md
@@ -84,5 +84,6 @@ The following articles/sites have been fundamental in shaping the security and b
 within this library, in no particular order:
 
 - [Google Cloud: 13 best practices for user account, authentication, and password management, 2021 edition](https://cloud.google.com/blog/products/identity-security/account-authentication-and-password-management-best-practices)
 - [NIST Digital Identity Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html)
 - [Implementing Secure User Authentication in PHP Applications with Long-Term Persistence (Login with "Remember Me" Cookies) ](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence)
+- [Password Storage - OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
diff --git a/UPGRADING.md b/UPGRADING.md
new file mode 100644
index 0000000..1ffe195
--- /dev/null
+++ b/UPGRADING.md
@@ -0,0 +1,51 @@
+# Upgrade Guide
+
+## Version 1.0.0-beta.3 to 1.0.0-beta.4
+
+### Important Password Changes
+
+#### Password Incompatibility
+
+Shield 1.0.0-beta.4 fixes a [vulnerability related to password storage](https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg).
+As a result, hashed passwords already stored in the database are no longer compatible
+and cannot be used by default.
+
+All hashed passwords stored in Shield v1.0.0-beta.3 or earlier are easier to
+crack than expected due to the above vulnerability. Therefore, they should be
+removed as soon as possible.
+
+Existing users will no longer be able to log in with their passwords and will
+need to log in with the magic link and then set their passwords again.
+
+#### If You Want to Allow Login with Existing Passwords
+
+If you want to use passwords saved in Shield v1.0.0-beta.3 or earlier,
+you must add the following property in `app/Config/Auth.php`:
+
+```php
+    public bool $supportOldDangerousPassword = true;
+```
+
+After upgrading, with the above setting, once a user logs in with the password,
+the hashed password is updated and stored in the database.
+
+In this case, the existing hashed passwords are still easier to crack than expected.
+Therefore, this setting should not be used for an extended period of time.
+So you should change the setting to `false` as soon as possible, and remove old
+hashed password.
+
+> **Note**
+>
+> This setting is deprecated. It will be removed in v1.0.0 official release.
+
+#### Limitations for the Default Password Handling
+
+By default, Shield uses the hashing algorithm `PASSWORD_DEFAULT` (see `app/Config/Auth.php`),
+that is, `PASSWORD_BCRYPT` at the time of writing.
+
+Now there are two limitations when you use `PASSWORD_BCRYPT`.
+
+1. the password will be truncated to a maximum length of 72 bytes.
+2. the password will be truncated at the first NULL byte (`\0`).
+
+If these behaviors are unacceptable, see [How to Strengthen the Password](https://github.com/codeigniter4/shield/blob/develop/docs/guides/strengthen_password.md).
diff --git a/docs/guides/strengthen_password.md b/docs/guides/strengthen_password.md
new file mode 100644
index 0000000..4e5067f
--- /dev/null
+++ b/docs/guides/strengthen_password.md
@@ -0,0 +1,122 @@
+# How to Strengthen the Password
+
+Shield allows you to customize password-related settings to make your passwords more secure.
+
+## Minimum Password Length
+
+The most important factor when it comes to passwords is the number of characters in the password.
+You can check password strength with [Password Strength Testing Tool](https://bitwarden.com/password-strength/).
+Short passwords may be cracked in less than one day.
+
+In Shield, you can set the users' minimum password length. The setting is
+`$minimumPasswordLength` in `app/Config/Auth.php`. The default value is 8 characters.
+It is the recommended minimum value by NIST. However, some organizations recommend
+12 to 14 characters.
+
+The longer the password, the stronger it is. Consider increasing the value.
+
+> **Note**
+>
+> This checking works when you validate passwords with the `strong_password`
+> validation rule.
+>
+> If you disable `CompositionValidator` (enabled by default) in `$passwordValidators`,
+> this checking will not work.
+
+## Password Hashing Algorithm
+
+You can change the password hashing algorithm by `$hashAlgorithm` in `app/Config/Auth.php`.
+The default value is `PASSWORD_DEFAULT` that is `PASSWORD_BCRYPT` at the time of writing.
+
+`PASSWORD_BCRYPT` means to create new password hashes using the bcrypt algorithm.
+
+You can use `PASSWORD_ARGON2ID` if your PHP has been compiled with Argon2 support.
+
+### PASSWORD_BCRYPT
+
+`PASSWORD_BCRYPT` has one configuration `$hashCost`. The bigger the cost, hashed passwords will be the stronger.
+
+You can find your appropriate cost with the following code:
+
+```php
+<?php
+/**
+ * This code will benchmark your server to determine how high of a cost you can
+ * afford. You want to set the highest cost that you can without slowing down
+ * you server too much. 8-10 is a good baseline, and more is good if your servers
+ * are fast enough. The code below aims for ≤ 50 milliseconds stretching time,
+ * which is a good baseline for systems handling interactive logins.
+ *
+ * From: https://www.php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-examples
+ */
+$timeTarget = 0.05; // 50 milliseconds
+
+$cost = 8;
+do {
+    $cost++;
+    $start = microtime(true);
+    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
+    $end = microtime(true);
+} while (($end - $start) < $timeTarget);
+
+echo "Appropriate Cost Found: " . $cost;
+```
+
+#### Limitations
+
+There are two limitations when you use `PASSWORD_BCRYPT`:
+
+1. the password will be truncated to a maximum length of 72 bytes.
+2. the password will be truncated at the first NULL byte (`\0`).
+
+##### 72 byte issue
+
+If a user submits a password longer than 72 bytes, the validation error will occur.
+If this behavior is unacceptable, consider:
+
+1. change the hashing algorithm to `PASSWORD_ARGON2ID`. It does not have such a limitation.
+
+##### NULL byte issue
+
+This is because `PASSWORD_BCRYPT` is not binary-safe. Normal users cannot
+send NULL bytes in a password string, so this is not a problem in most cases.
+
+But if this behavior is unacceptable, consider:
+
+1. adding a validation rule to prohibit NULL bytes or control codes.
+2. or change the hashing algorithm to `PASSWORD_ARGON2ID`. It is binary-safe.
+
+### PASSWORD_ARGON2ID
+
+`PASSWORD_ARGON2ID` has three configuration `$hashMemoryCost`, `$hashTimeCost`,
+and `$hashThreads`.
+
+If you use `PASSWORD_ARGON2ID`, you should use PHP's constants:
+
+```php
+    public int $hashMemoryCost = PASSWORD_ARGON2_DEFAULT_MEMORY_COST;
+
+    public int $hashTimeCost = PASSWORD_ARGON2_DEFAULT_TIME_COST;
+    public int $hashThreads  = PASSWORD_ARGON2_DEFAULT_THREADS;
+```
+
+## Maximum Password Length
+
+By default, Shield has the validation rules for maximum password length.
+
+- 72 bytes for PASSWORD_BCRYPT
+- 255 characters for others
+
+You can customize the validation rule. See [Customizing Shield](../customization.md).
+
+## $supportOldDangerousPassword
+
+In `app/Config/Auth.php` there is `$supportOldDangerousPassword`, which is a
+setting for using passwords stored in older versions of Shield that were [vulnerable](https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg).
+
+This setting is deprecated.  If you have this setting set to `true`, you should change
+it to `false` as soon as possible, and remove old hashed password in your database.
+
+> **Note**
+>
+> This setting will be removed in v1.0.0 official release.
diff --git a/docs/index.md b/docs/index.md
index ba0dfd6..32aed5e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -12,5 +12,6 @@
 * [Customization](customization.md)
 
 ## Guides
 * [Protecting an API with Access Tokens](guides/api_tokens.md)
 * [Mobile Authentication with Access Tokens](guides/mobile_apps.md)
+* [How to Strengthen the Password](guides/strengthen_password.md)
diff --git a/mkdocs.yml b/mkdocs.yml
index a99e038..36a514b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -49,5 +49,6 @@ nav:
   - Customization: customization.md
   - session_auth_event_and_logging.md
   - Guides:
     - guides/api_tokens.md
     - guides/mobile_apps.md
+    - guides/strengthen_password.md
diff --git a/src/Authentication/Authenticators/Session.php b/src/Authentication/Authenticators/Session.php
index c765f1a..c5e9ba1 100644
--- a/src/Authentication/Authenticators/Session.php
+++ b/src/Authentication/Authenticators/Session.php
@@ -334,23 +334,34 @@ public function check(array $credentials): Result
         }
 
         /** @var Passwords $passwords */
         $passwords = service('passwords');
 
+        // This is only for supportOldDangerousPassword.
+        $needsRehash = false;
+
         // Now, try matching the passwords.
         if (! $passwords->verify($givenPassword, $user->password_hash)) {
-            return new Result([
-                'success' => false,
-                'reason'  => lang('Auth.invalidPassword'),
-            ]);
+            if (
+                ! setting('Auth.supportOldDangerousPassword')
+                || ! $passwords->verifyDanger($givenPassword, $user->password_hash) // @phpstan-ignore-line
+            ) {
+                return new Result([
+                    'success' => false,
+                    'reason'  => lang('Auth.invalidPassword'),
+                ]);
+            }
+
+            // Passed with old dangerous password.
+            $needsRehash = true;
         }
 
         // Check to see if the password needs to be rehashed.
         // This would be due to the hash algorithm or hash
         // cost changing since the last time that a user
         // logged in.
-        if ($passwords->needsRehash($user->password_hash)) {
+        if ($passwords->needsRehash($user->password_hash) || $needsRehash) {
             $user->password_hash = $passwords->hash($givenPassword);
             $this->provider->save($user);
         }
 
         return new Result([
diff --git a/src/Authentication/Passwords.php b/src/Authentication/Passwords.php
index 3755b57..3eca6c7 100644
--- a/src/Authentication/Passwords.php
+++ b/src/Authentication/Passwords.php
@@ -29,40 +29,69 @@ public function __construct(Auth $config)
      *
      * @return false|string|null
      */
     public function hash(string $password)
     {
-        if ((defined('PASSWORD_ARGON2I') && $this->config->hashAlgorithm === PASSWORD_ARGON2I)
+        return password_hash($password, $this->config->hashAlgorithm, $this->getHashOptions());
+    }
+
+    private function getHashOptions(): array
+    {
+        if (
+            (defined('PASSWORD_ARGON2I') && $this->config->hashAlgorithm === PASSWORD_ARGON2I)
             || (defined('PASSWORD_ARGON2ID') && $this->config->hashAlgorithm === PASSWORD_ARGON2ID)
         ) {
-            $hashOptions = [
+            return [
                 'memory_cost' => $this->config->hashMemoryCost,
                 'time_cost'   => $this->config->hashTimeCost,
                 'threads'     => $this->config->hashThreads,
             ];
-        } else {
-            $hashOptions = [
-                'cost' => $this->config->hashCost,
-            ];
         }
 
+        return [
+            'cost' => $this->config->hashCost,
+        ];
+    }
+
+    /**
+     * Hash a password.
+     *
+     * @return false|string|null
+     *
+     * @deprecated This is only for backward compatibility.
+     */
+    public function hashDanger(string $password)
+    {
         return password_hash(
             base64_encode(
                 hash('sha384', $password, true)
             ),
             $this->config->hashAlgorithm,
-            $hashOptions
+            $this->getHashOptions()
         );
     }
 
     /**
      * Verifies a password against a previously hashed password.
      *
      * @param string $password The password we're checking
      * @param string $hash     The previously hashed password
      */
     public function verify(string $password, string $hash): bool
+    {
+        return password_verify($password, $hash);
+    }
+
+    /**
+     * Verifies a password against a previously hashed password.
+     *
+     * @param string $password The password we're checking
+     * @param string $hash     The previously hashed password
+     *
+     * @deprecated This is only for backward compatibility.
+     */
+    public function verifyDanger(string $password, string $hash): bool
     {
         return password_verify(base64_encode(
             hash('sha384', $password, true)
         ), $hash);
     }
@@ -70,11 +99,11 @@ public function verify(string $password, string $hash): bool
     /**
      * Checks to see if a password should be rehashed.
      */
     public function needsRehash(string $hashedPassword): bool
     {
-        return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm);
+        return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm, $this->getHashOptions());
     }
 
     /**
      * Checks a password against all of the Validators specified
      * in `$passwordValidators` setting in Config\Auth.php.
@@ -108,6 +137,18 @@ public function check(string $password, ?User $user = null): Result
 
         return new Result([
             'success' => true,
         ]);
     }
+
+    /**
+     * Returns the validation rule for max length.
+     */
+    public static function getMaxLenghtRule(): string
+    {
+        if (config('Auth')->hashAlgorithm === PASSWORD_BCRYPT) {
+            return 'max_byte[72]';
+        }
+
+        return 'max_length[255]';
+    }
 }
diff --git a/src/Authentication/Passwords/ValidationRules.php b/src/Authentication/Passwords/ValidationRules.php
index be02de6..53f078b 100644
--- a/src/Authentication/Passwords/ValidationRules.php
+++ b/src/Authentication/Passwords/ValidationRules.php
@@ -53,10 +53,18 @@ public function strong_password(string $value, ?string &$error1 = null, array $d
         }
 
         return $result->isOk();
     }
 
+    /**
+     * Returns true if $str is $val or fewer bytes in length.
+     */
+    public function max_byte(?string $str, string $val): bool
+    {
+        return is_numeric($val) && $val >= strlen($str ?? '');
+    }
+
     /**
      * Builds a new user instance from the global request.
      */
     protected function buildUserFromRequest(): User
     {
diff --git a/src/Config/Auth.php b/src/Config/Auth.php
index 2a06612..6cdd5d9 100644
--- a/src/Config/Auth.php
+++ b/src/Config/Auth.php
@@ -358,10 +358,20 @@ class Auth extends BaseConfig
      *
      * Valid range is between 4 - 31.
      */
     public int $hashCost = 10;
 
+    /**
+     * If you need to support passwords saved in versions prior to Shield v1.0.0-beta.4.
+     * set this to true.
+     *
+     * See https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg
+     *
+     * @deprecated This is only for backward compatibility.
+     */
+    public bool $supportOldDangerousPassword = false;
+
     /**
      * ////////////////////////////////////////////////////////////////////
      * OTHER SETTINGS
      * ////////////////////////////////////////////////////////////////////
      */
diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php
index 3478cdf..9cff8f3 100644
--- a/src/Controllers/LoginController.php
+++ b/src/Controllers/LoginController.php
@@ -5,10 +5,11 @@
 namespace CodeIgniter\Shield\Controllers;
 
 use App\Controllers\BaseController;
 use CodeIgniter\HTTP\RedirectResponse;
 use CodeIgniter\Shield\Authentication\Authenticators\Session;
+use CodeIgniter\Shield\Authentication\Passwords;
 use CodeIgniter\Shield\Traits\Viewable;
 
 class LoginController extends BaseController
 {
     use Viewable;
@@ -88,12 +89,15 @@ protected function getValidationRules(): array
             'email' => [
                 'label' => 'Auth.email',
                 'rules' => config('AuthSession')->emailValidationRules,
             ],
             'password' => [
-                'label' => 'Auth.password',
-                'rules' => 'required',
+                'label'  => 'Auth.password',
+                'rules'  => 'required|' . Passwords::getMaxLenghtRule(),
+                'errors' => [
+                    'max_byte' => 'Auth.errorPasswordTooLongBytes',
+                ],
             ],
         ];
     }
 
     /**
diff --git a/src/Controllers/RegisterController.php b/src/Controllers/RegisterController.php
index 041c1d6..2fe94c3 100644
--- a/src/Controllers/RegisterController.php
+++ b/src/Controllers/RegisterController.php
@@ -8,10 +8,11 @@
 use CodeIgniter\Events\Events;
 use CodeIgniter\HTTP\RedirectResponse;
 use CodeIgniter\HTTP\RequestInterface;
 use CodeIgniter\HTTP\ResponseInterface;
 use CodeIgniter\Shield\Authentication\Authenticators\Session;
+use CodeIgniter\Shield\Authentication\Passwords;
 use CodeIgniter\Shield\Config\Auth;
 use CodeIgniter\Shield\Entities\User;
 use CodeIgniter\Shield\Exceptions\ValidationException;
 use CodeIgniter\Shield\Models\UserModel;
 use CodeIgniter\Shield\Traits\Viewable;
@@ -193,12 +194,15 @@ protected function getValidationRules(): array
             'email' => [
                 'label' => 'Auth.email',
                 'rules' => $registrationEmailRules,
             ],
             'password' => [
-                'label' => 'Auth.password',
-                'rules' => 'required|strong_password',
+                'label'  => 'Auth.password',
+                'rules'  => 'required|' . Passwords::getMaxLenghtRule() . '|strong_password',
+                'errors' => [
+                    'max_byte' => 'Auth.errorPasswordTooLongBytes',
+                ],
             ],
             'password_confirm' => [
                 'label' => 'Auth.passwordConfirm',
                 'rules' => 'required|matches[password]',
             ],
diff --git a/src/Language/de/Auth.php b/src/Language/de/Auth.php
index 6da1207..d763ef7 100644
--- a/src/Language/de/Auth.php
+++ b/src/Language/de/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'Das Passwort ist dem Benutzernamen zu ähnlich.',
     'suggestPasswordTooSimilar' => 'Verwenden Sie keine Teile Ihres Benutzernamens in Ihrem Passwort.',
     'errorPasswordPwned'        => 'Das Passwort {0} wurde aufgrund einer Datenschutzverletzung aufgedeckt und wurde {1, number} Mal in {2} kompromittierten Passwörtern gesehen.',
     'suggestPasswordPwned'      => '{0} sollte niemals als Passwort verwendet werden. Wenn Sie es irgendwo verwenden, ändern Sie es sofort.',
     'errorPasswordEmpty'        => 'Ein Passwort ist erforderlich.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Passwort erfolgreich geändert',
     'userDoesNotExist'          => 'Passwort wurde nicht geändert. Der Benutzer existiert nicht',
     'resetTokenExpired'         => 'Tut mir leid. Ihr Reset-Token ist abgelaufen.',
 
     // Email Globals
diff --git a/src/Language/en/Auth.php b/src/Language/en/Auth.php
index ac75d33..306c233 100644
--- a/src/Language/en/Auth.php
+++ b/src/Language/en/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'Password is too similar to the username.',
     'suggestPasswordTooSimilar' => 'Do not use parts of your username in your password.',
     'errorPasswordPwned'        => 'The password {0} has been exposed due to a data breach and has been seen {1, number} times in {2} of compromised passwords.',
     'suggestPasswordPwned'      => '{0} should never be used as a password. If you are using it anywhere change it immediately.',
     'errorPasswordEmpty'        => 'A Password is required.',
+    'errorPasswordTooLongBytes' => 'Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Password changed successfully',
     'userDoesNotExist'          => 'Password was not changed. User does not exist',
     'resetTokenExpired'         => 'Sorry. Your reset token has expired.',
 
     // Email Globals
diff --git a/src/Language/es/Auth.php b/src/Language/es/Auth.php
index 6c0970f..ab72b16 100644
--- a/src/Language/es/Auth.php
+++ b/src/Language/es/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'La contraseña es demasiado parecida al usuario.',
     'suggestPasswordTooSimilar' => 'No uses partes de tu usuario en tu contraseña.',
     'errorPasswordPwned'        => 'La contraseña {0} ha quedado expuesta debido a una violación de datos y se ha visto comprometida {1, número} veces en {2} contraseñas.',
     'suggestPasswordPwned'      => '{0} no se debe usar nunca como contraseña. Si la estás usando en algún sitio, cámbiala inmediatamente.',
     'errorPasswordEmpty'        => 'Se necesita una contraseña.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Contraseña modificada correctamente',
     'userDoesNotExist'          => 'No se ha cambiado la contraseña. No existe el usuario',
     'resetTokenExpired'         => 'Lo sentimos. Tu token de reseteo ha caducado.',
 
     // Email Globals
diff --git a/src/Language/fa/Auth.php b/src/Language/fa/Auth.php
index ebf34a0..fb3acfc 100644
--- a/src/Language/fa/Auth.php
+++ b/src/Language/fa/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'رمز عبور بسیار شبیه نام کاربری است',
     'suggestPasswordTooSimilar' => 'از بخش های نام کاربری خود در رمز عبور استفاده نکنید.',
     'errorPasswordPwned'        => 'رمز عبور {0} با توجه به نقض داده ها و دیده شدن {1, number} بارها داخل رمز های عبور {2} به پسورد های ناامن تبدیل شده و در معرض قرار گرفته است.',
     'suggestPasswordPwned'      => '{0} هرگز نباید به عنوان رمز عبور استفاده شود. اگر در هر جایی از آن استفاده می کنید سریعا آن را تغییر دهید.',
     'errorPasswordEmpty'        => 'رمز عبور الزامی است.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'رمز عبور با موفقیت تغییر کرد',
     'userDoesNotExist'          => 'رمز عبور تغییر نکرد. کاربر وجود ندارد.',
     'resetTokenExpired'         => 'متاسفانه، توکن بازنشانی شما منقضی شده است.',
 
     // Email Globals
diff --git a/src/Language/fr/Auth.php b/src/Language/fr/Auth.php
index 9b7b7f2..5c4e0bb 100644
--- a/src/Language/fr/Auth.php
+++ b/src/Language/fr/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'Le mot de passe est trop similaire à l\'identifiant.',
     'suggestPasswordTooSimilar' => 'N\'utilisez pas de partie de votre identifiant dans votre mot de passe.',
     'errorPasswordPwned'        => 'Le mot de passe {0} a été exposé à la suite d\'une violation de données et a été vu {1, number} fois dans {2} des mots de passe compromis.',
     'suggestPasswordPwned'      => '{0} ne devrait jamais être utilisé comme mot de passe. Si vous l\'utilisez quelque part, changez-le immédiatement.',
     'errorPasswordEmpty'        => 'Un mot de passe est obligatoire.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Mot de passe modifié avec succès',
     'userDoesNotExist'          => 'Le mot de passe n\'a pas été modifié. L\'utilisateur n\'existe pas',
     'resetTokenExpired'         => 'Désolé. Votre jeton de réinitialisation a expiré.',
 
     // Email Globals
diff --git a/src/Language/id/Auth.php b/src/Language/id/Auth.php
index b71c9a4..ada97cf 100644
--- a/src/Language/id/Auth.php
+++ b/src/Language/id/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'Kata sandi mirip dengan nama pengguna.',
     'suggestPasswordTooSimilar' => 'Jangan gunakan bagian dari nama pengguna Anda dalam kata sandi Anda.',
     'errorPasswordPwned'        => 'Kata sandi {0} telah bocor karena pelanggaran data dan telah dilihat {1, number} kali dalam {2} sandi yang disusupi.',
     'suggestPasswordPwned'      => '{0} tidak boleh digunakan sebagai kata sandi. Jika Anda menggunakannya di mana saja, segera ubah.',
     'errorPasswordEmpty'        => 'Kata sandi wajib diisi.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Kata sandi berhasil diubah',
     'userDoesNotExist'          => 'Kata sandi tidak diubah. User tidak ditemukan',
     'resetTokenExpired'         => 'Maaf, token setel ulang Anda sudah habis waktu.',
 
     // Email Globals
diff --git a/src/Language/it/Auth.php b/src/Language/it/Auth.php
index d9cd2e4..f517ac9 100644
--- a/src/Language/it/Auth.php
+++ b/src/Language/it/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'La password è troppo simile all\'username.',
     'suggestPasswordTooSimilar' => 'Non utilizzare parti del tuo username nella password.',
     'errorPasswordPwned'        => 'La password {0} è stata esposta ad un furto di dati ed è stata vista {1, number} volte in {2} di password compromesse.',
     'suggestPasswordPwned'      => '{0} non dovrebbe mai essere usata come password. Se la stai utilizzando da qualche parte, cambiala immediatamente.',
     'errorPasswordEmpty'        => 'Una password è richiesta.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'La password è stata cambiata con successo',
     'userDoesNotExist'          => 'La password non è stata cambiata. L\'utente non esiste',
     'resetTokenExpired'         => 'Spiacente. Il tuo reset token è scaduto.',
 
     // Email Globals
diff --git a/src/Language/ja/Auth.php b/src/Language/ja/Auth.php
index a4bd4a5..40f3624 100644
--- a/src/Language/ja/Auth.php
+++ b/src/Language/ja/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'パスワードがユーザー名と似すぎています。', // 'Password is too similar to the username.',
     'suggestPasswordTooSimilar' => 'パスワードにユーザー名の一部を使用しないでください。', // 'Do not use parts of your username in your password.',
     'errorPasswordPwned'        => 'パスワード {0} はデータ漏洩により公開されており、{2} の漏洩したパスワード中で {1, number} 回見られます。', // 'The password {0} has been exposed due to a data breach and has been seen {1, number} times in {2} of compromised passwords.',
     'suggestPasswordPwned'      => '{0} は絶対にパスワードとして使ってはいけません。もしどこかで使っていたら、すぐに変更してください。', // '{0} should never be used as a password. If you are using it anywhere change it immediately.',
     'errorPasswordEmpty'        => 'パスワードが必要です。', // 'A Password is required.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'パスワードの変更に成功しました', // 'Password changed successfully',
     'userDoesNotExist'          => 'パスワードは変更されていません。ユーザーは存在しません', // 'Password was not changed. User does not exist',
     'resetTokenExpired'         => '申し訳ありません。リセットトークンの有効期限が切れました。', // 'Sorry. Your reset token has expired.',
 
     // Email Globals
diff --git a/src/Language/pt-BR/Auth.php b/src/Language/pt-BR/Auth.php
index 2feb6a2..dda64e6 100644
--- a/src/Language/pt-BR/Auth.php
+++ b/src/Language/pt-BR/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'A senha é muito semelhante ao nome de usuário.',
     'suggestPasswordTooSimilar' => 'Não use partes do seu nome de usuário na sua senha.',
     'errorPasswordPwned'        => 'A senha {0} foi exposta devido a uma violação de dados e foi vista {1, number} vezes em {2} de senhas comprometidas.',
     'suggestPasswordPwned'      => '{0} nunca deve ser usado como uma senha. Se você estiver usando em algum lugar, altere imediatamente.',
     'errorPasswordEmpty'        => 'É necessária uma senha.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Senha alterada com sucesso',
     'userDoesNotExist'          => 'Senha não foi alterada. Usuário não existe',
     'resetTokenExpired'         => 'Desculpe. Seu token de redefinição expirou.',
 
     // E-mails Globais
diff --git a/src/Language/sk/Auth.php b/src/Language/sk/Auth.php
index 5a73ba2..5d504b5 100644
--- a/src/Language/sk/Auth.php
+++ b/src/Language/sk/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'Heslo je príliš podobné používateľskému menu.',
     'suggestPasswordTooSimilar' => 'Vo svojom hesle nepoužívajte časti svojho používateľského mena.',
     'errorPasswordPwned'        => 'Heslo {0} bolo odhalené z dôvodu porušenia ochrany údajov a bolo videné {1, number}-krát z {2} prelomených hesiel.',
     'suggestPasswordPwned'      => '{0} by sa nikdy nemalo používať ako heslo. Ak ho niekde používate, okamžite ho zmeňte.',
     'errorPasswordEmpty'        => 'Vyžaduje sa heslo.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Heslo bolo úspešne zmenené',
     'userDoesNotExist'          => 'Heslo nebolo zmenené. Používateľ neexistuje',
     'resetTokenExpired'         => 'Prepáčte. Platnosť vášho resetovacieho tokenu vypršala.',
 
     // Email Globals
diff --git a/src/Language/tr/Auth.php b/src/Language/tr/Auth.php
index 56cea06..48c7a24 100644
--- a/src/Language/tr/Auth.php
+++ b/src/Language/tr/Auth.php
@@ -59,10 +59,11 @@
     'errorPasswordTooSimilar'   => 'Şifre, kullanıcı adınıza çok benziyor.',
     'suggestPasswordTooSimilar' => 'Kullanıcı adınızın bazı kısımlarını şifrenizde kullanmayın.',
     'errorPasswordPwned'        => '{0} şifresi, bir veri ihlali nedeniyle açığa çıktı ve güvenliği ihlal edilmiş şifrelerin {2} tanesinde {1, sayı} kez görüldü.',
     'suggestPasswordPwned'      => '{0} asla şifre olarak kullanılmamalıdır. Herhangi bir yerde kullanıyorsanız hemen değiştirin.',
     'errorPasswordEmpty'        => 'Şifre gerekli.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Şifre başarıyla değiştirildi.',
     'userDoesNotExist'          => 'Şifre değiştirilmedi. Kullanıcı yok.',
     'resetTokenExpired'         => 'Üzgünüz. Sıfırlama anahtarınızın süresi doldu.',
 
     // Email Globals
diff --git a/tests/Authentication/Authenticators/SessionAuthenticatorTest.php b/tests/Authentication/Authenticators/SessionAuthenticatorTest.php
index 2a3249f..662b32f 100644
--- a/tests/Authentication/Authenticators/SessionAuthenticatorTest.php
+++ b/tests/Authentication/Authenticators/SessionAuthenticatorTest.php
@@ -10,10 +10,11 @@
 use CodeIgniter\Shield\Authentication\Authenticators\Session;
 use CodeIgniter\Shield\Config\Auth;
 use CodeIgniter\Shield\Entities\User;
 use CodeIgniter\Shield\Exceptions\LogicException;
 use CodeIgniter\Shield\Models\RememberModel;
+use CodeIgniter\Shield\Models\UserIdentityModel;
 use CodeIgniter\Shield\Models\UserModel;
 use CodeIgniter\Shield\Result;
 use CodeIgniter\Test\Mock\MockEvents;
 use Config\Services;
 use Tests\Support\DatabaseTestCase;
@@ -301,10 +302,38 @@ public function testCheckSuccess(): void
 
         $foundUser = $result->extraInfo();
         $this->assertSame($this->user->id, $foundUser->id);
     }
 
+    public function testCheckSuccessOldDangerousPassword(): void
+    {
+        /** @var Auth $config */
+        $config                              = config('Auth');
+        $config->supportOldDangerousPassword = true; // @phpstan-ignore-line
+
+        fake(
+            UserIdentityModel::class,
+            [
+                'user_id' => $this->user->id,
+                'type'    => Session::ID_TYPE_EMAIL_PASSWORD,
+                'secret'  => 'foo@example.com',
+                'secret2' => '$2y$10$WswjNNcR24cJvsXvBc5TveVVVQ9/EYC0eq.Ad9e/2cVnmeSEYBOEm',
+            ]
+        );
+
+        $result = $this->auth->check([
+            'email'    => 'foo@example.com',
+            'password' => 'passw0rd!',
+        ]);
+
+        $this->assertInstanceOf(Result::class, $result);
+        $this->assertTrue($result->isOK());
+
+        $foundUser = $result->extraInfo();
+        $this->assertSame($this->user->id, $foundUser->id);
+    }
+
     public function testAttemptCannotFindUser(): void
     {
         $result = $this->auth->attempt([
             'email'    => 'johnsmith@example.com',
             'password' => 'secret',
diff --git a/tests/Controllers/LoginTest.php b/tests/Controllers/LoginTest.php
index c27190e..b18ca3f 100644
--- a/tests/Controllers/LoginTest.php
+++ b/tests/Controllers/LoginTest.php
@@ -6,10 +6,11 @@
 
 use CodeIgniter\CodeIgniter;
 use CodeIgniter\Config\Factories;
 use CodeIgniter\I18n\Time;
 use CodeIgniter\Shield\Authentication\Actions\Email2FA;
+use CodeIgniter\Shield\Config\Auth;
 use CodeIgniter\Test\FeatureTestTrait;
 use Config\Services;
 use Config\Validation;
 use Tests\Support\DatabaseTestCase;
 use Tests\Support\FakeUser;
@@ -59,10 +60,56 @@ public function testLoginBadEmail(): void
 
         $this->assertNotEmpty(session('error'));
         $this->assertSame(lang('Auth.badAttempt'), session('error'));
     }
 
+    public function testLoginTooLongPasswordDefault(): void
+    {
+        $this->user->createEmailIdentity([
+            'email'    => 'foo@example.com',
+            'password' => 'secret123',
+        ]);
+
+        $result = $this->post('/login', [
+            'email'    => 'fooled@example.com',
+            'password' => str_repeat('a', 73),
+        ]);
+
+        $result->assertStatus(302);
+        $result->assertRedirect();
+        $result->assertSessionMissing('error');
+        $result->assertSessionHas(
+            'errors',
+            ['password' => 'Password cannot exceed 72 bytes in length.']
+        );
+    }
+
+    public function testLoginTooLongPasswordArgon2id(): void
+    {
+        /** @var Auth $config */
+        $config                = config('Auth');
+        $config->hashAlgorithm = PASSWORD_ARGON2ID;
+
+        $this->user->createEmailIdentity([
+            'email'    => 'foo@example.com',
+            'password' => 'secret123',
+        ]);
+
+        $result = $this->post('/login', [
+            'email'    => 'fooled@example.com',
+            'password' => str_repeat('a', 256),
+        ]);
+
+        $result->assertStatus(302);
+        $result->assertRedirect();
+        $result->assertSessionMissing('error');
+        $result->assertSessionHas(
+            'errors',
+            ['password' => 'The Password field cannot exceed 255 characters in length.']
+        );
+    }
+
     public function testLoginActionEmailSuccess(): void
     {
         if (version_compare(CodeIgniter::CI_VERSION, '4.3.0', '>=')) {
             Time::setTestNow('March 10, 2017', 'UTC');
         } else {
diff --git a/tests/Controllers/RegisterTest.php b/tests/Controllers/RegisterTest.php
index 3a44098..320db29 100644
--- a/tests/Controllers/RegisterTest.php
+++ b/tests/Controllers/RegisterTest.php
@@ -6,10 +6,11 @@
 
 use CodeIgniter\Config\Factories;
 use CodeIgniter\Shield\Authentication\Actions\EmailActivator;
 use CodeIgniter\Shield\Authentication\Authenticators\Session;
 use CodeIgniter\Shield\Authentication\Passwords\ValidationRules;
+use CodeIgniter\Shield\Config\Auth;
 use CodeIgniter\Shield\Entities\User;
 use CodeIgniter\Shield\Models\UserModel;
 use CodeIgniter\Test\FeatureTestTrait;
 use Config\Services;
 use Tests\Support\DatabaseTestCase;
@@ -79,10 +80,50 @@ public function testRegisterActionSuccess(): void
         $this->assertFalse($user->inGroup('admin'));
 
         $this->assertTrue($user->active);
     }
 
+    public function testRegisterTooLongPasswordDefault(): void
+    {
+        $result = $this->withSession()->post('/register', [
+            'username'         => 'JohnDoe',
+            'email'            => 'john.doe@example.com',
+            'password'         => str_repeat('a', 73),
+            'password_confirm' => str_repeat('a', 73),
+        ]);
+
+        $result->assertStatus(302);
+        $result->assertRedirect();
+        $result->assertSessionMissing('error');
+        $result->assertSessionHas(
+            'errors',
+            ['password' => 'Password cannot exceed 72 bytes in length.']
+        );
+    }
+
+    public function testRegisterTooLongPasswordArgon2id(): void
+    {
+        /** @var Auth $config */
+        $config                = config('Auth');
+        $config->hashAlgorithm = PASSWORD_ARGON2ID;
+
+        $result = $this->withSession()->post('/register', [
+            'username'         => 'JohnDoe',
+            'email'            => 'john.doe@example.com',
+            'password'         => str_repeat('a', 256),
+            'password_confirm' => str_repeat('a', 256),
+        ]);
+
+        $result->assertStatus(302);
+        $result->assertRedirect();
+        $result->assertSessionMissing('error');
+        $result->assertSessionHas(
+            'errors',
+            ['password' => 'The Password field cannot exceed 255 characters in length.']
+        );
+    }
+
     public function testRegisterDisplaysForm(): void
     {
         $result = $this->withSession()->get('/register');
 
         $result->assertStatus(200);
diff --git a/tests/Unit/PasswordsTest.php b/tests/Unit/PasswordsTest.php
index 62ebe20..7398fc5 100644
--- a/tests/Unit/PasswordsTest.php
+++ b/tests/Unit/PasswordsTest.php
@@ -21,6 +21,42 @@ public function testEmptyPassword(): void
         $result = $passwords->check('', new User());
 
         $this->assertFalse($result->isOK());
         $this->assertSame('A Password is required.', $result->reason());
     }
+
+    public function testHash(): string
+    {
+        $config    = new AuthConfig();
+        $passwords = new Passwords($config);
+
+        $plainPassword  = 'passw0rd!';
+        $hashedPassword = $passwords->hash($plainPassword);
+
+        $user = new User([
+            'id'       => 1,
+            'username' => 'John',
+        ]);
+        $user->email         = 'john@example.org';
+        $user->password_hash = $hashedPassword;
+
+        $result = $passwords->check($plainPassword, $user);
+
+        $this->assertTrue($result->isOK());
+
+        return $hashedPassword;
+    }
+
+    /**
+     * @depends testHash
+     */
+    public function testNeedsRehashTakesCareOptions(string $hashedPassword): void
+    {
+        $config           = new AuthConfig();
+        $config->hashCost = 12;
+        $passwords        = new Passwords($config);
+
+        $result = $passwords->needsRehash($hashedPassword);
+
+        $this->assertTrue($result);
+    }
 }

Vulnerable code:             return new Result([
                'success' => false,
                'reason'  => lang('Auth.invalidPassword'),
            ]);
        if ($passwords->needsRehash($user->password_hash)) {
        if ((defined('PASSWORD_ARGON2I') && $this->config->hashAlgorithm === PASSWORD_ARGON2I)
            $hashOptions = [
        } else {
            $hashOptions = [
                'cost' => $this->config->hashCost,
            ];
            $hashOptions
        return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm);
                'label' => 'Auth.password',
                'rules' => 'required',
                'label' => 'Auth.password',
                'rules' => 'required|strong_password',
Fixed code: - [Password Storage - OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
# Upgrade Guide

## Version 1.0.0-beta.3 to 1.0.0-beta.4

### Important Password Changes

#### Password Incompatibility

Shield 1.0.0-beta.4 fixes a [vulnerability related to password storage](https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg).
As a result, hashed passwords already stored in the database are no longer compatible
and cannot be used by default.

All hashed passwords stored in Shield v1.0.0-beta.3 or earlier are easier to
crack than expected due to the above vulnerability. Therefore, they should be
removed as soon as possible.

Existing users will no longer be able to log in with their passwords and will
need to log in with the magic link and then set their passwords again.

#### If You Want to Allow Login with Existing Passwords

If you want to use passwords saved in Shield v1.0.0-beta.3 or earlier,
you must add the following property in `app/Config/Auth.php`:

```php
    public bool $supportOldDangerousPassword = true;
```

After upgrading, with the above setting, once a user logs in with the password,
the hashed password is updated and stored in the database.

In this case, the existing hashed passwords are still easier to crack than expected.
Therefore, this setting should not be used for an extended period of time.
So you should change the setting to `false` as soon as possible, and remove old
hashed password.

> **Note**
>
> This setting is deprecated. It will be removed in v1.0.0 official release.

#### Limitations for the Default Password Handling

By default, Shield uses the hashing algorithm `PASSWORD_DEFAULT` (see `app/Config/Auth.php`),
that is, `PASSWORD_BCRYPT` at the time of writing.

Now there are two limitations when you use `PASSWORD_BCRYPT`.

1. the password will be truncated to a maximum length of 72 bytes.
2. the password will be truncated at the first NULL byte (`\0`).

If these behaviors are unacceptable, see [How to Strengthen the Password](https://github.com/codeigniter4/shield/blob/develop/docs/guides/strengthen_password.md).
# How to Strengthen the Password

Shield allows you to customize password-related settings to make your passwords more secure.

## Minimum Password Length

The most important factor when it comes to passwords is the number of characters in the password.
You can check password strength with [Password Strength Testing Tool](https://bitwarden.com/password-strength/).
Short passwords may be cracked in less than one day.

In Shield, you can set the users' minimum password length. The setting is
`$minimumPasswordLength` in `app/Config/Auth.php`. The default value is 8 characters.
It is the recommended minimum value by NIST. However, some organizations recommend
12 to 14 characters.

The longer the password, the stronger it is. Consider increasing the value.

> **Note**
>
> This checking works when you validate passwords with the `strong_password`
> validation rule.
>
> If you disable `CompositionValidator` (enabled by default) in `$passwordValidators`,
> this checking will not work.

## Password Hashing Algorithm

You can change the password hashing algorithm by `$hashAlgorithm` in `app/Config/Auth.php`.
The default value is `PASSWORD_DEFAULT` that is `PASSWORD_BCRYPT` at the time of writing.

`PASSWORD_BCRYPT` means to create new password hashes using the bcrypt algorithm.

You can use `PASSWORD_ARGON2ID` if your PHP has been compiled with Argon2 support.

### PASSWORD_BCRYPT

`PASSWORD_BCRYPT` has one configuration `$hashCost`. The bigger the cost, hashed passwords will be the stronger.

You can find your appropriate cost with the following code:

```php
<?php
/**
 * This code will benchmark your server to determine how high of a cost you can
 * afford. You want to set the highest cost that you can without slowing down
 * you server too much. 8-10 is a good baseline, and more is good if your servers
 * are fast enough. The code below aims for ≤ 50 milliseconds stretching time,
 * which is a good baseline for systems handling interactive logins.
 *
 * From: https://www.php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-examples
 */
$timeTarget = 0.05; // 50 milliseconds

$cost = 8;
do {
    $cost++;
    $start = microtime(true);
    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
} while (($end - $start) < $timeTarget);

echo "Appropriate Cost Found: " . $cost;
```

#### Limitations

There are two limitations when you use `PASSWORD_BCRYPT`:

1. the password will be truncated to a maximum length of 72 bytes.
2. the password will be truncated at the first NULL byte (`\0`).

##### 72 byte issue

If a user submits a password longer than 72 bytes, the validation error will occur.
If this behavior is unacceptable, consider:

1. change the hashing algorithm to `PASSWORD_ARGON2ID`. It does not have such a limitation.

##### NULL byte issue

This is because `PASSWORD_BCRYPT` is not binary-safe. Normal users cannot
send NULL bytes in a password string, so this is not a problem in most cases.

But if this behavior is unacceptable, consider:

1. adding a validation rule to prohibit NULL bytes or control codes.
2. or change the hashing algorithm to `PASSWORD_ARGON2ID`. It is binary-safe.

### PASSWORD_ARGON2ID

`PASSWORD_ARGON2ID` has three configuration `$hashMemoryCost`, `$hashTimeCost`,
and `$hashThreads`.

If you use `PASSWORD_ARGON2ID`, you should use PHP's constants:

```php
    public int $hashMemoryCost = PASSWORD_ARGON2_DEFAULT_MEMORY_COST;

    public int $hashTimeCost = PASSWORD_ARGON2_DEFAULT_TIME_COST;
    public int $hashThreads  = PASSWORD_ARGON2_DEFAULT_THREADS;
```

## Maximum Password Length

By default, Shield has the validation rules for maximum password length.

- 72 bytes for PASSWORD_BCRYPT
- 255 characters for others

You can customize the validation rule. See [Customizing Shield](../customization.md).

## $supportOldDangerousPassword

In `app/Config/Auth.php` there is `$supportOldDangerousPassword`, which is a
setting for using passwords stored in older versions of Shield that were [vulnerable](https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg).

This setting is deprecated.  If you have this setting set to `true`, you should change
it to `false` as soon as possible, and remove old hashed password in your database.

> **Note**
>
> This setting will be removed in v1.0.0 official release.
* [How to Strengthen the Password](guides/strengthen_password.md)
    - guides/strengthen_password.md
        // This is only for supportOldDangerousPassword.
        $needsRehash = false;

            if (
                ! setting('Auth.supportOldDangerousPassword')
                || ! $passwords->verifyDanger($givenPassword, $user->password_hash) // @phpstan-ignore-line
            ) {
                return new Result([
                    'success' => false,
                    'reason'  => lang('Auth.invalidPassword'),
                ]);
            }

            // Passed with old dangerous password.
            $needsRehash = true;
        if ($passwords->needsRehash($user->password_hash) || $needsRehash) {
        return password_hash($password, $this->config->hashAlgorithm, $this->getHashOptions());
    }

    private function getHashOptions(): array
    {
        if (
            (defined('PASSWORD_ARGON2I') && $this->config->hashAlgorithm === PASSWORD_ARGON2I)
            return [
        return [
            'cost' => $this->config->hashCost,
        ];
    }

    /**
     * Hash a password.
     *
     * @return false|string|null
     *
     * @deprecated This is only for backward compatibility.
     */
    public function hashDanger(string $password)
    {
            $this->getHashOptions()
    {
        return password_verify($password, $hash);
    }

    /**
     * Verifies a password against a previously hashed password.
     *
     * @param string $password The password we're checking
     * @param string $hash     The previously hashed password
     *
     * @deprecated This is only for backward compatibility.
     */
    public function verifyDanger(string $password, string $hash): bool
        return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm, $this->getHashOptions());

    /**
     * Returns the validation rule for max length.
     */
    public static function getMaxLenghtRule(): string
    {
        if (config('Auth')->hashAlgorithm === PASSWORD_BCRYPT) {
            return 'max_byte[72]';
        }

        return 'max_length[255]';
    }
    /**
     * Returns true if $str is $val or fewer bytes in length.
     */
    public function max_byte(?string $str, string $val): bool
    {
        return is_numeric($val) && $val >= strlen($str ?? '');
    }

    /**
     * If you need to support passwords saved in versions prior to Shield v1.0.0-beta.4.
     * set this to true.
     *
     * See https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg
     *
     * @deprecated This is only for backward compatibility.
     */
    public bool $supportOldDangerousPassword = false;

use CodeIgniter\Shield\Authentication\Passwords;
                'label'  => 'Auth.password',
                'rules'  => 'required|' . Passwords::getMaxLenghtRule(),
                'errors' => [
                    'max_byte' => 'Auth.errorPasswordTooLongBytes',
                ],
use CodeIgniter\Shield\Authentication\Passwords;
                'label'  => 'Auth.password',
                'rules'  => 'required|' . Passwords::getMaxLenghtRule() . '|strong_password',
                'errors' => [
                    'max_byte' => 'Auth.errorPasswordTooLongBytes',
                ],
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => 'Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
use CodeIgniter\Shield\Models\UserIdentityModel;
    public function testCheckSuccessOldDangerousPassword(): void
    {
        /** @var Auth $config */
        $config                              = config('Auth');
        $config->supportOldDangerousPassword = true; // @phpstan-ignore-line

        fake(
            UserIdentityModel::class,
            [
                'user_id' => $this->user->id,
                'type'    => Session::ID_TYPE_EMAIL_PASSWORD,
                'secret'  => 'foo@example.com',
                'secret2' => '$2y$10$WswjNNcR24cJvsXvBc5TveVVVQ9/EYC0eq.Ad9e/2cVnmeSEYBOEm',
            ]
        );

        $result = $this->auth->check([
            'email'    => 'foo@example.com',
            'password' => 'passw0rd!',
        ]);

        $this->assertInstanceOf(Result::class, $result);
        $this->assertTrue($result->isOK());

        $foundUser = $result->extraInfo();
        $this->assertSame($this->user->id, $foundUser->id);
    }

use CodeIgniter\Shield\Config\Auth;
    public function testLoginTooLongPasswordDefault(): void
    {
        $this->user->createEmailIdentity([
            'email'    => 'foo@example.com',
            'password' => 'secret123',
        ]);

        $result = $this->post('/login', [
            'email'    => 'fooled@example.com',
            'password' => str_repeat('a', 73),
        ]);

        $result->assertStatus(302);
        $result->assertRedirect();
        $result->assertSessionMissing('error');
        $result->assertSessionHas(
            'errors',
            ['password' => 'Password cannot exceed 72 bytes in length.']
        );
    }

    public function testLoginTooLongPasswordArgon2id(): void
    {
        /** @var Auth $config */
        $config                = config('Auth');
        $config->hashAlgorithm = PASSWORD_ARGON2ID;

        $this->user->createEmailIdentity([
            'email'    => 'foo@example.com',
            'password' => 'secret123',
        ]);

        $result = $this->post('/login', [
            'email'    => 'fooled@example.com',
            'password' => str_repeat('a', 256),
        ]);

        $result->assertStatus(302);
        $result->assertRedirect();
        $result->assertSessionMissing('error');
        $result->assertSessionHas(
            'errors',
            ['password' => 'The Password field cannot exceed 255 characters in length.']
        );
    }

use CodeIgniter\Shield\Config\Auth;
    public function testRegisterTooLongPasswordDefault(): void
    {
        $result = $this->withSession()->post('/register', [
            'username'         => 'JohnDoe',
            'email'            => 'john.doe@example.com',
            'password'         => str_repeat('a', 73),
            'password_confirm' => str_repeat('a', 73),
        ]);

        $result->assertStatus(302);
        $result->assertRedirect();
        $result->assertSessionMissing('error');
        $result->assertSessionHas(
            'errors',
            ['password' => 'Password cannot exceed 72 bytes in length.']
        );
    }

    public function testRegisterTooLongPasswordArgon2id(): void
    {
        /** @var Auth $config */
        $config                = config('Auth');
        $config->hashAlgorithm = PASSWORD_ARGON2ID;

        $result = $this->withSession()->post('/register', [
            'username'         => 'JohnDoe',
            'email'            => 'john.doe@example.com',
            'password'         => str_repeat('a', 256),
            'password_confirm' => str_repeat('a', 256),
        ]);

        $result->assertStatus(302);
        $result->assertRedirect();
        $result->assertSessionMissing('error');
        $result->assertSessionHas(
            'errors',
            ['password' => 'The Password field cannot exceed 255 characters in length.']
        );
    }


    public function testHash(): string
    {
        $config    = new AuthConfig();
        $passwords = new Passwords($config);

        $plainPassword  = 'passw0rd!';
        $hashedPassword = $passwords->hash($plainPassword);

        $user = new User([
            'id'       => 1,
            'username' => 'John',
        ]);
        $user->email         = 'john@example.org';
        $user->password_hash = $hashedPassword;

        $result = $passwords->check($plainPassword, $user);

        $this->assertTrue($result->isOK());

        return $hashedPassword;
    }

    /**
     * @depends testHash
     */
    public function testNeedsRehashTakesCareOptions(string $hashedPassword): void
    {
        $config           = new AuthConfig();
        $config->hashCost = 12;
        $passwords        = new Passwords($config);

        $result = $passwords->needsRehash($hashedPassword);

        $this->assertTrue($result);
    }