diff --git a/README.md b/README.md index d428cc905..3b148ebdd 100644 --- a/README.md +++ b/README.md @@ -86,3 +86,4 @@ 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 000000000..1ffe19509 --- /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 000000000..4e5067fc4 --- /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 + $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 ba0dfd6c9..32aed5e61 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,3 +14,4 @@ ## 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 a99e0386f..36a514bf3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,3 +51,4 @@ nav: - 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 c765f1aa8..c5e9ba138 100644 --- a/src/Authentication/Authenticators/Session.php +++ b/src/Authentication/Authenticators/Session.php @@ -336,19 +336,30 @@ 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); } diff --git a/src/Authentication/Passwords.php b/src/Authentication/Passwords.php index 3755b5709..3eca6c71f 100644 --- a/src/Authentication/Passwords.php +++ b/src/Authentication/Passwords.php @@ -31,26 +31,42 @@ public function __construct(Auth $config) */ 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() ); } @@ -61,6 +77,19 @@ public function hash(string $password) * @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) @@ -72,7 +101,7 @@ public function verify(string $password, string $hash): bool */ public function needsRehash(string $hashedPassword): bool { - return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm); + return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm, $this->getHashOptions()); } /** @@ -110,4 +139,16 @@ public function check(string $password, ?User $user = null): 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 be02de628..53f078b8e 100644 --- a/src/Authentication/Passwords/ValidationRules.php +++ b/src/Authentication/Passwords/ValidationRules.php @@ -55,6 +55,14 @@ 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. */ diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 2a0661202..6cdd5d9b8 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -360,6 +360,16 @@ class Auth extends BaseConfig */ 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 3478cdfa1..9cff8f3aa 100644 --- a/src/Controllers/LoginController.php +++ b/src/Controllers/LoginController.php @@ -7,6 +7,7 @@ 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 @@ -90,8 +91,11 @@ protected function getValidationRules(): array '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 041c1d65a..2fe94c31a 100644 --- a/src/Controllers/RegisterController.php +++ b/src/Controllers/RegisterController.php @@ -10,6 +10,7 @@ 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; @@ -195,8 +196,11 @@ protected function getValidationRules(): array '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', diff --git a/src/Language/de/Auth.php b/src/Language/de/Auth.php index 6da1207ad..d763ef7d2 100644 --- a/src/Language/de/Auth.php +++ b/src/Language/de/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/src/Language/en/Auth.php b/src/Language/en/Auth.php index ac75d3315..306c233d5 100644 --- a/src/Language/en/Auth.php +++ b/src/Language/en/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/src/Language/es/Auth.php b/src/Language/es/Auth.php index 6c0970f16..ab72b1608 100644 --- a/src/Language/es/Auth.php +++ b/src/Language/es/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/src/Language/fa/Auth.php b/src/Language/fa/Auth.php index ebf34a003..fb3acfce5 100644 --- a/src/Language/fa/Auth.php +++ b/src/Language/fa/Auth.php @@ -61,6 +61,7 @@ 'errorPasswordPwned' => 'رمز عبور {0} با توجه به نقض داده ها و دیده شدن {1, number} بارها داخل رمز های عبور {2} به پسورد های ناامن تبدیل شده و در معرض قرار گرفته است.', 'suggestPasswordPwned' => '{0} هرگز نباید به عنوان رمز عبور استفاده شود. اگر در هر جایی از آن استفاده می کنید سریعا آن را تغییر دهید.', 'errorPasswordEmpty' => 'رمز عبور الزامی است.', + 'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.', 'passwordChangeSuccess' => 'رمز عبور با موفقیت تغییر کرد', 'userDoesNotExist' => 'رمز عبور تغییر نکرد. کاربر وجود ندارد.', 'resetTokenExpired' => 'متاسفانه، توکن بازنشانی شما منقضی شده است.', diff --git a/src/Language/fr/Auth.php b/src/Language/fr/Auth.php index 9b7b7f263..5c4e0bbd9 100644 --- a/src/Language/fr/Auth.php +++ b/src/Language/fr/Auth.php @@ -61,6 +61,7 @@ '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é.', diff --git a/src/Language/id/Auth.php b/src/Language/id/Auth.php index b71c9a477..ada97cf22 100644 --- a/src/Language/id/Auth.php +++ b/src/Language/id/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/src/Language/it/Auth.php b/src/Language/it/Auth.php index d9cd2e4a6..f517ac988 100644 --- a/src/Language/it/Auth.php +++ b/src/Language/it/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/src/Language/ja/Auth.php b/src/Language/ja/Auth.php index a4bd4a54e..40f3624cd 100644 --- a/src/Language/ja/Auth.php +++ b/src/Language/ja/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/src/Language/pt-BR/Auth.php b/src/Language/pt-BR/Auth.php index 2feb6a2df..dda64e644 100644 --- a/src/Language/pt-BR/Auth.php +++ b/src/Language/pt-BR/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/src/Language/sk/Auth.php b/src/Language/sk/Auth.php index 5a73ba29f..5d504b5be 100644 --- a/src/Language/sk/Auth.php +++ b/src/Language/sk/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/src/Language/tr/Auth.php b/src/Language/tr/Auth.php index 56cea0615..48c7a247f 100644 --- a/src/Language/tr/Auth.php +++ b/src/Language/tr/Auth.php @@ -61,6 +61,7 @@ '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.', diff --git a/tests/Authentication/Authenticators/SessionAuthenticatorTest.php b/tests/Authentication/Authenticators/SessionAuthenticatorTest.php index 2a3249f00..662b32fd0 100644 --- a/tests/Authentication/Authenticators/SessionAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/SessionAuthenticatorTest.php @@ -12,6 +12,7 @@ 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; @@ -303,6 +304,34 @@ public function testCheckSuccess(): void $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([ diff --git a/tests/Controllers/LoginTest.php b/tests/Controllers/LoginTest.php index c27190e01..b18ca3f3d 100644 --- a/tests/Controllers/LoginTest.php +++ b/tests/Controllers/LoginTest.php @@ -8,6 +8,7 @@ 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; @@ -61,6 +62,52 @@ public function testLoginBadEmail(): void $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', '>=')) { diff --git a/tests/Controllers/RegisterTest.php b/tests/Controllers/RegisterTest.php index 3a44098f4..320db297f 100644 --- a/tests/Controllers/RegisterTest.php +++ b/tests/Controllers/RegisterTest.php @@ -8,6 +8,7 @@ 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; @@ -81,6 +82,46 @@ public function testRegisterActionSuccess(): void $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'); diff --git a/tests/Unit/PasswordsTest.php b/tests/Unit/PasswordsTest.php index 62ebe2066..7398fc533 100644 --- a/tests/Unit/PasswordsTest.php +++ b/tests/Unit/PasswordsTest.php @@ -23,4 +23,40 @@ public function testEmptyPassword(): void $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); + } }