diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b596d4a9a..3f55bd999 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x - name: Install Dependencies diff --git a/UPGRADING.md b/UPGRADING.md index e004c9d2c..c0248a97f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,13 @@ # Upgrade Guide +## Version 1.0.0-beta.8 to 1.0.0 + +## Removed Deprecated Items + +The [$supportOldDangerousPassword](#if-you-want-to-allow-login-with-existing-passwords) +feature for backward compatiblity has been removed. The old passwords saved in +Shield v1.0.0-beta.3 or earlier are no longer supported. + ## Version 1.0.0-beta.7 to 1.0.0-beta.8 ### Mandatory Config Changes diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 5940307c3..123d57d6d 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -38,6 +38,8 @@ the changelog. * [ ] Clone **codeigniter4/shield** and resolve any necessary PRs ```console + rm -rf shield.bk + mv shield shield.bk git clone git@github.com:codeigniter4/shield.git ``` * [ ] Merge any Security Advisory PRs in private forks diff --git a/composer.json b/composer.json index f7eed49ca..5c89f271f 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "mockery/mockery": "^1.0", "phpstan/extension-installer": "^1.3", "phpstan/phpstan-strict-rules": "^1.5", - "rector/rector": "0.18.10" + "rector/rector": "0.18.13" }, "provide": { "codeigniter4/authentication-implementation": "1.0" diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 000000000..c30bf3c28 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +shield.codeigniter.com diff --git a/docs/assets/css/codeigniter.css b/docs/assets/css/codeigniter.css new file mode 100644 index 000000000..98952073d --- /dev/null +++ b/docs/assets/css/codeigniter.css @@ -0,0 +1,18 @@ +[data-md-color-scheme="codeigniter"] { + --md-primary-fg-color: #dd4814; + --md-primary-fg-color--light: #ECB7B7; + --md-primary-fg-color--dark: #90030C; + + --md-default-bg-color: #fcfcfc; + + --md-typeset-a-color: #e74c3c; + --md-accent-fg-color: #97310e; + + --md-accent-fg-color--transparent: #ECB7B7; + + --md-code-bg-color: #ffffff; + + .md-typeset code { + border: 1px solid #e1e4e5; + } +} diff --git a/docs/assets/css/dark_mode.css b/docs/assets/css/codeigniter_dark_mode.css similarity index 77% rename from docs/assets/css/dark_mode.css rename to docs/assets/css/codeigniter_dark_mode.css index 8bc72d6a8..88ff364be 100644 --- a/docs/assets/css/dark_mode.css +++ b/docs/assets/css/codeigniter_dark_mode.css @@ -1,8 +1,17 @@ [data-md-color-scheme="slate"] { - --md-primary-fg-color: #6a290d; + --md-primary-fg-color: #b13a10; --md-primary-fg-color--light: #8d7474; --md-primary-fg-color--dark: #6d554d; + --md-default-bg-color: #1e2129; + + --md-typeset-a-color: #ed6436; + --md-accent-fg-color: #f18a67; + + --md-accent-fg-color--transparent: #625151; + + --md-code-bg-color: #282b2d; + .hljs-title, .hljs-title.class_, .hljs-title.class_.inherited__, @@ -43,31 +52,30 @@ color: #ddba52 } - .md-typeset .note > .admonition-title, - .md-typeset .note > summary { - background-color: #0000001a; + .md-typeset code { + border: 1px solid #3f4547; } .md-typeset .admonition.note, .md-typeset details.note { - border-color: #675647; + border-color: #2c5293; } .md-typeset .note > .admonition-title:before, .md-typeset .note > summary:before { - background-color: #65686d; + background-color: #2c5293; -webkit-mask-image: var(--md-admonition-icon--note); mask-image: var(--md-admonition-icon--note); } .md-typeset .admonition.warning, .md-typeset details.warning { - border-color: #776144; + border-color: #97631e; } .md-typeset .warning > .admonition-title:before, .md-typeset .warning > summary:before { - background-color: #d9913bc2; + background-color: #97631e; -webkit-mask-image: var(--md-admonition-icon--warning); mask-image: var(--md-admonition-icon--warning); } diff --git a/docs/assets/js/hljs.js b/docs/assets/js/hljs.js index 6f9098ac1..56159c4c9 100644 --- a/docs/assets/js/hljs.js +++ b/docs/assets/js/hljs.js @@ -1,3 +1,3 @@ -document.addEventListener('DOMContentLoaded', (event) => { +window.document$.subscribe(() => { hljs.highlightAll(); }); diff --git a/docs/customization/adding_attributes_to_users.md b/docs/customization/adding_attributes_to_users.md index 05530c6f3..83488afd2 100644 --- a/docs/customization/adding_attributes_to_users.md +++ b/docs/customization/adding_attributes_to_users.md @@ -74,6 +74,8 @@ php spark db:table users See [Customizing User Provider](./user_provider.md). +Don't forget to add the added attributes to the `$allowedFields` property. + ## Update Validation Rules You need to update the [validation rules](./validation_rules.md) for registration. diff --git a/docs/customization/route_config.md b/docs/customization/route_config.md index 09a15da09..7b06c7d70 100644 --- a/docs/customization/route_config.md +++ b/docs/customization/route_config.md @@ -19,6 +19,16 @@ $routes->get('register', '\App\Controllers\Auth\RegisterController::registerView After customization, check your routes with the [spark routes](https://codeigniter.com/user_guide/incoming/routing.html#spark-routes) command. +## Change Namespace + +If you are overriding all of the auth controllers, you can specify the namespace as an option to the `routes()` helper: + +```php +service('auth')->routes($routes, ['namespace' => '\App\Controllers\Auth']); +``` + +This will generate the routes with the specified namespace instead of the default Shield namespace. This can be combined with any other options, like `except`. + ## Use Locale Routes You can use the `{locale}` placeholder in your routes diff --git a/docs/customization/user_provider.md b/docs/customization/user_provider.md index 12246043c..379574e80 100644 --- a/docs/customization/user_provider.md +++ b/docs/customization/user_provider.md @@ -1,5 +1,7 @@ # Customizing User Provider +## Creating Your Own UserModel + If you want to customize user attributes, you need to create your own [User Provider](../getting_started/concepts.md#user-providers) class. The only requirement is that your new class MUST extend the provided `CodeIgniter\Shield\Models\UserModel`. @@ -13,8 +15,42 @@ php spark shield:model UserModel The class name is optional. If none is provided, the generated class name would be `UserModel`. -After creating the class, set the `$userProvider` property in **app/Config/Auth.php** as follows: +## Configuring to Use Your UserModel + +After creating the class, set your model classname to the `$userProvider` property +in **app/Config/Auth.php**: ```php public string $userProvider = \App\Models\UserModel::class; ``` + +## Customizing Your UserModel + +Customize your model as you like. + +If you add attributes, don't forget to add the attributes to the `$allowedFields` +property. + +```php +allowedFields = [ + ...$this->allowedFields, + 'first_name', // Added + 'last_name', // Added + ]; + } +} +``` diff --git a/docs/index.md b/docs/index.md index dc4865884..10a91e15e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1,12 @@ # Shield Documentation -## What is Shield? 🤔 +## What is Shield? Shield is the official authentication and authorization framework for CodeIgniter 4. While it does provide a base set of tools that are commonly used in websites, it is designed to be flexible and easily customizable. -### Primary Goals 🥅 +### Primary Goals The primary goals for Shield are: @@ -14,27 +14,27 @@ The primary goals for Shield are: 2. It must have security at its core. It is an auth lib after all. 3. To cover many auth needs right out of the box, but be simple to add additional functionality to. -### Important Features 🌠 +### Important Features -* **Session-based Authentication** (traditional **ID/Password** with **Remember-me**) -* **Stateless Authentication** using **Access Token**, **HMAC SHA256 Token**, or **JWT** -* Optional **Email verification** on account registration -* Optional **Email-based Two-Factor Authentication** after login -* **Magic Link Login** when a user forgets their password -* Flexible **Group-based Access Control** (think Roles, but more flexible), and users can be granted additional **Permissions** -* A simple **Auth Helper** that provides access to the most common auth actions -* Save initial settings in your code, so it can be in version control, but can also be updated in the database, thanks to our [Settings](https://github.com/codeigniter4/settings) library -* Highly configurable -* **User Entity** and **User Provider** (`UserModel`) ready for you to use or extend -* Built to extend and modify - * Easily extendable controllers - * All required views that can be used as is or swapped out for your own +- **Session-based Authentication** (traditional **ID/Password** with **Remember-me**) +- **Stateless Authentication** using **Access Token**, **HMAC SHA256 Token**, or **JWT** +- Optional **Email verification** on account registration +- Optional **Email-based Two-Factor Authentication** after login +- **Magic Link Login** when a user forgets their password +- Flexible **Group-based Access Control** (think Roles, but more flexible), and users can be granted additional **Permissions** +- A simple **Auth Helper** that provides access to the most common auth actions +- Save initial settings in your code, so it can be in version control, but can also be updated in the database, thanks to our [Settings](https://github.com/codeigniter4/settings) library +- Highly configurable +- **User Entity** and **User Provider** (`UserModel`) ready for you to use or extend +- Built to extend and modify + - Easily extendable controllers + - All required views that can be used as is or swapped out for your own -### License 📑 +### License Shield is licensed under the MIT License - see the [LICENSE](https://github.com/codeigniter4/shield/blob/develop/LICENSE) file for details. -### Acknowledgements 🙌🏼 +### Acknowledgements Every open-source project depends on it's contributors to be a success. The following users have contributed in one manner or another in making Shield: @@ -48,7 +48,7 @@ Made with [contrib.rocks](https://contrib.rocks). The following articles/sites have been fundamental in shaping the security and best practices used 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) +- [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/mkdocs.yml b/mkdocs.yml index 24808cccf..bfd21dc99 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,23 +12,22 @@ theme: palette: # Palette toggle for light mode - media: "(prefers-color-scheme: light)" - scheme: default - primary: deep orange - accent: orange + scheme: codeigniter + primary: custom + accent: custom toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: deep orange - accent: orange + primary: custom + accent: custom toggle: icon: material/brightness-4 name: Switch to light mode features: - navigation.instant - - navigation.instant.prefetch - content.code.copy - navigation.footer - content.action.edit @@ -55,7 +54,7 @@ extra: link: https://join.slack.com/t/codeigniterchat/shared_invite/zt-244xrrslc-l_I69AJSi5y2a2RVN~xIdQ name: Slack - +site_url: https://shield.codeigniter.com/ repo_url: https://github.com/codeigniter4/shield edit_uri: edit/develop/docs/ copyright: Copyright © 2023 CodeIgniter Foundation. @@ -68,8 +67,9 @@ markdown_extensions: - pymdownx.details extra_css: - - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/styles/default.min.css - - assets/css/dark_mode.css + - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/styles/github.min.css + - assets/css/codeigniter.css + - assets/css/codeigniter_dark_mode.css extra_javascript: - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/highlight.min.js diff --git a/phpstan-baseline.php b/phpstan-baseline.php index a261d48ba..dd4fafb64 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -311,31 +311,11 @@ 'count' => 1, 'path' => __DIR__ . '/src/Models/UserIdentityModel.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot unset offset \'email\' on array\\{username\\: string, status\\: string, status_message\\: string, active\\: bool, last_active\\: string, deleted_at\\: string\\}\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/src/Models/UserModel.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot unset offset \'password_hash\' on array\\{username\\: string, status\\: string, status_message\\: string, active\\: bool, last_active\\: string, deleted_at\\: string\\}\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/src/Models/UserModel.php', -]; $ignoreErrors[] = [ 'message' => '#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#', 'count' => 2, 'path' => __DIR__ . '/src/Models/UserModel.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Offset \'email\' does not exist on array\\{username\\: string, status\\: string, status_message\\: string, active\\: bool, last_active\\: string, deleted_at\\: string\\}\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/src/Models/UserModel.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Offset \'password_hash\' does not exist on array\\{username\\: string, status\\: string, status_message\\: string, active\\: bool, last_active\\: string, deleted_at\\: string\\}\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/src/Models/UserModel.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$data \\(array\\|CodeIgniter\\\\Shield\\\\Entities\\\\User\\) of method CodeIgniter\\\\Shield\\\\Models\\\\UserModel\\:\\:insert\\(\\) should be contravariant with parameter \\$data \\(array\\|object\\|null\\) of method CodeIgniter\\\\Model\\:\\:insert\\(\\)$#', 'count' => 1, @@ -368,7 +348,7 @@ ]; $ignoreErrors[] = [ 'message' => '#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with \'CodeIgniter\\\\\\\\Shield\\\\\\\\Result\' and CodeIgniter\\\\Shield\\\\Result will always evaluate to true\\.$#', - 'count' => 9, + 'count' => 8, 'path' => __DIR__ . '/tests/Authentication/Authenticators/SessionAuthenticatorTest.php', ]; $ignoreErrors[] = [ diff --git a/src/Auth.php b/src/Auth.php index f64e7cbaa..a082c8e9a 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -41,7 +41,7 @@ class Auth /** * The current version of CodeIgniter Shield */ - public const SHIELD_VERSION = '1.0.0-beta.8'; + public const SHIELD_VERSION = '1.0.0'; protected AuthConfig $config; protected ?Authentication $authenticate = null; @@ -138,7 +138,9 @@ public function routes(RouteCollection &$routes, array $config = []): void { $authRoutes = config('AuthRoutes')->routes; - $routes->group('/', ['namespace' => 'CodeIgniter\Shield\Controllers'], static function (RouteCollection $routes) use ($authRoutes, $config): void { + $namespace = $config['namespace'] ?? 'CodeIgniter\Shield\Controllers'; + + $routes->group('/', ['namespace' => $namespace], static function (RouteCollection $routes) use ($authRoutes, $config): void { foreach ($authRoutes as $name => $row) { if (! isset($config['except']) || ! in_array($name, $config['except'], true)) { foreach ($row as $params) { diff --git a/src/Authentication/Authenticators/Session.php b/src/Authentication/Authenticators/Session.php index 98e4a29a0..c480dae5c 100644 --- a/src/Authentication/Authenticators/Session.php +++ b/src/Authentication/Authenticators/Session.php @@ -102,8 +102,8 @@ private function checkSecurityConfig(): void if ($securityConfig->csrfProtection === 'cookie') { throw new SecurityException( 'Config\Security::$csrfProtection is set to \'cookie\'.' - . ' Same-site attackers may bypass the CSRF protection.' - . ' Please set it to \'session\'.' + . ' Same-site attackers may bypass the CSRF protection.' + . ' Please set it to \'session\'.' ); } } @@ -343,30 +343,19 @@ 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)) { - 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; + return new Result([ + 'success' => false, + 'reason' => lang('Auth.invalidPassword'), + ]); } // 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) || $needsRehash) { + if ($passwords->needsRehash($user->password_hash)) { $user->password_hash = $passwords->hash($givenPassword); $this->provider->save($user); } @@ -661,10 +650,10 @@ public function startLogin(User $user): void if ($userId !== null) { throw new LogicException( 'The user has User Info in Session, so already logged in or in pending login state.' - . ' If a logged in user logs in again with other account, the session data of the previous' - . ' user will be used as the new user.' - . ' Fix your code to prevent users from logging in without logging out or delete the session data.' - . ' user_id: ' . $userId + . ' If a logged in user logs in again with other account, the session data of the previous' + . ' user will be used as the new user.' + . ' Fix your code to prevent users from logging in without logging out or delete the session data.' + . ' user_id: ' . $userId ); } @@ -749,18 +738,18 @@ public function login(User $user): void if ($this->getIdentitiesForAction($user) !== []) { throw new LogicException( 'The user has identities for action, so cannot complete login.' - . ' If you want to start to login with auth action, use startLogin() instead.' - . ' Or delete identities for action in database.' - . ' user_id: ' . $user->id + . ' If you want to start to login with auth action, use startLogin() instead.' + . ' Or delete identities for action in database.' + . ' user_id: ' . $user->id ); } // Check auth_action in Session if ($this->getSessionKey('auth_action')) { throw new LogicException( 'The user has auth action in session, so cannot complete login.' - . ' If you want to start to login with auth action, use startLogin() instead.' - . ' Or delete `auth_action` and `auth_action_message` in session data.' - . ' user_id: ' . $user->id + . ' If you want to start to login with auth action, use startLogin() instead.' + . ' Or delete `auth_action` and `auth_action_message` in session data.' + . ' user_id: ' . $user->id ); } diff --git a/src/Authentication/Passwords.php b/src/Authentication/Passwords.php index 994c192a8..42f9fe0ed 100644 --- a/src/Authentication/Passwords.php +++ b/src/Authentication/Passwords.php @@ -90,21 +90,6 @@ 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); - } - /** * Checks to see if a password should be rehashed. */ diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 22a052b01..bf4c9ca02 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -374,16 +374,6 @@ class Auth extends BaseConfig */ public int $hashCost = 12; - /** - * 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/Language/bg/Auth.php b/src/Language/bg/Auth.php index 8aa8f898d..b34e37f86 100644 --- a/src/Language/bg/Auth.php +++ b/src/Language/bg/Auth.php @@ -39,7 +39,7 @@ 'password' => 'Парола', 'passwordConfirm' => 'Парола (отново)', 'haveAccount' => 'Вече имате акаунт?', - 'token' => '(To be translated) Token', + 'token' => 'Токен', // Бутони 'confirm' => 'Потвърди', @@ -61,7 +61,7 @@ 'magicLinkExpired' => 'Съжаляваме, линкът е изтекъл.', 'checkYourEmail' => 'Проверете вашия имейл!', 'magicLinkDetails' => 'Току що ви изпратихме имейл с линк за вход. Линкът ще бъде валиден само {0} минути.', - 'magicLinkDisabled' => '(To be translated) Use of MagicLink is currently not allowed.', + 'magicLinkDisabled' => 'Използването на линк за вход в момента не е разрешено.', 'successLogout' => 'Успешно излязохте от системата.', 'backToLogin' => 'Обратно към входа', @@ -83,7 +83,7 @@ 'resetTokenExpired' => 'Съжаляваме. Вашият токен за нулиране на паролата е изтекъл.', // Глобални променливи за електронна поща - 'emailInfo' => 'Някаква информации за потребителя:', + 'emailInfo' => 'Информации за потребител:', 'emailIpAddress' => 'IP Адрес:', 'emailDevice' => 'Устройство:', 'emailDate' => 'Дата:', diff --git a/src/Models/UserModel.php b/src/Models/UserModel.php index 73d293033..2cc1210e3 100644 --- a/src/Models/UserModel.php +++ b/src/Models/UserModel.php @@ -36,7 +36,6 @@ class UserModel extends BaseModel 'status_message', 'active', 'last_active', - 'deleted_at', ]; protected $useTimestamps = true; protected $afterFind = ['fetchIdentities']; @@ -205,6 +204,7 @@ public function findByCredentials(array $credentials): ?User } if ($email !== null) { + /** @var array|null $data */ $data = $this->select( sprintf('%1$s.*, %2$s.secret as email, %2$s.secret2 as password_hash', $this->table, $this->tables['identities']) ) diff --git a/src/Views/email_2fa_show.php b/src/Views/email_2fa_show.php index b030cfde8..58d69ae54 100644 --- a/src/Views/email_2fa_show.php +++ b/src/Views/email_2fa_show.php @@ -22,7 +22,7 @@
+ value="email) ?>" required>
diff --git a/tests/Authentication/Authenticators/SessionAuthenticatorTest.php b/tests/Authentication/Authenticators/SessionAuthenticatorTest.php index ca1a8b53e..3fb04a386 100644 --- a/tests/Authentication/Authenticators/SessionAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/SessionAuthenticatorTest.php @@ -21,7 +21,6 @@ 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; @@ -313,34 +312,6 @@ 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/Language/AbstractTranslationTestCase.php b/tests/Language/AbstractTranslationTestCase.php index 641b65601..4dcaeac47 100644 --- a/tests/Language/AbstractTranslationTestCase.php +++ b/tests/Language/AbstractTranslationTestCase.php @@ -304,6 +304,61 @@ final public function testAllConfiguredLanguageKeysAreInOrder(string $locale): v )); } + /** + * @see https://codeigniter4.github.io/CodeIgniter4/outgoing/localization.html#replacing-parameters + * + * @dataProvider localesProvider + */ + final public function testAllLocalizationParametersAreNotTranslated(string $locale): void + { + $diffs = []; + + foreach ($this->foundSets($locale) as $file) { + $original = $this->loadFile($file); + $translated = $this->loadFile($file, $locale); + + foreach ($original as $key => $translation) { + if (! array_key_exists($key, $translated)) { + continue; + } + + preg_match_all('/(\{[^\}]+\})/', $translation, $matches); + array_shift($matches); + + if ($matches === []) { + unset($matches); + + continue; + } + + foreach ($matches as $match) { + foreach ($match as $parameter) { + if (strpos($translated[$key], (string) $parameter) === false) { + $id = sprintf('%s.%s', substr($file, 0, -4), $key); + + $diffs[$id] ??= []; + + $diffs[$id][] = $parameter; + } + } + } + + unset($matches); + } + } + + ksort($diffs); + + $this->assertEmpty($diffs, sprintf( + "Failed asserting that parameters of translation keys are not translated:\n%s", + implode("\n", array_map( + static fn (string $key, array $values): string => sprintf(' * %s => %s', $key, implode(', ', $values)), + array_keys($diffs), + array_values($diffs) + )) + )); + } + /** * @return string[][] */ diff --git a/tests/Unit/AuthRoutesTest.php b/tests/Unit/AuthRoutesTest.php index 9012b731e..c0c5f2741 100644 --- a/tests/Unit/AuthRoutesTest.php +++ b/tests/Unit/AuthRoutesTest.php @@ -51,4 +51,16 @@ public function testRoutesExcept(): void $this->assertArrayHasKey('logout', $routes); $this->assertArrayHasKey('auth/a/show', $routes); } + + public function testRoutesCustomNamespace(): void + { + $collection = single_service('routes'); + $auth = service('auth'); + + $auth->routes($collection, ['namespace' => 'Auth']); + + $routes = $collection->getRoutes('get'); + + $this->assertSame('\Auth\RegisterController::registerView', $routes['register']); + } } diff --git a/tests/Unit/Authentication/JWT/JWTManagerTest.php b/tests/Unit/Authentication/JWT/JWTManagerTest.php index 6acd82717..684281898 100644 --- a/tests/Unit/Authentication/JWT/JWTManagerTest.php +++ b/tests/Unit/Authentication/JWT/JWTManagerTest.php @@ -182,9 +182,9 @@ public function testIssueAddHeader(): void $this->assertIsString($token); $headers = $this->decodeJWT($token, 'header'); - $this->assertSame([ - 'extra_key' => 'extra_value', + $this->assertEqualsCanonicalizing([ 'typ' => 'JWT', + 'extra_key' => 'extra_value', 'alg' => 'HS256', ], $headers); }