From 16d140a9bff0b486332c55eaf5fa52130cfd4418 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 22 Mar 2023 17:48:47 +0900 Subject: [PATCH 001/142] docs: add RELEASE.md --- admin/RELEASE.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 admin/RELEASE.md diff --git a/admin/RELEASE.md b/admin/RELEASE.md new file mode 100644 index 000000000..ab9f997c7 --- /dev/null +++ b/admin/RELEASE.md @@ -0,0 +1,85 @@ +# Release Process + +> Documentation guide based on the releases of `1.0.0-beta.5` on March 17, 2023. +> +> -kenjis + +## Changelog + +When generating the changelog each Pull Request to be included must have one of +the following [labels](https://github.com/codeigniter4/shield/labels): +- **bug** ... PRs that fix bugs +- **enhancement** ... PRs to improve existing functionalities +- **new feature** ... PRs for new features +- **refactor** ... PRs to refactor + +PRs with breaking changes must have the following additional label: +- **breaking change** ... PRs that may break existing functionalities + +### Check Generated Changelog + +This process is checking only. Do not create a release. + +To auto-generate, navigate to the +[Releases](https://github.com/codeigniter4/shield/releases) page, +click the "Draft a new release" button. + +* Tag: "v1.0.0-beta.5" (Create new tag) +* Target: develop + +Click the "Generate release notes" button. + +Check the resulting content. If there are items in the *Others* section which +should be included in the changelog, add a label to the PR and regenerate +the changelog. + +## Preparation + +* Clone **codeigniter4/shield** and resolve any necessary PRs +```console +git clone git@github.com:codeigniter4/shield.git +``` +* Merge any Security Advisory PRs in private forks + +## Process + +> **Note** Most changes that need noting in the User Guide and docs should have +> been included with their PR, so this process assumes you will not be +> generating much new content. + +* Create a new branch `release-1.x.x` +* Update **src/Auth.php** with the new version number: + `const SHIELD_VERSION = '1.x.x';` +* Commit the changes with "Prep for 1.x.x release" and push to origin +* Create a new PR from `release-1.x.x` to `develop`: + * Title: "Prep for 1.x.x release" + * Description: "Updates version references for `1.x.x`." (plus checklist) +* Let all tests run, then review and merge the PR +* Create a new PR from `develop` to `master`: + * Title: "1.x.x Ready code" + * Description: blank +* Merge the PR +* Create a new Release: + * Version: "v1.x.x" + * Target: master + * Title: "v1.x.x" + * Click the "Generate release notes" button + * Remove "### Others (Only for checking. Remove this category)" section + * Click the "Publish release" button +* Watch for the "docs" action and verify that the user guide updated: + * [docs](https://github.com/codeigniter4/shield/actions/workflows/docs.yml) +* Fast-forward `develop` branch to catch the merge commit from `master` +```console +git fetch origin +git checkout develop +git merge origin/develop +git merge origin/master +git push origin HEAD # @TODO can't push to protected branch. +``` +* Publish any Security Advisories that were resolved from private forks +* Announce the release on the forums and Slack channel + (note: this forum is restricted to administrators): + * Make a new topic in the "News & Discussion" forums: + https://forum.codeigniter.com/forum-2.html + * The content is somewhat organic, but should include any major features and + changes as well as a link to the User Guide's changelog From 92b9359b3ccb78465c8015bbcefbbe162d138672 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 22 Mar 2023 19:07:59 +0900 Subject: [PATCH 002/142] docs: add about label lang Co-authored-by: Pooya Parsa Dadashi --- admin/RELEASE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/RELEASE.md b/admin/RELEASE.md index ab9f997c7..3db3bcc0c 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -12,6 +12,7 @@ the following [labels](https://github.com/codeigniter4/shield/labels): - **enhancement** ... PRs to improve existing functionalities - **new feature** ... PRs for new features - **refactor** ... PRs to refactor +- **lang** ... PRs for new/update language PRs with breaking changes must have the following additional label: - **breaking change** ... PRs that may break existing functionalities From 426fecf3348dfedc314e19bff3e959219fa34b44 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 22 Mar 2023 19:15:05 +0900 Subject: [PATCH 003/142] docs: add about "Create a discussion for this release" --- admin/RELEASE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 3db3bcc0c..ef6456905 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -66,6 +66,7 @@ git clone git@github.com:codeigniter4/shield.git * Title: "v1.x.x" * Click the "Generate release notes" button * Remove "### Others (Only for checking. Remove this category)" section + * Check "Create a discussion for this release" * Click the "Publish release" button * Watch for the "docs" action and verify that the user guide updated: * [docs](https://github.com/codeigniter4/shield/actions/workflows/docs.yml) From 7d62c89144c8a3cd26e65f8f6d45632c9fcd4fb1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 22 Mar 2023 19:17:52 +0900 Subject: [PATCH 004/142] docs: indent git commands --- admin/RELEASE.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/admin/RELEASE.md b/admin/RELEASE.md index ef6456905..a844e9a1f 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -37,9 +37,9 @@ the changelog. ## Preparation * Clone **codeigniter4/shield** and resolve any necessary PRs -```console -git clone git@github.com:codeigniter4/shield.git -``` + ```console + git clone git@github.com:codeigniter4/shield.git + ``` * Merge any Security Advisory PRs in private forks ## Process @@ -71,13 +71,13 @@ git clone git@github.com:codeigniter4/shield.git * Watch for the "docs" action and verify that the user guide updated: * [docs](https://github.com/codeigniter4/shield/actions/workflows/docs.yml) * Fast-forward `develop` branch to catch the merge commit from `master` -```console -git fetch origin -git checkout develop -git merge origin/develop -git merge origin/master -git push origin HEAD # @TODO can't push to protected branch. -``` + ```console + git fetch origin + git checkout develop + git merge origin/develop + git merge origin/master + git push origin HEAD # @TODO can't push to protected branch. + ``` * Publish any Security Advisories that were resolved from private forks * Announce the release on the forums and Slack channel (note: this forum is restricted to administrators): From 86e99574f4e661c3aed1bc8cf5d08e1ae5d8a4bd Mon Sep 17 00:00:00 2001 From: Marco Monteiro Date: Thu, 23 Mar 2023 10:18:43 +0000 Subject: [PATCH 005/142] Language file for native portuguese language --- src/Language/pt/Auth.php | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/Language/pt/Auth.php diff --git a/src/Language/pt/Auth.php b/src/Language/pt/Auth.php new file mode 100644 index 000000000..4a9057c3e --- /dev/null +++ b/src/Language/pt/Auth.php @@ -0,0 +1,101 @@ + '{0} não é um autenticador válido.', + 'unknownUserProvider' => 'Não foi possível determinar o provedor de utilizador a ser usado.', + 'invalidUser' => 'Não foi possível localizar o utilizador especificado.', + 'bannedUser' => 'Não é possível fazer login porque está banido de momento.', + 'logOutBannedUser' => 'Foi desconectado porque foi banido.', + 'badAttempt' => 'Não foi possível fazer login. Por favor, verifique as suas credenciais.', + 'noPassword' => 'Não é possível validar um utilizador sem uma password.', + 'invalidPassword' => 'Não foi possível fazer login. Por favor, verifique a sua password.', + 'noToken' => 'Todos os pedidos devem ter um token portador no cabeçalho {0}.', + 'badToken' => 'O token de acesso é inválido.', + 'oldToken' => 'O token de acesso expirou.', + 'noUserEntity' => 'A entidade do utilizador deve ser fornecida para validação da password.', + 'invalidEmail' => 'Não foi possível verificar se o endereço de email corresponde ao e-mail registado.', + 'unableSendEmailToUser' => 'Desculpe, houve um problema ao enviar o email. Não pudemos enviar um email para {0}.', + 'throttled' => 'Muitas solicitações feitas a partir deste endereço IP. Pode tentar novamente em {0} segundos.', + 'notEnoughPrivilege' => 'Não tem a permissão necessária para realizar a operação desejada.', + + 'email' => 'Endereço de Email', + 'username' => 'Nome de utilizador', + 'password' => 'Senha', + 'passwordConfirm' => 'Senha (novamente)', + 'haveAccount' => 'Já tem uma conta?', + + // Botões + 'confirm' => 'Confirmar', + 'send' => 'Enviar', + + // Registro + 'register' => 'Registar', + 'registerDisabled' => 'O registo não é permitido no momento.', + 'registerSuccess' => 'Bem-vindo a bordo!', + + // Login + 'login' => 'Login', + 'needAccount' => 'Precisa de uma conta?', + 'rememberMe' => 'Lembrar', + 'forgotPassword' => 'Esqueceu a sua password?', + 'useMagicLink' => 'Use um Link de Login', + 'magicLinkSubject' => 'O seu Link de Login', + 'magicTokenNotFound' => 'Não foi possível verificar o link.', + 'magicLinkExpired' => 'Desculpe, o link expirou.', + 'checkYourEmail' => 'Verifique o seu e-mail!', + 'magicLinkDetails' => 'Acabamos de enviar um e-mail com um link de Login. Ele é válido apenas por {0} minutos.', + 'successLogout' => 'Saiu com sucesso.', + + // Senhas + 'errorPasswordLength' => 'As passwords devem ter pelo menos {0, number} caracteres.', + 'suggestPasswordLength' => 'Frases de password - até 255 caracteres - criam passwords mais seguras que são fáceis de lembrar.', + 'errorPasswordCommon' => 'A password não deve ser uma password comum.', + 'suggestPasswordCommon' => 'A password foi verificada contra mais de 65k passwords comuns ou passwords que foram vazadas por invasões.', + 'errorPasswordPersonal' => 'As passwords não podem conter informações pessoais re-criptografadas.', + 'suggestPasswordPersonal' => 'Variações do seu endereço de e-mail ou nome de utilizador não devem ser usadas como passwords.', + 'errorPasswordTooSimilar' => 'A password é muito semelhante ao nome de utilizador.', + 'suggestPasswordTooSimilar' => 'Não use partes do seu nome de utilizador na sua password.', + 'errorPasswordPwned' => 'A password {0} foi exposta devido a uma violação de dados e foi vista {1, number} vezes em {2} de passwords comprometidas.', + 'suggestPasswordPwned' => '{0} nunca deve ser usado como uma password. Se você estiver usando em algum lugar, altere imediatamente.', + 'errorPasswordEmpty' => 'É necessária uma password.', + 'errorPasswordTooLongBytes' => 'A password não pode exceder {param} bytes.', + 'passwordChangeSuccess' => 'Senha alterada com sucesso', + 'userDoesNotExist' => 'Senha não foi alterada. utilizador não existe', + 'resetTokenExpired' => 'Desculpe. Seu token de redefinição expirou.', + + // E-mails Globais + 'emailInfo' => 'Algumas informações sobre a pessoa:', + 'emailIpAddress' => 'Endereço IP:', + 'emailDevice' => 'Dispositivo:', + 'emailDate' => 'Data:', + + // 2FA + 'email2FATitle' => 'Autenticação de dois fatores', + 'confirmEmailAddress' => 'Confirme seu endereço de e-mail.', + 'emailEnterCode' => 'Confirme seu email', + 'emailConfirmCode' => 'Insira o código de 6 dígitos que acabamos de enviar para seu endereço de e-mail.', + 'email2FASubject' => 'Seu código de autenticação', + 'email2FAMailBody' => 'Seu código de autenticação é:', + 'invalid2FAToken' => 'O código estava incorreto.', + 'need2FA' => 'Deve concluir uma verificação de dois fatores.', + 'needVerification' => 'Verifique seu e-mail para concluir a ativação da conta.', + + // Ativar + 'emailActivateTitle' => 'Ativação de email', + 'emailActivateBody' => 'Acabamos de enviar um email para você com um código para confirmar seu endereço de e-mail. Copie esse código e cole abaixo.', + 'emailActivateSubject' => 'O seu código de ativação', + 'emailActivateMailBody' => 'Use o código abaixo para ativar sua conta e começar a usar o site.', + 'invalidActivateToken' => 'O código estava incorreto.', + 'needActivate' => 'Deve concluir seu registro confirmando o código enviado para seu endereço de e-mail.', + 'activationBlocked' => 'Deve ativar sua conta antes de fazer o login.', + + // Grupos + 'unknownGroup' => '{0} não é um grupo válido.', + 'missingTitle' => 'Os grupos devem ter um título.', + + // Permissões + 'unknownPermission' => '{0} não é uma permissão válida.', +]; From 993b7d0df918127a92a3b6710e6ef7f103a8a15d Mon Sep 17 00:00:00 2001 From: Marco Monteiro Date: Thu, 23 Mar 2023 10:39:10 +0000 Subject: [PATCH 006/142] unit testing for portuguese language --- tests/Language/AbstractTranslationTestCase.php | 2 +- tests/Language/PortugueseTranslationTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/Language/PortugueseTranslationTest.php diff --git a/tests/Language/AbstractTranslationTestCase.php b/tests/Language/AbstractTranslationTestCase.php index 49e114184..5389609e4 100644 --- a/tests/Language/AbstractTranslationTestCase.php +++ b/tests/Language/AbstractTranslationTestCase.php @@ -65,7 +65,7 @@ abstract class AbstractTranslationTestCase extends TestCase // DutchTranslationTest::class => 'nl', // NorwegianTranslationTest::class => 'no', // PolishTranslationTest::class => 'pl', - // PortugueseTranslationTest::class => 'pt', + PortugueseTranslationTest::class => 'pt', BrazilianTranslationTest::class => 'pt-BR', // RussianTranslationTest::class => 'ru', // SinhalaTranslationTest::class => 'si', diff --git a/tests/Language/PortugueseTranslationTest.php b/tests/Language/PortugueseTranslationTest.php new file mode 100644 index 000000000..1b74b90ad --- /dev/null +++ b/tests/Language/PortugueseTranslationTest.php @@ -0,0 +1,15 @@ + Date: Thu, 23 Mar 2023 11:12:45 +0000 Subject: [PATCH 007/142] coding standards fix on the AbstractTranslationTestCase class --- tests/Language/AbstractTranslationTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Language/AbstractTranslationTestCase.php b/tests/Language/AbstractTranslationTestCase.php index 5389609e4..94b5503ba 100644 --- a/tests/Language/AbstractTranslationTestCase.php +++ b/tests/Language/AbstractTranslationTestCase.php @@ -65,7 +65,7 @@ abstract class AbstractTranslationTestCase extends TestCase // DutchTranslationTest::class => 'nl', // NorwegianTranslationTest::class => 'no', // PolishTranslationTest::class => 'pl', - PortugueseTranslationTest::class => 'pt', + PortugueseTranslationTest::class => 'pt', BrazilianTranslationTest::class => 'pt-BR', // RussianTranslationTest::class => 'ru', // SinhalaTranslationTest::class => 'si', From e7fc1855aa75c6e7644f3946abf18e1cbe2ca9d4 Mon Sep 17 00:00:00 2001 From: Marco Monteiro Date: Thu, 23 Mar 2023 11:46:42 +0000 Subject: [PATCH 008/142] coding standards fix on the AbstractTranslationTestCase class --- tests/Language/AbstractTranslationTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Language/AbstractTranslationTestCase.php b/tests/Language/AbstractTranslationTestCase.php index 94b5503ba..30252fc8a 100644 --- a/tests/Language/AbstractTranslationTestCase.php +++ b/tests/Language/AbstractTranslationTestCase.php @@ -66,7 +66,7 @@ abstract class AbstractTranslationTestCase extends TestCase // NorwegianTranslationTest::class => 'no', // PolishTranslationTest::class => 'pl', PortugueseTranslationTest::class => 'pt', - BrazilianTranslationTest::class => 'pt-BR', + BrazilianTranslationTest::class => 'pt-BR', // RussianTranslationTest::class => 'ru', // SinhalaTranslationTest::class => 'si', SlovakTranslationTest::class => 'sk', From c0908357560d9756d6e7c2153b1930dbc8def596 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Mar 2023 07:57:28 +0900 Subject: [PATCH 009/142] docs: add note for permissions --- admin/RELEASE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/admin/RELEASE.md b/admin/RELEASE.md index a844e9a1f..27f73b24a 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -71,14 +71,16 @@ the changelog. * Watch for the "docs" action and verify that the user guide updated: * [docs](https://github.com/codeigniter4/shield/actions/workflows/docs.yml) * Fast-forward `develop` branch to catch the merge commit from `master` + (note: pushing to develop is restricted to administrators): ```console git fetch origin git checkout develop git merge origin/develop git merge origin/master - git push origin HEAD # @TODO can't push to protected branch. + git push origin HEAD # Only administrators can push to the protected branch. ``` * Publish any Security Advisories that were resolved from private forks + (note: publishing is restricted to administrators) * Announce the release on the forums and Slack channel (note: this forum is restricted to administrators): * Make a new topic in the "News & Discussion" forums: From d4b5e58f976a47dd90f53d13954e83bfbc11c63f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 25 Mar 2023 12:52:57 +0900 Subject: [PATCH 010/142] docs: replace command with function Because it is a PHP user function. --- docs/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authentication.md b/docs/authentication.md index 9d5f505c5..e505f8662 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -50,7 +50,7 @@ public $defaultAuthenticator = 'session'; ## Auth Helper The auth functionality is designed to be used with the `auth_helper` that comes with Shield. This -helper method provides the `auth()` command which returns a convenient interface to the most frequently +helper method provides the `auth()` function which returns a convenient interface to the most frequently used functionality within the auth libraries. ```php From da1666f7f003e385f78e573f028efa343f765361 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 25 Mar 2023 12:54:09 +0900 Subject: [PATCH 011/142] docs: add auth()->getProvider() --- docs/authentication.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/authentication.md b/docs/authentication.md index e505f8662..3b177d3dc 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -61,6 +61,9 @@ auth()->user(); auth()->id(); // or user_id(); + +// get the User Provider (UserModel by default) +auth()->getProvider(); ``` > **Note** From 9e4f5c02e9cef9f8e49b18a392e70d235e4c386f Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 25 Mar 2023 12:55:25 +0900 Subject: [PATCH 012/142] docs: remplace model('UserModel') with auth()->getProvider() --- docs/quickstart.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 70b960f9c..0e639136e 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -271,7 +271,7 @@ By default, the only values stored in the users table is the username. The first ```php use CodeIgniter\Shield\Entities\User; -$users = model('UserModel'); +$users = auth()->getProvider(); $user = new User([ 'username' => 'foo-bar', 'email' => 'foo.bar@example.com', @@ -291,7 +291,7 @@ $users->addToDefaultGroup($user); A user's data can be spread over a few different tables so you might be concerned about how to delete all of the user's data from the system. This is handled automatically at the database level for all information that Shield knows about, through the `onCascade` settings of the table's foreign keys. You can delete a user like any other entity. ```php -$users = model('UserModel'); +$users = auth()->getProvider(); $users->delete($user->id, true); ``` @@ -302,7 +302,7 @@ $users->delete($user->id, true); The `UserModel::save()`, `update()` and `insert()` methods have been modified to ensure that an email or password previously set on the `User` entity will be automatically updated in the correct `UserIdentity` record. ```php -$users = model('UserModel'); +$users = auth()->getProvider(); $user = $users->findById(123); $user->fill([ From da6e3cc9b19a0dfd864f62f88943b70457231fa4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 25 Mar 2023 12:58:22 +0900 Subject: [PATCH 013/142] docs: add comments in sample code --- docs/quickstart.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 0e639136e..49e25a38f 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -271,7 +271,9 @@ By default, the only values stored in the users table is the username. The first ```php use CodeIgniter\Shield\Entities\User; +// Get the User Provider (UserModel by default) $users = auth()->getProvider(); + $user = new User([ 'username' => 'foo-bar', 'email' => 'foo.bar@example.com', @@ -291,7 +293,9 @@ $users->addToDefaultGroup($user); A user's data can be spread over a few different tables so you might be concerned about how to delete all of the user's data from the system. This is handled automatically at the database level for all information that Shield knows about, through the `onCascade` settings of the table's foreign keys. You can delete a user like any other entity. ```php +// Get the User Provider (UserModel by default) $users = auth()->getProvider(); + $users->delete($user->id, true); ``` @@ -302,9 +306,10 @@ $users->delete($user->id, true); The `UserModel::save()`, `update()` and `insert()` methods have been modified to ensure that an email or password previously set on the `User` entity will be automatically updated in the correct `UserIdentity` record. ```php +// Get the User Provider (UserModel by default) $users = auth()->getProvider(); -$user = $users->findById(123); +$user = $users->findById(123); $user->fill([ 'username' => 'JoeSmith111', 'email' => 'joe.smith@example.com', From afe8c9c647abee1aeb61994c6b79104c7d629657 Mon Sep 17 00:00:00 2001 From: Celio Marcos Date: Tue, 28 Mar 2023 17:54:10 -0300 Subject: [PATCH 014/142] Update Auth.php Revision and Update translated text to Spanish(Spain) language --- src/Language/es/Auth.php | 122 +++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/src/Language/es/Auth.php b/src/Language/es/Auth.php index ab72b1608..557c3a68a 100644 --- a/src/Language/es/Auth.php +++ b/src/Language/es/Auth.php @@ -4,27 +4,27 @@ return [ // Excepciones - 'unknownAuthenticator' => '{0} no es un handler válido.', - 'unknownUserProvider' => 'No podemos determinar que Proveedor de Usuarios usar.', - 'invalidUser' => 'No podemos localizar este usuario.', - 'bannedUser' => '(To be translated) Can not log you in as you are currently banned.', - 'logOutBannedUser' => '(To be translated) You have been logged out because you have been banned.', - 'badAttempt' => 'No puedes entrar. Por favor, comprueba tus creenciales.', - 'noPassword' => 'No se puede validar un usuario sin una contraseña.', - 'invalidPassword' => 'No uedes entrar. Por favor, comprueba tu contraseña.', - 'noToken' => 'Cada petición debe tenerun token en la {0}.', - 'badToken' => 'Token de acceso no válido.', + 'unknownAuthenticator' => '{0} no es un autenticador válido.', + 'unknownUserProvider' => 'No se puede determinar el proveedor de usuario a utilizar.', + 'invalidUser' => 'No se puede localizar al usuario especificado.', + 'bannedUser' => 'No puedes iniciar sesión ya que estás actualmente vetado.', + 'logOutBannedUser' => 'Se ha cerrado la sesión porque se ha vetado al usuario.', + 'badAttempt' => 'No se puede iniciar sesión. Por favor, comprueba tus credenciales.', + 'noPassword' => 'No se puede validar un usuario sin contraseña.', + 'invalidPassword' => 'No se puede iniciar sesión. Por favor, comprueba tu contraseña.', + 'noToken' => 'Cada solicitud debe tener un token de portador en la cabecera {0}.', + 'badToken' => 'El token de acceso no es válido.', 'oldToken' => 'El token de acceso ha caducado.', - 'noUserEntity' => 'Se debe dar una Entidad de Usuario para validar la contraseña.', - 'invalidEmail' => 'No podemos verificar que el email coincida con un email registrado.', - 'unableSendEmailToUser' => 'Lo sentimaos, ha habido un problema al enviar el email. No podemos enviar un email a "{0}".', - 'throttled' => 'Demasiadas peticiones hechas desde esta IP. Puedes intentarlo de nuevo en {0} segundos.', - 'notEnoughPrivilege' => 'No tiene los permisos necesarios para realizar la operación deseada.', + 'noUserEntity' => 'Se debe proporcionar una entidad de usuario para la validación de contraseña.', + 'invalidEmail' => 'No se puede verificar que la dirección de correo electrónico coincida con la registrada.', + 'unableSendEmailToUser' => 'Lo siento, hubo un problema al enviar el correo electrónico. No pudimos enviar un correo electrónico a "{0}".', + 'throttled' => 'Se han realizado demasiadas solicitudes desde esta dirección IP. Puedes intentarlo de nuevo en {0} segundos.', + 'notEnoughPrivilege' => 'No tienes los permisos necesarios para realizar la operación deseada.', - 'email' => 'Dirección Email', - 'username' => 'Usuario', + 'email' => 'Correo Electrónico', + 'username' => 'Nombre de usuario', 'password' => 'Contraseña', - 'passwordConfirm' => 'Contraseña (de nuevo)', + 'passwordConfirm' => 'Contraseña (otra vez)', 'haveAccount' => '¿Ya tienes una cuenta?', // Botones @@ -32,70 +32,70 @@ 'send' => 'Enviar', // Registro - 'register' => 'Registro', - 'registerDisabled' => 'Actualmente no se permiten registros.', + 'register' => 'Registrarse', + 'registerDisabled' => 'Actualmente no se permite el registro.', 'registerSuccess' => '¡Bienvenido a bordo!', // Login - 'login' => 'Entrar', + 'login' => 'Iniciar sesión', 'needAccount' => '¿Necesitas una cuenta?', - 'rememberMe' => '¿Recordarme?', - 'forgotPassword' => '¿Has olvidado tu contraseña?', - 'useMagicLink' => 'Recordar contraseña', - 'magicLinkSubject' => 'Tu Enlace para Entrar', - 'magicTokenNotFound' => 'No podemos verificar el enlace.', - 'magicLinkExpired' => 'Lo sentimos, el enlace ha caducado.', - 'checkYourEmail' => 'Comprueba tu email', - 'magicLinkDetails' => 'Te hemos enviado un email que contiene un enlace para Entrar. Solo es válido durante {0} minutos.', - 'successLogout' => 'Has salido de forma correcta.', + 'rememberMe' => 'Recordarme', + 'forgotPassword' => '¿Olvidaste tu contraseña', + 'useMagicLink' => 'Usar un enlace de inicio de sesión', + 'magicLinkSubject' => 'Tu enlace de inicio de sesión', + 'magicTokenNotFound' => 'No se puede verificar el enlace.', + 'magicLinkExpired' => 'Lo siento, el enlace ha caducado.', + 'checkYourEmail' => '¡Revisa tu correo electrónico!', + 'magicLinkDetails' => 'Acabamos de enviarte un correo electrónico con un enlace de inicio de sesión. Solo es válido durante {0} minutos.', + 'successLogout' => 'Has cerrado sesión correctamente.', // Contraseñas - 'errorPasswordLength' => 'La contraseña debe tener al menos {0, number} caracteres.', - 'suggestPasswordLength' => 'Las claves de acceso, de hasta 255 caracteres, crean contraseñas más seguras y fáciles de recordar.', - 'errorPasswordCommon' => 'La contraseña no debe ser una contraseña común.', - 'suggestPasswordCommon' => 'La contraseña se comparó con más de 65.000 contraseñas de uso común o contraseñas que se filtraron a través de hacks.', - 'errorPasswordPersonal' => 'Las contraseñas no pueden contener información personal modificada.', - 'suggestPasswordPersonal' => 'No deben usarse variaciones de tu dirección de correo electrónico o nombre de usuario para contraseñas.', - '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.', + 'errorPasswordLength' => 'Las contraseñas deben tener al menos {0, number} caracteres.', + 'suggestPasswordLength' => 'Las frases de contraseña, de hasta 255 caracteres de longitud, hacen que las contraseñas sean más seguras y fáciles de recordar.', + 'errorPasswordCommon' => 'La contraseña no puede ser una contraseña común.', + 'suggestPasswordCommon' => 'La contraseña se comprobó frente a más de 65k contraseñas comúnmente utilizadas o contraseñas que se filtraron a través de ataques.', + 'errorPasswordPersonal' => 'Las contraseñas no pueden contener información personal reutilizada.', + 'suggestPasswordPersonal' => 'No se deben usar variaciones de su dirección de correo electrónico o nombre de usuario como contraseña.', + 'errorPasswordTooSimilar' => 'La contraseña es demasiado similar al nombre de usuario.', + 'suggestPasswordTooSimilar' => 'No use partes de su nombre de usuario en su contraseña.', + 'errorPasswordPwned' => 'La contraseña {0} se ha expuesto debido a una violación de datos y se ha visto {1, number} veces en {2} de contraseñas comprometidas.', + 'suggestPasswordPwned' => 'Nunca se debe usar {0} como contraseña. Si lo está utilizando en algún lugar, cambie su contraseña de inmediato.', + 'errorPasswordEmpty' => 'Se requiere una contraseña.', + 'errorPasswordTooLongBytes' => 'La contraseña no puede tener más de {param} caracteres', + 'passwordChangeSuccess' => 'Contraseña cambiada correctamente', + 'userDoesNotExist' => 'La contraseña no se cambió. El usuario no existe', + 'resetTokenExpired' => 'Lo siento. Su token de reinicio ha caducado.', // Email Globals - 'emailInfo' => 'Algunos datos sobre la persona:', + 'emailInfo' => 'Alguna información sobre la persona:', 'emailIpAddress' => 'Dirección IP:', 'emailDevice' => 'Dispositivo:', 'emailDate' => 'Fecha:', // 2FA - 'email2FATitle' => 'Authenticación de Doble Factor', - 'confirmEmailAddress' => 'Confirma tu dirección de email.', - 'emailEnterCode' => 'Confirma tu Email', - 'emailConfirmCode' => 'teclea el código de 6 dígitos qu ete hemos enviado a tu dirección email.', + 'email2FATitle' => 'Autenticación de dos factores', + 'confirmEmailAddress' => 'Confirma tu dirección de correo electrónico.', + 'emailEnterCode' => 'Confirma tu correo electrónico', + 'emailConfirmCode' => 'Ingresa el código de 6 dígitos que acabamos de enviar a tu correo electrónico.', 'email2FASubject' => 'Tu código de autenticación', 'email2FAMailBody' => 'Tu código de autenticación es:', - 'invalid2FAToken' => 'El token era incorrecto.', - 'need2FA' => 'Debes completar la verificación de doble factor.', - 'needVerification' => 'Comprueba tu buzón para completar la activación de la cuenta.', - + 'invalid2FAToken' => 'El código era incorrecto.', + 'need2FA' => 'Debes completar la verificación de dos factores.', + 'needVerification' => 'Verifica tu correo electrónico para completar la activación de la cuenta.', + // Activar - 'emailActivateTitle' => 'Email de Activación', - 'emailActivateBody' => 'Te enviamos un email con un código, para confirmar tu dirección email. Copia ese código y pégalo abajo.', + 'emailActivateTitle' => 'Activación de correo electrónico', + 'emailActivateBody' => 'Acabamos de enviarte un correo electrónico con un código para confirmar tu dirección de correo electrónico. Copia ese código y pégalo a continuación.', 'emailActivateSubject' => 'Tu código de activación', - 'emailActivateMailBody' => 'Por favor, usa el código de abajo para activar tu cuenta y empezar a usar el sitio.', - 'invalidActivateToken' => 'El código no es correcto.', - 'needActivate' => '(To be translated) You must complete your registration by confirming the code sent to your email address.', - 'activationBlocked' => '(to be translated) You must activate your account before logging in.', + 'emailActivateMailBody' => 'Utiliza el código siguiente para activar tu cuenta y comenzar a usar el sitio.', + 'invalidActivateToken' => 'El código era incorrecto.', + 'needActivate' => 'Debes completar tu registro confirmando el código enviado a tu dirección de correo electrónico.', + 'activationBlocked' => 'Debes activar tu cuenta antes de iniciar sesión.', // Grupos 'unknownGroup' => '{0} no es un grupo válido.', 'missingTitle' => 'Los grupos deben tener un título.', // Permisos - 'unknownPermission' => '{0} no es un permiso válido.', + 'unknownPermission' => '{0} no es un permiso válido.', ]; From fe3bb3a70028990800364b9e26d87f6dc0cc3bc6 Mon Sep 17 00:00:00 2001 From: Celio Marcos Date: Tue, 28 Mar 2023 18:18:37 -0300 Subject: [PATCH 015/142] Update Auth.php PHPCSFixer fix --- src/Language/es/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Language/es/Auth.php b/src/Language/es/Auth.php index 557c3a68a..31d92e2d2 100644 --- a/src/Language/es/Auth.php +++ b/src/Language/es/Auth.php @@ -97,5 +97,5 @@ 'missingTitle' => 'Los grupos deben tener un título.', // Permisos - 'unknownPermission' => '{0} no es un permiso válido.', + 'unknownPermission' => '{0} no es un permiso válido.' ]; From fabc9ae059ea4de7ce0bd4c8165137c799021f2d Mon Sep 17 00:00:00 2001 From: Celio Marcos Date: Thu, 6 Apr 2023 16:25:47 -0300 Subject: [PATCH 016/142] cs-fix: removed withespace in blankline --- src/Language/es/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Language/es/Auth.php b/src/Language/es/Auth.php index 31d92e2d2..14ab3514d 100644 --- a/src/Language/es/Auth.php +++ b/src/Language/es/Auth.php @@ -82,7 +82,7 @@ 'invalid2FAToken' => 'El código era incorrecto.', 'need2FA' => 'Debes completar la verificación de dos factores.', 'needVerification' => 'Verifica tu correo electrónico para completar la activación de la cuenta.', - + // Activar 'emailActivateTitle' => 'Activación de correo electrónico', 'emailActivateBody' => 'Acabamos de enviarte un correo electrónico con un código para confirmar tu dirección de correo electrónico. Copia ese código y pégalo a continuación.', From 333f2730a37111714b569852d10dc6721f13a7f1 Mon Sep 17 00:00:00 2001 From: Celio Marcos Date: Thu, 6 Apr 2023 16:46:20 -0300 Subject: [PATCH 017/142] cs-fix: trailing_comma_in_multiline line_ending --- src/Language/es/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Language/es/Auth.php b/src/Language/es/Auth.php index 14ab3514d..bd99cb003 100644 --- a/src/Language/es/Auth.php +++ b/src/Language/es/Auth.php @@ -97,5 +97,5 @@ 'missingTitle' => 'Los grupos deben tener un título.', // Permisos - 'unknownPermission' => '{0} no es un permiso válido.' + 'unknownPermission' => '{0} no es un permiso válido.', ]; From 60105c7c799922216baf070d9125bb96ed677f21 Mon Sep 17 00:00:00 2001 From: Miguel Renaud-Nolte Date: Mon, 10 Apr 2023 02:48:22 +0000 Subject: [PATCH 018/142] fix: accept only valid data from POST With the current data validation check, an empty POST request with valid GET parameters will cause the validation to pass but a TypeError will be thrown because on line 110 it is fetching null data from POST. --- src/Controllers/RegisterController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/RegisterController.php b/src/Controllers/RegisterController.php index 2fe94c31a..6f394faa0 100644 --- a/src/Controllers/RegisterController.php +++ b/src/Controllers/RegisterController.php @@ -100,7 +100,7 @@ public function registerAction(): RedirectResponse // like the password, can only be validated properly here. $rules = $this->getValidationRules(); - if (! $this->validate($rules)) { + if (! $this->validateData($this->request->getPost(), $rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } From a96ccdf503d80a1c474b54b5f7b03e49d23b6be1 Mon Sep 17 00:00:00 2001 From: Miguel Renaud-Nolte Date: Mon, 10 Apr 2023 02:55:33 +0000 Subject: [PATCH 019/142] fix: only validate data from POST request body With the current data validation check, an empty POST request with valid GET parameters will cause the validation to pass but credentials which are later fetched from POST request body will be null. --- src/Controllers/LoginController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php index 9cff8f3aa..8c5bc445d 100644 --- a/src/Controllers/LoginController.php +++ b/src/Controllers/LoginController.php @@ -47,7 +47,7 @@ public function loginAction(): RedirectResponse // like the password, can only be validated properly here. $rules = $this->getValidationRules(); - if (! $this->validate($rules)) { + if (! $this->validateData($this->request->getPost(), $rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } From eed24a4380336f14e19b7e29a7728719f86cab17 Mon Sep 17 00:00:00 2001 From: Miguel Renaud-Nolte Date: Mon, 10 Apr 2023 02:59:21 +0000 Subject: [PATCH 020/142] fix: only validate data from POST request body With the current data validation check, an empty POST request with valid GET parameters will cause the validation to pass later when data is fetched from POST request body, it will be null. --- src/Controllers/MagicLinkController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/MagicLinkController.php b/src/Controllers/MagicLinkController.php index 84fdfbb7e..2f081a82c 100644 --- a/src/Controllers/MagicLinkController.php +++ b/src/Controllers/MagicLinkController.php @@ -65,7 +65,7 @@ public function loginAction() { // Validate email format $rules = $this->getValidationRules(); - if (! $this->validate($rules)) { + if (! $this->validateData($this->request->getPost(), $rules)) { return redirect()->route('magic-link')->with('errors', $this->validator->getErrors()); } From 75ec7e564cf74c1636429708dc9cd95e21a3a456 Mon Sep 17 00:00:00 2001 From: Miguel Renaud-Nolte Date: Mon, 10 Apr 2023 23:19:08 +0000 Subject: [PATCH 021/142] Update mobile_apps.md Fix validation check to only validate POST request body. https://github.com/codeigniter4/shield/pull/695 --- docs/guides/mobile_apps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/mobile_apps.md b/docs/guides/mobile_apps.md index bece95b2a..611ab919e 100644 --- a/docs/guides/mobile_apps.md +++ b/docs/guides/mobile_apps.md @@ -34,7 +34,7 @@ class LoginController extends BaseController ], ]; - if (! $this->validate($rules)) { + if (! $this->validateData($this->request->getPost(), $rules)) { return $this->response ->setJSON(['errors' => $this->validator->getErrors()]) ->setStatusCode(422); From 9c7aacb1c11bcbe4728f7a23f1f58785b852be0c Mon Sep 17 00:00:00 2001 From: Samuel Asor Date: Tue, 11 Apr 2023 07:23:20 +0100 Subject: [PATCH 022/142] get credentials for login --- docs/guides/mobile_apps.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/guides/mobile_apps.md b/docs/guides/mobile_apps.md index bece95b2a..668c4b387 100644 --- a/docs/guides/mobile_apps.md +++ b/docs/guides/mobile_apps.md @@ -40,8 +40,13 @@ class LoginController extends BaseController ->setStatusCode(422); } + // Get the credentials for login + $credentials = $this->request->getPost(setting('Auth.validFields')); + $credentials = array_filter($credentials); + $credentials['password'] = $this->request->getPost('password'); + // Attempt to login - $result = auth()->attempt($this->request->getPost(setting('Auth.validFields'))); + $result = auth()->attempt($credentials); if (! $result->isOK()) { return $this->response ->setJSON(['error' => $result->reason()]) From daa0da56c16b2a173da1904586a07803f3eb98ba Mon Sep 17 00:00:00 2001 From: Samuel Asor Date: Tue, 11 Apr 2023 09:52:44 +0100 Subject: [PATCH 023/142] add device_name to validation rules --- docs/guides/mobile_apps.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guides/mobile_apps.md b/docs/guides/mobile_apps.md index 6f5e157b3..4a22b81d5 100644 --- a/docs/guides/mobile_apps.md +++ b/docs/guides/mobile_apps.md @@ -32,6 +32,10 @@ class LoginController extends BaseController 'label' => 'Auth.password', 'rules' => 'required', ], + 'device_name' => [ + 'label' => 'Device Name', + 'rules' => 'required|string', + ], ]; if (! $this->validateData($this->request->getPost(), $rules)) { From 2c8f488b25a570d4e0447ed3a565ec31fe1c8c78 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Apr 2023 14:11:07 +0900 Subject: [PATCH 024/142] docs: remove unneeded "of" --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index b37202eed..c9add2306 100644 --- a/docs/install.md +++ b/docs/install.md @@ -108,7 +108,7 @@ Require it with an explicit version constraint allowing its desired stability. There are a few setup items to do before you can start using Shield in your project. -1. Copy the **Auth.php** and **AuthGroups.php** from **vendor/codeigniter4/shield/src/Config/** into your project's config folder and update the namespace to `Config`. You will also need to have these classes extend the original classes. See the example below. These files contain all of the settings, group, and permission information for your application and will need to be modified to meet the needs of your site. +1. Copy the **Auth.php** and **AuthGroups.php** from **vendor/codeigniter4/shield/src/Config/** into your project's config folder and update the namespace to `Config`. You will also need to have these classes extend the original classes. See the example below. These files contain all the settings, group, and permission information for your application and will need to be modified to meet the needs of your site. ```php // new file - app/Config/Auth.php From 709191298de12758d6a0ad48c6fd7720111553f9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Apr 2023 14:11:44 +0900 Subject: [PATCH 025/142] docs: update sample config file --- docs/install.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/install.md b/docs/install.md index c9add2306..6514e936b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -114,6 +114,8 @@ your project. // new file - app/Config/Auth.php Date: Tue, 18 Apr 2023 10:51:15 +0900 Subject: [PATCH 026/142] fix: incorrect base class for exceptions All exceptions by Shield should implement BaseException. --- src/Exceptions/SecurityException.php | 2 -- src/Exceptions/ValidationException.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Exceptions/SecurityException.php b/src/Exceptions/SecurityException.php index b0646c057..c72d7aa04 100644 --- a/src/Exceptions/SecurityException.php +++ b/src/Exceptions/SecurityException.php @@ -4,8 +4,6 @@ namespace CodeIgniter\Shield\Exceptions; -use RuntimeException; - class SecurityException extends RuntimeException { } diff --git a/src/Exceptions/ValidationException.php b/src/Exceptions/ValidationException.php index 9b6d51d52..3aae180ef 100644 --- a/src/Exceptions/ValidationException.php +++ b/src/Exceptions/ValidationException.php @@ -4,8 +4,6 @@ namespace CodeIgniter\Shield\Exceptions; -use RuntimeException; - class ValidationException extends RuntimeException { } From 7716ee08d91117966fd545d3626e75bc5ea745e5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 May 2022 17:25:34 +0900 Subject: [PATCH 027/142] chore: add firebase/php-jwt All users do not use JWT, so I install as dev package. --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8fb7cb750..415bc738e 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "codeigniter4/devkit": "^1.0", "codeigniter4/framework": "^4.2.7", "mikey179/vfsstream": "^1.6.7", - "mockery/mockery": "^1.0" + "mockery/mockery": "^1.0", + "firebase/php-jwt": "^6.2" }, "provide": { "codeigniter4/authentication-implementation": "1.0" From 9ec8cc7fe17349dbdc839339be0982494662838d Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 1 Jun 2022 16:46:42 +0900 Subject: [PATCH 028/142] feat: add Authenticators\JWT --- src/Authentication/Authenticators/JWT.php | 244 ++++++++++++++++++ .../Authenticators/JWT/Firebase.php | 41 +++ .../JWT/JWTDecoderInterface.php | 15 ++ .../TokenGenerator/JWT/Firebase.php | 18 ++ .../JWT/JWTGeneratorInterface.php | 13 + .../TokenGenerator/JWTGenerator.php | 41 +++ src/Config/Auth.php | 25 ++ .../Authenticators/JWTAuthenticatorTest.php | 237 +++++++++++++++++ .../Authenticators/JWT/FirebaseTest.php | 67 +++++ .../TokenGenerator/JWTGeneratorTest.php | 41 +++ 10 files changed, 742 insertions(+) create mode 100644 src/Authentication/Authenticators/JWT.php create mode 100644 src/Authentication/Authenticators/JWT/Firebase.php create mode 100644 src/Authentication/Authenticators/JWT/JWTDecoderInterface.php create mode 100644 src/Authentication/TokenGenerator/JWT/Firebase.php create mode 100644 src/Authentication/TokenGenerator/JWT/JWTGeneratorInterface.php create mode 100644 src/Authentication/TokenGenerator/JWTGenerator.php create mode 100644 tests/Authentication/Authenticators/JWTAuthenticatorTest.php create mode 100644 tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php create mode 100644 tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php new file mode 100644 index 000000000..2bf34f3e8 --- /dev/null +++ b/src/Authentication/Authenticators/JWT.php @@ -0,0 +1,244 @@ +provider = $provider; + $this->jwtDecoder = $jwtDecoder ?? new Firebase(); + + $this->loginModel = model(TokenLoginModel::class); // @phpstan-ignore-line + } + + /** + * Attempts to authenticate a user with the given $credentials. + * Logs the user in with a successful check. + * + * @param array{token?: string} $credentials + */ + public function attempt(array $credentials): Result + { + /** @var IncomingRequest $request */ + $request = service('request'); + + $ipAddress = $request->getIPAddress(); + $userAgent = $request->getUserAgent(); + + $result = $this->check($credentials); + + if (! $result->isOK()) { + // Always record a login attempt, whether success or not. + $this->loginModel->recordLoginAttempt( + self::ID_TYPE_JWT, + $credentials['token'] ?? '', + false, + $ipAddress, + $userAgent + ); + + return $result; + } + + $user = $result->extraInfo(); + + $this->login($user); + + $this->loginModel->recordLoginAttempt( + self::ID_TYPE_JWT, + $credentials['token'] ?? '', + true, + $ipAddress, + $userAgent, + $this->user->id + ); + + return $result; + } + + /** + * Checks a user's $credentials to see if they match an + * existing user. + * + * In this case, $credentials has only a single valid value: token, + * which is the plain text token to return. + * + * @param array{token?: string} $credentials + */ + public function check(array $credentials): Result + { + if (! array_key_exists('token', $credentials) || $credentials['token'] === '') { + return new Result([ + 'success' => false, + 'reason' => lang('Auth.noToken'), + ]); + } + + if (strpos($credentials['token'], 'Bearer') === 0) { + $credentials['token'] = trim(substr($credentials['token'], 6)); + } + + // Check JWT + try { + $this->payload = $this->decodeJWT($credentials['token']); + } catch (RuntimeException $e) { + return new Result([ + 'success' => false, + 'reason' => $e->getMessage(), + ]); + } + + $userId = $this->payload->sub ?? null; + + if ($userId === null) { + return new Result([ + 'success' => false, + 'reason' => 'Invalid JWT: no user_id', + ]); + } + + // Find User + $user = $this->provider->findById($userId); + + if ($user === null) { + return new Result([ + 'success' => false, + 'reason' => lang('Auth.invalidUser'), + ]); + } + + return new Result([ + 'success' => true, + 'extraInfo' => $user, + ]); + } + + /** + * Checks if the user is currently logged in. + * Since AccessToken usage is inherently stateless, + * it runs $this->attempt on each usage. + */ + public function loggedIn(): bool + { + if ($this->user !== null) { + return true; + } + + /** @var IncomingRequest $request */ + $request = service('request'); + + return $this->attempt([ + 'token' => $request->getHeaderLine(config('Auth')->authenticatorHeader['jwt']), + ])->isOK(); + } + + /** + * Logs the given user in by saving them to the class. + */ + public function login(User $user): void + { + $this->user = $user; + } + + /** + * Logs a user in based on their ID. + * + * @param int|string $userId + * + * @throws AuthenticationException + */ + public function loginById($userId): void + { + $user = $this->provider->findById($userId); + + if ($user === null) { + throw AuthenticationException::forInvalidUser(); + } + + $this->login($user); + } + + /** + * Logs the current user out. + */ + public function logout(): bool + { + $this->user = null; + + return true; + } + + /** + * Returns the currently logged in user. + */ + public function getUser(): ?User + { + return $this->user; + } + + /** + * Updates the user's last active date. + */ + public function recordActiveDate(): void + { + if (! $this->user instanceof User) { + throw new InvalidArgumentException( + __METHOD__ . '() requires logged in user before calling.' + ); + } + + $this->user->last_active = Time::now(); + + $this->provider->save($this->user); + } + + /** + * Returns payload of the JWT + */ + public function decodeJWT(string $encodedToken): stdClass + { + return $this->jwtDecoder->decode($encodedToken); + } + + /** + * Returns payload + */ + public function getPayload(): ?stdClass + { + return $this->payload; + } +} diff --git a/src/Authentication/Authenticators/JWT/Firebase.php b/src/Authentication/Authenticators/JWT/Firebase.php new file mode 100644 index 000000000..ba4a2bd38 --- /dev/null +++ b/src/Authentication/Authenticators/JWT/Firebase.php @@ -0,0 +1,41 @@ +getMessage(), 0, $e); + } catch ( + InvalidArgumentException|DomainException|UnexpectedValueException + |SignatureInvalidException $e + ) { + throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); + } + } +} diff --git a/src/Authentication/Authenticators/JWT/JWTDecoderInterface.php b/src/Authentication/Authenticators/JWT/JWTDecoderInterface.php new file mode 100644 index 000000000..7c9f66a27 --- /dev/null +++ b/src/Authentication/Authenticators/JWT/JWTDecoderInterface.php @@ -0,0 +1,15 @@ +currentTime = $currentTime ?? new Time(); + $this->jwtGenerator = $jwtGenerator ?? new Firebase(); + } + + /** + * Issues JWT Access Token + */ + public function generateAccessToken(User $user): string + { + $config = setting('Auth.jwtConfig'); + + $iat = $this->currentTime->getTimestamp(); + $exp = $iat + $config['timeToLive']; + + $payload = [ + 'iss' => $config['issuer'], // issuer + 'aud' => $config['audience'], // audience + 'sub' => (string) $user->id, // subject + 'iat' => $iat, // issued at + 'exp' => $exp, // expiration time + ]; + + return $this->jwtGenerator->generate($payload, $config['secretKey'], $config['algorithm']); + } +} diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 6cdd5d9b8..a45f4cc42 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -8,6 +8,7 @@ use CodeIgniter\Shield\Authentication\Actions\ActionInterface; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; use CodeIgniter\Shield\Authentication\Authenticators\AccessTokens; +use CodeIgniter\Shield\Authentication\Authenticators\JWT; use CodeIgniter\Shield\Authentication\Authenticators\Session; use CodeIgniter\Shield\Authentication\Passwords\CompositionValidator; use CodeIgniter\Shield\Authentication\Passwords\DictionaryValidator; @@ -122,6 +123,7 @@ class Auth extends BaseConfig public array $authenticators = [ 'tokens' => AccessTokens::class, 'session' => Session::class, + 'jwt' => JWT::class, ]; /** @@ -134,6 +136,7 @@ class Auth extends BaseConfig */ public array $authenticatorHeader = [ 'tokens' => 'Authorization', + 'jwt' => 'Authorization', ]; /** @@ -232,6 +235,28 @@ class Auth extends BaseConfig 'rememberLength' => 30 * DAY, ]; + /** + * -------------------------------------------------------------------- + * JWT Authenticator Configuration + * -------------------------------------------------------------------- + * These settings only apply if you are using the JWT Authenticator + * for authentication. + * + * - secretKey The secret key. Needs more than 256 bits random string. + * E.g., $ php -r 'echo base64_encode(random_bytes(32));' + * - algorithm JWT Signing Algorithms. + * - timeToLive Specifies the amount of time, in seconds, that a token is valid. + * + * @var array + */ + public array $jwtConfig = [ + 'issuer' => '', + 'audience' => '', + 'secretKey' => '', + 'algorithm' => 'HS256', + 'timeToLive' => 1 * HOUR, + ]; + /** * -------------------------------------------------------------------- * Minimum Password Length diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php new file mode 100644 index 000000000..c755b630a --- /dev/null +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -0,0 +1,237 @@ +setProvider(\model(UserModel::class)); // @phpstan-ignore-line + + /** @var JWT $authenticator */ + $authenticator = $auth->factory('jwt'); + $this->auth = $authenticator; + + Services::injectMock('events', new MockEvents()); + } + + private function createUser(): User + { + return \fake(UserModel::class); + } + + public function testLogin() + { + $user = $this->createUser(); + + $this->auth->login($user); + + // Stores the user + $this->assertInstanceOf(User::class, $this->auth->getUser()); + $this->assertSame($user->id, $this->auth->getUser()->id); + } + + public function testLogout() + { + // this one's a little odd since it's stateless, but roll with it... + + $user = $this->createUser(); + + $this->auth->login($user); + $this->assertNotNull($this->auth->getUser()); + + $this->auth->logout(); + $this->assertNull($this->auth->getUser()); + } + + public function testLoginById() + { + $user = $this->createUser(); + + $this->assertFalse($this->auth->loggedIn()); + + $this->auth->loginById($user->id); + + $this->assertTrue($this->auth->loggedIn()); + } + + public function testLoginByIdNoUser() + { + $this->expectException(AuthenticationException::class); + $this->expectExceptionMessage('Unable to locate the specified user.'); + + $this->createUser(); + + $this->assertFalse($this->auth->loggedIn()); + + $this->auth->loginById(9999); + } + + public function testCheckNoToken() + { + $result = $this->auth->check([]); + + $this->assertFalse($result->isOK()); + $this->assertSame(\lang('Auth.noToken'), $result->reason()); + } + + public function testCheckBadSignatureToken() + { + $result = $this->auth->check(['token' => self::BAD_JWT]); + + $this->assertFalse($result->isOK()); + $this->assertSame('Invalid JWT: Signature verification failed', $result->reason()); + } + + public function testCheckNoSubToken() + { + $config = setting('Auth.jwtConfig'); + $payload = [ + 'iss' => $config['issuer'], // issuer + 'aud' => $config['audience'], // audience + ]; + $token = FirebaseJWT::encode($payload, $config['secretKey'], $config['algorithm']); + + $result = $this->auth->check(['token' => $token]); + + $this->assertFalse($result->isOK()); + $this->assertSame('Invalid JWT: no user_id', $result->reason()); + } + + public function testCheckOldToken() + { + $currentTime = new Time('-1 hour'); + $token = $this->generateJWT($currentTime); + + $result = $this->auth->check(['token' => $token]); + + $this->assertFalse($result->isOK()); + $this->assertSame('Expired JWT: Expired token', $result->reason()); + } + + public function testCheckNoUserInDatabase() + { + $token = $this->generateJWT(); + + $users = \model(UserModel::class); + $users->delete(1); + + $result = $this->auth->check(['token' => $token]); + + $this->assertFalse($result->isOK()); + $this->assertSame(\lang('Auth.invalidUser'), $result->reason()); + } + + public function testCheckSuccess() + { + $token = $this->generateJWT(); + + $result = $this->auth->check(['token' => $token]); + + $this->assertTrue($result->isOK()); + $this->assertInstanceOf(User::class, $result->extraInfo()); + $this->assertSame(1, $result->extraInfo()->id); + } + + public function testGetPayload() + { + $token = $this->generateJWT(); + + $this->auth->check(['token' => $token]); + $payload = $this->auth->getPayload(); + + $this->assertSame((string) $this->user->id, $payload->sub); + $this->assertSame((\setting('Auth.jwtConfig')['issuer']), $payload->iss); + } + + public function testAttemptBadSignatureToken() + { + $result = $this->auth->attempt([ + 'token' => self::BAD_JWT, + ]); + + $this->assertInstanceOf(Result::class, $result); + $this->assertFalse($result->isOK()); + $this->assertSame('Invalid JWT: Signature verification failed', $result->reason()); + + // A login attempt should have always been recorded + $this->seeInDatabase('auth_token_logins', [ + 'id_type' => JWT::ID_TYPE_JWT, + 'identifier' => self::BAD_JWT, + 'success' => 0, + ]); + } + + public function testAttemptSuccess() + { + $token = $this->generateJWT(); + + $result = $this->auth->attempt([ + 'token' => $token, + ]); + + $this->assertInstanceOf(Result::class, $result); + $this->assertTrue($result->isOK()); + + $foundUser = $result->extraInfo(); + + $this->assertInstanceOf(User::class, $foundUser); + $this->assertSame(1, $foundUser->id); + + // A login attempt should have been recorded + $this->seeInDatabase('auth_token_logins', [ + 'id_type' => JWT::ID_TYPE_JWT, + 'identifier' => $token, + 'success' => 1, + ]); + } + + public function testRecordActiveDateNoUser() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Authentication\Authenticators\JWT::recordActiveDate() requires logged in user before calling.' + ); + + $this->auth->recordActiveDate(); + } + + /** + * @param Time|null $currentTime The current time + */ + private function generateJWT(?Time $currentTime = null): string + { + $this->user = \fake(UserModel::class, ['id' => 1, 'username' => 'John Smith']); + + $generator = new JWTGenerator($currentTime); + + return $generator->generateAccessToken($this->user); + } +} diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php new file mode 100644 index 000000000..07bf11990 --- /dev/null +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php @@ -0,0 +1,67 @@ +generateJWT(); + + $jwtDecoder = new Firebase(); + + $payload = $jwtDecoder->decode($token); + + $this->assertSame(setting('Auth.jwtConfig')['issuer'], $payload->iss); + $this->assertSame(setting('Auth.jwtConfig')['audience'], $payload->aud); + $this->assertSame('1', $payload->sub); + } + + /** + * @param Time|null $currentTime The current time + */ + public static function generateJWT(?Time $currentTime = null): string + { + /** @var User $user */ + $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); + + $generator = new JWTGenerator($currentTime); + + return $generator->generateAccessToken($user); + } + + public function testDecodeSignatureInvalidException() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Invalid JWT: Signature verification failed'); + + $jwtDecoder = new Firebase(); + + $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJc3N1ZXIgb2YgdGhlIEpXVCIsImF1ZCI6IkF1ZGllbmNlIG9mIHRoZSBKV1QiLCJzdWIiOiIxIiwiaWF0IjoxNjUzOTkxOTg5LCJleHAiOjE2NTM5OTU1ODl9.hgOYHEcT6RGHb3po1lspTcmjrylY1Cy1IvYmHOyx0CY'; + $jwtDecoder->decode($token); + } + + public function testDecodeExpiredException() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Expired JWT: Expired token'); + + $jwtDecoder = new Firebase(); + + $currentTime = new Time('-1 hour'); + $token = $this->generateJWT($currentTime); + + $jwtDecoder->decode($token); + } +} diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php new file mode 100644 index 000000000..800c6e51d --- /dev/null +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -0,0 +1,41 @@ + 1, 'username' => 'John Smith'], false); + $generator = new JWTGenerator(); + + $token = $generator->generateAccessToken($user); + + $this->assertIsString($token); + $this->assertStringStartsWith('eyJ', $token); + + return $token; + } + + /** + * @depends testGenerate + */ + public function testTokenSubIsUserId(string $token) + { + $auth = new JWT(new UserModel()); + + $payload = $auth->decodeJWT($token); + + $this->assertSame('1', $payload->sub); + } +} From 0c8bdc7d1a01bf95a0cfbaad5b57f319a958c624 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 1 Jun 2022 16:51:07 +0900 Subject: [PATCH 029/142] feat: add Filters\JWTAuth --- src/Config/Registrar.php | 2 + src/Filters/JWTAuth.php | 70 ++++++++++++++++ .../Authentication/Filters/JWTFilterTest.php | 79 +++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/Filters/JWTAuth.php create mode 100644 tests/Authentication/Filters/JWTFilterTest.php diff --git a/src/Config/Registrar.php b/src/Config/Registrar.php index d3bcf8672..290b036d5 100644 --- a/src/Config/Registrar.php +++ b/src/Config/Registrar.php @@ -10,6 +10,7 @@ use CodeIgniter\Shield\Filters\ChainAuth; use CodeIgniter\Shield\Filters\ForcePasswordResetFilter; use CodeIgniter\Shield\Filters\GroupFilter; +use CodeIgniter\Shield\Filters\JWTAuth; use CodeIgniter\Shield\Filters\PermissionFilter; use CodeIgniter\Shield\Filters\SessionAuth; use CodeIgniter\Shield\Filters\TokenAuth; @@ -30,6 +31,7 @@ public static function Filters(): array 'group' => GroupFilter::class, 'permission' => PermissionFilter::class, 'force-reset' => ForcePasswordResetFilter::class, + 'jwt' => JWTAuth::class, ], ]; } diff --git a/src/Filters/JWTAuth.php b/src/Filters/JWTAuth.php new file mode 100644 index 000000000..01c6c8151 --- /dev/null +++ b/src/Filters/JWTAuth.php @@ -0,0 +1,70 @@ +getAuthenticator(); + + $result = $authenticator->attempt([ + 'token' => $request->getHeaderLine( + setting('Auth.authenticatorHeader')['jwt'] ?? 'Authorization' + ), + ]); + + if (! $result->isOK()) { + return Services::response() + ->setJSON([ + 'error' => $result->reason(), + ]) + ->setStatusCode(ResponseInterface::HTTP_UNAUTHORIZED); + } + + if (setting('Auth.recordActiveDate')) { + $authenticator->recordActiveDate(); + } + } + + /** + * We don't have anything to do here. + * + * @param Response|ResponseInterface $response + * @param array|null $arguments + * + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + } +} diff --git a/tests/Authentication/Filters/JWTFilterTest.php b/tests/Authentication/Filters/JWTFilterTest.php new file mode 100644 index 000000000..5ed72894a --- /dev/null +++ b/tests/Authentication/Filters/JWTFilterTest.php @@ -0,0 +1,79 @@ +aliases['jwtAuth'] = JWTAuth::class; + Factories::injectMock('filters', 'filters', $filterConfig); + + // Add a test route that we can visit to trigger. + $routes = \service('routes'); + $routes->group('/', ['filter' => 'jwtAuth'], static function ($routes) { + $routes->get('protected-route', static function () { + echo 'Protected'; + }); + }); + $routes->get('open-route', static function () { + echo 'Open'; + }); + $routes->get('login', 'AuthController::login', ['as' => 'login']); + Services::injectMock('routes', $routes); + } + + public function testFilterNotAuthorized() + { + $result = $this->call('get', 'protected-route'); + + $result->assertStatus(401); + + $result = $this->get('open-route'); + + $result->assertStatus(200); + $result->assertSee('Open'); + } + + public function testFilterSuccess() + { + /** @var User $user */ + $user = \fake(UserModel::class); + + $generator = new JWTGenerator(); + $token = $generator->generateAccessToken($user); + + $result = $this->withHeaders(['Authorization' => 'Bearer ' . $token]) + ->get('protected-route'); + + $result->assertStatus(200); + $result->assertSee('Protected'); + + $this->assertSame($user->id, \auth('jwt')->id()); + $this->assertSame($user->id, \auth('jwt')->user()->id); + } +} From 7a95425abb8f64d2d2eac464937f5d76fe84a76d Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 1 Jun 2022 17:35:00 +0900 Subject: [PATCH 030/142] feat: remove `Bearer` in token at the first time --- src/Authentication/Authenticators/JWT.php | 4 ---- src/Filters/JWTAuth.php | 21 ++++++++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 2bf34f3e8..dbe90e38f 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -108,10 +108,6 @@ public function check(array $credentials): Result ]); } - if (strpos($credentials['token'], 'Bearer') === 0) { - $credentials['token'] = trim(substr($credentials['token'], 6)); - } - // Check JWT try { $this->payload = $this->decodeJWT($credentials['token']); diff --git a/src/Filters/JWTAuth.php b/src/Filters/JWTAuth.php index 01c6c8151..a24ac7f6b 100644 --- a/src/Filters/JWTAuth.php +++ b/src/Filters/JWTAuth.php @@ -37,11 +37,9 @@ public function before(RequestInterface $request, $arguments = null) /** @var JWT $authenticator */ $authenticator = auth('jwt')->getAuthenticator(); - $result = $authenticator->attempt([ - 'token' => $request->getHeaderLine( - setting('Auth.authenticatorHeader')['jwt'] ?? 'Authorization' - ), - ]); + $token = $this->getTokenFromHeader($request); + + $result = $authenticator->attempt(['token' => $token]); if (! $result->isOK()) { return Services::response() @@ -56,6 +54,19 @@ public function before(RequestInterface $request, $arguments = null) } } + private function getTokenFromHeader(RequestInterface $request): string + { + $tokenHeader = $request->getHeaderLine( + setting('Auth.authenticatorHeader')['jwt'] ?? 'Authorization' + ); + + if (strpos($tokenHeader, 'Bearer') === 0) { + return trim(substr($tokenHeader, 6)); + } + + return $tokenHeader; + } + /** * We don't have anything to do here. * From 3ce9ed9a2fe7780dbf72ee01182814fe441a1aaf Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Jun 2022 10:25:28 +0900 Subject: [PATCH 031/142] config: fix typo Co-authored-by: MGatner --- src/Config/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Auth.php b/src/Config/Auth.php index a45f4cc42..f9a6c9a35 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -252,7 +252,7 @@ class Auth extends BaseConfig public array $jwtConfig = [ 'issuer' => '', 'audience' => '', - 'secretKey' => '', + 'secretKey' => '', 'algorithm' => 'HS256', 'timeToLive' => 1 * HOUR, ]; From 62404b524c5c7ba05635dc1a9d6569e9c28748c7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Jun 2022 11:05:20 +0900 Subject: [PATCH 032/142] refactor: rename classname --- src/Authentication/Authenticators/JWT.php | 4 ++-- .../JWT/{Firebase.php => FirebaseAdapter.php} | 2 +- .../Authentication/Authenticators/JWT/FirebaseTest.php | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/Authentication/Authenticators/JWT/{Firebase.php => FirebaseAdapter.php} (95%) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index dbe90e38f..afe43bf95 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -6,7 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; -use CodeIgniter\Shield\Authentication\Authenticators\JWT\Firebase; +use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; use CodeIgniter\Shield\Authentication\Authenticators\JWT\JWTDecoderInterface; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; @@ -40,7 +40,7 @@ class JWT implements AuthenticatorInterface public function __construct(UserModel $provider, ?JWTDecoderInterface $jwtDecoder = null) { $this->provider = $provider; - $this->jwtDecoder = $jwtDecoder ?? new Firebase(); + $this->jwtDecoder = $jwtDecoder ?? new FirebaseAdapter(); $this->loginModel = model(TokenLoginModel::class); // @phpstan-ignore-line } diff --git a/src/Authentication/Authenticators/JWT/Firebase.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php similarity index 95% rename from src/Authentication/Authenticators/JWT/Firebase.php rename to src/Authentication/Authenticators/JWT/FirebaseAdapter.php index ba4a2bd38..b3f89054f 100644 --- a/src/Authentication/Authenticators/JWT/Firebase.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -13,7 +13,7 @@ use stdClass; use UnexpectedValueException; -class Firebase implements JWTDecoderInterface +class FirebaseAdapter implements JWTDecoderInterface { /** * Decode JWT diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php index 07bf11990..edafa989a 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php @@ -3,7 +3,7 @@ namespace Tests\Unit\Authentication\Authenticators\JWT; use CodeIgniter\I18n\Time; -use CodeIgniter\Shield\Authentication\Authenticators\JWT\Firebase; +use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; @@ -19,7 +19,7 @@ public function testDecode() { $token = $this->generateJWT(); - $jwtDecoder = new Firebase(); + $jwtDecoder = new FirebaseAdapter(); $payload = $jwtDecoder->decode($token); @@ -46,7 +46,7 @@ public function testDecodeSignatureInvalidException() $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Invalid JWT: Signature verification failed'); - $jwtDecoder = new Firebase(); + $jwtDecoder = new FirebaseAdapter(); $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJc3N1ZXIgb2YgdGhlIEpXVCIsImF1ZCI6IkF1ZGllbmNlIG9mIHRoZSBKV1QiLCJzdWIiOiIxIiwiaWF0IjoxNjUzOTkxOTg5LCJleHAiOjE2NTM5OTU1ODl9.hgOYHEcT6RGHb3po1lspTcmjrylY1Cy1IvYmHOyx0CY'; $jwtDecoder->decode($token); @@ -57,7 +57,7 @@ public function testDecodeExpiredException() $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Expired JWT: Expired token'); - $jwtDecoder = new Firebase(); + $jwtDecoder = new FirebaseAdapter(); $currentTime = new Time('-1 hour'); $token = $this->generateJWT($currentTime); From adbc5e578ede63fa02a7e9ae6004dc71be0d1c99 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Jun 2022 11:24:35 +0900 Subject: [PATCH 033/142] refactor: combine JWT interfaces into one --- src/Authentication/Authenticators/JWT.php | 6 +++--- .../Authenticators/JWT/FirebaseAdapter.php | 15 ++++++++++++++- ...erInterface.php => JWTAdapterInterface.php} | 9 ++++++++- .../TokenGenerator/JWT/Firebase.php | 18 ------------------ .../JWT/JWTGeneratorInterface.php | 13 ------------- .../TokenGenerator/JWTGenerator.php | 16 ++++++++++------ ...rebaseTest.php => FirebaseAdapaterTest.php} | 2 +- 7 files changed, 36 insertions(+), 43 deletions(-) rename src/Authentication/Authenticators/JWT/{JWTDecoderInterface.php => JWTAdapterInterface.php} (53%) delete mode 100644 src/Authentication/TokenGenerator/JWT/Firebase.php delete mode 100644 src/Authentication/TokenGenerator/JWT/JWTGeneratorInterface.php rename tests/Unit/Authentication/Authenticators/JWT/{FirebaseTest.php => FirebaseAdapaterTest.php} (97%) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index afe43bf95..0186a4d91 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -7,7 +7,7 @@ use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\Authenticators\JWT\JWTDecoderInterface; +use CodeIgniter\Shield\Authentication\Authenticators\JWT\JWTAdapterInterface; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; use CodeIgniter\Shield\Models\TokenLoginModel; @@ -33,11 +33,11 @@ class JWT implements AuthenticatorInterface protected UserModel $provider; protected ?User $user = null; - protected JWTDecoderInterface $jwtDecoder; + protected JWTAdapterInterface $jwtDecoder; protected TokenLoginModel $loginModel; protected ?stdClass $payload = null; - public function __construct(UserModel $provider, ?JWTDecoderInterface $jwtDecoder = null) + public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtDecoder = null) { $this->provider = $provider; $this->jwtDecoder = $jwtDecoder ?? new FirebaseAdapter(); diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php index b3f89054f..07403ce71 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -13,7 +13,7 @@ use stdClass; use UnexpectedValueException; -class FirebaseAdapter implements JWTDecoderInterface +class FirebaseAdapter implements JWTAdapterInterface { /** * Decode JWT @@ -38,4 +38,17 @@ public static function decode(string $encodedToken): stdClass throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); } } + + /** + * Issues JWT + * + * @param string $key + */ + public static function generate( + array $payload, + $key, + string $algorithm + ): string { + return JWT::encode($payload, $key, $algorithm); + } } diff --git a/src/Authentication/Authenticators/JWT/JWTDecoderInterface.php b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php similarity index 53% rename from src/Authentication/Authenticators/JWT/JWTDecoderInterface.php rename to src/Authentication/Authenticators/JWT/JWTAdapterInterface.php index 7c9f66a27..dab01ff76 100644 --- a/src/Authentication/Authenticators/JWT/JWTDecoderInterface.php +++ b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php @@ -4,8 +4,15 @@ use stdClass; -interface JWTDecoderInterface +interface JWTAdapterInterface { + /** + * Issues JWT + * + * @param string $key The secret key. + */ + public static function generate(array $payload, $key, string $algorithm): string; + /** * Decode JWT * diff --git a/src/Authentication/TokenGenerator/JWT/Firebase.php b/src/Authentication/TokenGenerator/JWT/Firebase.php deleted file mode 100644 index 323070f9a..000000000 --- a/src/Authentication/TokenGenerator/JWT/Firebase.php +++ /dev/null @@ -1,18 +0,0 @@ -currentTime = $currentTime ?? new Time(); - $this->jwtGenerator = $jwtGenerator ?? new Firebase(); + $this->jwtGenerator = $jwtGenerator ?? new FirebaseAdapter(); } /** @@ -36,6 +36,10 @@ public function generateAccessToken(User $user): string 'exp' => $exp, // expiration time ]; - return $this->jwtGenerator->generate($payload, $config['secretKey'], $config['algorithm']); + return $this->jwtGenerator->generate( + $payload, + $config['secretKey'], + $config['algorithm'] + ); } } diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php similarity index 97% rename from tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php rename to tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index edafa989a..910d534df 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -13,7 +13,7 @@ /** * @internal */ -final class FirebaseTest extends TestCase +final class FirebaseAdapaterTest extends TestCase { public function testDecode() { From 99f71d5800835d15f46ff4e9323707e74fac7c57 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Jun 2022 11:27:28 +0900 Subject: [PATCH 034/142] style: remove line breaks --- src/Authentication/Authenticators/JWT/FirebaseAdapter.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php index 07403ce71..765e0cabb 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -44,11 +44,8 @@ public static function decode(string $encodedToken): stdClass * * @param string $key */ - public static function generate( - array $payload, - $key, - string $algorithm - ): string { + public static function generate(array $payload, $key, string $algorithm): string + { return JWT::encode($payload, $key, $algorithm); } } From 62355e609d0a41fe2d6863024a8424b6c99efaf5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Jun 2022 11:28:36 +0900 Subject: [PATCH 035/142] refactor: rename propery name $loginModel is confusing with LoginModel. --- src/Authentication/Authenticators/JWT.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 0186a4d91..557237ffb 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -34,7 +34,7 @@ class JWT implements AuthenticatorInterface protected ?User $user = null; protected JWTAdapterInterface $jwtDecoder; - protected TokenLoginModel $loginModel; + protected TokenLoginModel $tokenLoginModel; protected ?stdClass $payload = null; public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtDecoder = null) @@ -42,7 +42,7 @@ public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtDecode $this->provider = $provider; $this->jwtDecoder = $jwtDecoder ?? new FirebaseAdapter(); - $this->loginModel = model(TokenLoginModel::class); // @phpstan-ignore-line + $this->tokenLoginModel = model(TokenLoginModel::class); // @phpstan-ignore-line } /** @@ -63,7 +63,7 @@ public function attempt(array $credentials): Result if (! $result->isOK()) { // Always record a login attempt, whether success or not. - $this->loginModel->recordLoginAttempt( + $this->tokenLoginModel->recordLoginAttempt( self::ID_TYPE_JWT, $credentials['token'] ?? '', false, @@ -78,7 +78,7 @@ public function attempt(array $credentials): Result $this->login($user); - $this->loginModel->recordLoginAttempt( + $this->tokenLoginModel->recordLoginAttempt( self::ID_TYPE_JWT, $credentials['token'] ?? '', true, From 79b9be3a8f6761c39b5ad9fdbd529e6fbf054f35 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Jun 2022 11:44:23 +0900 Subject: [PATCH 036/142] refactor: rename property names --- src/Authentication/Authenticators/JWT.php | 8 ++++---- src/Authentication/TokenGenerator/JWTGenerator.php | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 557237ffb..ccab1090b 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -33,14 +33,14 @@ class JWT implements AuthenticatorInterface protected UserModel $provider; protected ?User $user = null; - protected JWTAdapterInterface $jwtDecoder; + protected JWTAdapterInterface $jwtAdapter; protected TokenLoginModel $tokenLoginModel; protected ?stdClass $payload = null; - public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtDecoder = null) + public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtAdapter = null) { $this->provider = $provider; - $this->jwtDecoder = $jwtDecoder ?? new FirebaseAdapter(); + $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); $this->tokenLoginModel = model(TokenLoginModel::class); // @phpstan-ignore-line } @@ -227,7 +227,7 @@ public function recordActiveDate(): void */ public function decodeJWT(string $encodedToken): stdClass { - return $this->jwtDecoder->decode($encodedToken); + return $this->jwtAdapter->decode($encodedToken); } /** diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 3e03c4970..8a122b4f7 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -10,12 +10,12 @@ class JWTGenerator { private Time $currentTime; - private JWTAdapterInterface $jwtGenerator; + private JWTAdapterInterface $jwtAdapter; - public function __construct(?Time $currentTime = null, ?JWTAdapterInterface $jwtGenerator = null) + public function __construct(?Time $currentTime = null, ?JWTAdapterInterface $jwtAdapter = null) { - $this->currentTime = $currentTime ?? new Time(); - $this->jwtGenerator = $jwtGenerator ?? new FirebaseAdapter(); + $this->currentTime = $currentTime ?? new Time(); + $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); } /** @@ -36,7 +36,7 @@ public function generateAccessToken(User $user): string 'exp' => $exp, // expiration time ]; - return $this->jwtGenerator->generate( + return $this->jwtAdapter->generate( $payload, $config['secretKey'], $config['algorithm'] From 55255ccafedfeea1a6b6ea95b890562882588383 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 2 Jun 2022 13:18:48 +0900 Subject: [PATCH 037/142] feat: add JWTGenerator::generate() General purpose JWT generate() method. --- .../TokenGenerator/JWTGenerator.php | 36 ++++++++++++++++ .../TokenGenerator/JWTGeneratorTest.php | 41 ++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 8a122b4f7..bd9766524 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -42,4 +42,40 @@ public function generateAccessToken(User $user): string $config['algorithm'] ); } + + /** + * Issues JWT + * + * @param int|null $ttl Time to live in seconds. + * @param string $key The secret key. + */ + public function generate(array $payload, ?int $ttl = null, $key = null, ?string $algorithm = null): string + { + assert( + (array_key_exists('exp', $payload) && ($ttl !== null)) === false, + 'Cannot pass $payload[\'exp\'] and $ttl at the same time.' + ); + + $config = setting('Auth.jwtConfig'); + $algorithm ??= $config['algorithm']; + $key ??= $config['secretKey']; + + if (! array_key_exists('iat', $payload)) { + $payload['iat'] = $this->currentTime->getTimestamp(); + } + + if (! array_key_exists('exp', $payload)) { + $payload['exp'] = $payload['iat'] + $config['timeToLive']; + } + + if ($ttl !== null) { + $payload['exp'] = $payload['iat'] + $ttl; + } + + return $this->jwtAdapter->generate( + $payload, + $key, + $algorithm + ); + } } diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index 800c6e51d..a46f212cc 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Authentication\TokenGenerator; +use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\JWT; use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; use CodeIgniter\Shield\Entities\User; @@ -13,7 +14,7 @@ */ final class JWTGeneratorTest extends TestCase { - public function testGenerate() + public function testGenerateAccessToken() { /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); @@ -28,7 +29,7 @@ public function testGenerate() } /** - * @depends testGenerate + * @depends testGenerateAccessToken */ public function testTokenSubIsUserId(string $token) { @@ -38,4 +39,40 @@ public function testTokenSubIsUserId(string $token) $this->assertSame('1', $payload->sub); } + + public function testGenerate() + { + $currentTime = new Time('2022-06-01 12:00:00 +00:00'); + $generator = new JWTGenerator($currentTime); + + $payload = [ + 'user_id' => '1', + 'email' => 'admin@example.jp', + ]; + + $token = $generator->generate($payload, 1 * DAY); + + $this->assertIsString($token); + $this->assertStringStartsWith('eyJ', $token); + + return $token; + } + + /** + * @depends testGenerate + */ + public function testTokenHasIatAndExp(string $token) + { + $auth = new JWT(new UserModel()); + + $payload = $auth->decodeJWT($token); + + $expected = [ + 'user_id' => '1', + 'email' => 'admin@example.jp', + 'iat' => 1_654_084_800, + 'exp' => 1_654_171_200, + ]; + $this->assertSame($expected, (array) $payload); + } } From 39c3cd4320411471afdfb1b56180fb10f0379217 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 3 Jun 2022 11:51:41 +0900 Subject: [PATCH 038/142] feat: update logout() return type --- src/Authentication/Authenticators/JWT.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index ccab1090b..4b9a7dc42 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -191,11 +191,9 @@ public function loginById($userId): void /** * Logs the current user out. */ - public function logout(): bool + public function logout(): void { $this->user = null; - - return true; } /** From f23f51f25d42e990d6e66d88caebe5d5511e597f Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 3 Jun 2022 14:00:32 +0900 Subject: [PATCH 039/142] feat: change JWTAdapterInterface::decode() signature --- src/Authentication/Authenticators/JWT.php | 7 ++++++- .../Authenticators/JWT/FirebaseAdapter.php | 9 +++------ .../Authenticators/JWT/JWTAdapterInterface.php | 4 +++- .../Authenticators/JWT/FirebaseAdapaterTest.php | 12 ++++++++++-- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 4b9a7dc42..f4506e2c3 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -225,7 +225,12 @@ public function recordActiveDate(): void */ public function decodeJWT(string $encodedToken): stdClass { - return $this->jwtAdapter->decode($encodedToken); + $config = setting('Auth.jwtConfig'); + + $key = $config['secretKey']; + $algorithm = $config['algorithm']; + + return $this->jwtAdapter->decode($encodedToken, $key, $algorithm); } /** diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php index 765e0cabb..95bd699ad 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -18,15 +18,12 @@ class FirebaseAdapter implements JWTAdapterInterface /** * Decode JWT * + * @param string $key + * * @return stdClass Payload */ - public static function decode(string $encodedToken): stdClass + public static function decode(string $encodedToken, $key, string $algorithm): stdClass { - $config = setting('Auth.jwtConfig'); - - $key = $config['secretKey']; - $algorithm = $config['algorithm']; - try { return JWT::decode($encodedToken, new Key($key, $algorithm)); } catch (BeforeValidException|ExpiredException $e) { diff --git a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php index dab01ff76..a907a0160 100644 --- a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php +++ b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php @@ -16,7 +16,9 @@ public static function generate(array $payload, $key, string $algorithm): string /** * Decode JWT * + * @param string $key The secret key. + * * @return stdClass Payload */ - public static function decode(string $encodedToken): stdClass; + public static function decode(string $encodedToken, $key, string $algorithm): stdClass; } diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index 910d534df..74da5a181 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -48,8 +48,12 @@ public function testDecodeSignatureInvalidException() $jwtDecoder = new FirebaseAdapter(); + $config = setting('Auth.jwtConfig'); + $key = $config['secretKey']; + $algorithm = $config['algorithm']; + $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJc3N1ZXIgb2YgdGhlIEpXVCIsImF1ZCI6IkF1ZGllbmNlIG9mIHRoZSBKV1QiLCJzdWIiOiIxIiwiaWF0IjoxNjUzOTkxOTg5LCJleHAiOjE2NTM5OTU1ODl9.hgOYHEcT6RGHb3po1lspTcmjrylY1Cy1IvYmHOyx0CY'; - $jwtDecoder->decode($token); + $jwtDecoder->decode($token, $key, $algorithm); } public function testDecodeExpiredException() @@ -62,6 +66,10 @@ public function testDecodeExpiredException() $currentTime = new Time('-1 hour'); $token = $this->generateJWT($currentTime); - $jwtDecoder->decode($token); + $config = setting('Auth.jwtConfig'); + $key = $config['secretKey']; + $algorithm = $config['algorithm']; + + $jwtDecoder->decode($token, $key, $algorithm); } } From d0d52edd31ef28e62271b8957e5239e4764fca16 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 3 Jun 2022 14:01:17 +0900 Subject: [PATCH 040/142] fix: broken test --- .../TokenGenerator/JWTGeneratorTest.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index a46f212cc..fdfe7402b 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -42,7 +42,7 @@ public function testTokenSubIsUserId(string $token) public function testGenerate() { - $currentTime = new Time('2022-06-01 12:00:00 +00:00'); + $currentTime = new Time(); $generator = new JWTGenerator($currentTime); $payload = [ @@ -55,14 +55,16 @@ public function testGenerate() $this->assertIsString($token); $this->assertStringStartsWith('eyJ', $token); - return $token; + return [$token, $currentTime]; } /** * @depends testGenerate */ - public function testTokenHasIatAndExp(string $token) + public function testTokenHasIatAndExp(array $data) { + [$token, $currentTime] = $data; + $auth = new JWT(new UserModel()); $payload = $auth->decodeJWT($token); @@ -70,8 +72,8 @@ public function testTokenHasIatAndExp(string $token) $expected = [ 'user_id' => '1', 'email' => 'admin@example.jp', - 'iat' => 1_654_084_800, - 'exp' => 1_654_171_200, + 'iat' => $currentTime->getTimestamp(), + 'exp' => $currentTime->getTimestamp() + 1 * DAY, ]; $this->assertSame($expected, (array) $payload); } From 1bd81caec641b1a4dd41830a890771c77d595a3d Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 3 Jun 2022 14:02:20 +0900 Subject: [PATCH 041/142] feat: change Config\Auth::jwtConfig --- .../TokenGenerator/JWTGenerator.php | 32 +++++++++++-------- src/Config/Auth.php | 11 +++++-- .../Authenticators/JWTAuthenticatorTest.php | 6 ++-- .../JWT/FirebaseAdapaterTest.php | 10 ++++-- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index bd9766524..d826d93a2 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -28,13 +28,14 @@ public function generateAccessToken(User $user): string $iat = $this->currentTime->getTimestamp(); $exp = $iat + $config['timeToLive']; - $payload = [ - 'iss' => $config['issuer'], // issuer - 'aud' => $config['audience'], // audience - 'sub' => (string) $user->id, // subject - 'iat' => $iat, // issued at - 'exp' => $exp, // expiration time - ]; + $payload = array_merge( + $config['claims'], + [ + 'sub' => (string) $user->id, // subject + 'iat' => $iat, // issued at + 'exp' => $exp, // expiration time + ] + ); return $this->jwtAdapter->generate( $payload, @@ -46,25 +47,28 @@ public function generateAccessToken(User $user): string /** * Issues JWT * - * @param int|null $ttl Time to live in seconds. - * @param string $key The secret key. + * @param array $claims The payload items. + * @param int|null $ttl Time to live in seconds. + * @param string $key The secret key. */ - public function generate(array $payload, ?int $ttl = null, $key = null, ?string $algorithm = null): string + public function generate(array $claims, ?int $ttl = null, $key = null, ?string $algorithm = null): string { assert( - (array_key_exists('exp', $payload) && ($ttl !== null)) === false, - 'Cannot pass $payload[\'exp\'] and $ttl at the same time.' + (array_key_exists('exp', $claims) && ($ttl !== null)) === false, + 'Cannot pass $claims[\'exp\'] and $ttl at the same time.' ); $config = setting('Auth.jwtConfig'); $algorithm ??= $config['algorithm']; $key ??= $config['secretKey']; - if (! array_key_exists('iat', $payload)) { + $payload = $claims; + + if (! array_key_exists('iat', $claims)) { $payload['iat'] = $this->currentTime->getTimestamp(); } - if (! array_key_exists('exp', $payload)) { + if (! array_key_exists('exp', $claims)) { $payload['exp'] = $payload['iat'] + $config['timeToLive']; } diff --git a/src/Config/Auth.php b/src/Config/Auth.php index f9a6c9a35..7b7b309e0 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -242,16 +242,21 @@ class Auth extends BaseConfig * These settings only apply if you are using the JWT Authenticator * for authentication. * + * These are the default values when you generate and validate JWT + * + * - claims The payload items that all JWT have. * - secretKey The secret key. Needs more than 256 bits random string. * E.g., $ php -r 'echo base64_encode(random_bytes(32));' * - algorithm JWT Signing Algorithms. * - timeToLive Specifies the amount of time, in seconds, that a token is valid. * - * @var array + * @var array|bool|int|string> */ public array $jwtConfig = [ - 'issuer' => '', - 'audience' => '', + 'claims' => [ + 'iss' => '', + 'aud' => '', + ], 'secretKey' => '', 'algorithm' => 'HS256', 'timeToLive' => 1 * HOUR, diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index c755b630a..f1babacd9 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -114,8 +114,8 @@ public function testCheckNoSubToken() { $config = setting('Auth.jwtConfig'); $payload = [ - 'iss' => $config['issuer'], // issuer - 'aud' => $config['audience'], // audience + 'iss' => $config['claims']['iss'], // issuer + 'aud' => $config['claims']['aud'], // audience ]; $token = FirebaseJWT::encode($payload, $config['secretKey'], $config['algorithm']); @@ -168,7 +168,7 @@ public function testGetPayload() $payload = $this->auth->getPayload(); $this->assertSame((string) $this->user->id, $payload->sub); - $this->assertSame((\setting('Auth.jwtConfig')['issuer']), $payload->iss); + $this->assertSame((\setting('Auth.jwtConfig')['claims']['iss']), $payload->iss); } public function testAttemptBadSignatureToken() diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index 74da5a181..cfb863a46 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -21,10 +21,14 @@ public function testDecode() $jwtDecoder = new FirebaseAdapter(); - $payload = $jwtDecoder->decode($token); + $config = setting('Auth.jwtConfig'); + $key = $config['secretKey']; + $algorithm = $config['algorithm']; + + $payload = $jwtDecoder->decode($token, $key, $algorithm); - $this->assertSame(setting('Auth.jwtConfig')['issuer'], $payload->iss); - $this->assertSame(setting('Auth.jwtConfig')['audience'], $payload->aud); + $this->assertSame(setting('Auth.jwtConfig')['claims']['iss'], $payload->iss); + $this->assertSame(setting('Auth.jwtConfig')['claims']['aud'], $payload->aud); $this->assertSame('1', $payload->sub); } From 1dd31e4d997c0a7691cc550f85f5eacc64d891c1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 31 Aug 2022 15:15:23 +0900 Subject: [PATCH 042/142] docs: remove @phpstan-ignore-line --- src/Authentication/Authenticators/JWT.php | 2 +- tests/Authentication/Authenticators/JWTAuthenticatorTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index f4506e2c3..8146584b2 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -42,7 +42,7 @@ public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtAdapte $this->provider = $provider; $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); - $this->tokenLoginModel = model(TokenLoginModel::class); // @phpstan-ignore-line + $this->tokenLoginModel = model(TokenLoginModel::class); } /** diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index f1babacd9..82c1a49f8 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -33,7 +33,7 @@ protected function setUp(): void $config = new Auth(); $auth = new Authentication($config); - $auth->setProvider(\model(UserModel::class)); // @phpstan-ignore-line + $auth->setProvider(\model(UserModel::class)); /** @var JWT $authenticator */ $authenticator = $auth->factory('jwt'); From dab58b691f40d942cc48bcbc5295eb32a1e93977 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 31 Aug 2022 15:15:59 +0900 Subject: [PATCH 043/142] fix: add logic to check if $request is IncomingRequest --- src/Filters/JWTAuth.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Filters/JWTAuth.php b/src/Filters/JWTAuth.php index a24ac7f6b..fbe1beaae 100644 --- a/src/Filters/JWTAuth.php +++ b/src/Filters/JWTAuth.php @@ -3,6 +3,7 @@ namespace CodeIgniter\Shield\Filters; use CodeIgniter\Filters\FilterInterface; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; @@ -32,6 +33,10 @@ class JWTAuth implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { + if (! $request instanceof IncomingRequest) { + return; + } + helper(['auth', 'setting']); /** @var JWT $authenticator */ @@ -56,6 +61,8 @@ public function before(RequestInterface $request, $arguments = null) private function getTokenFromHeader(RequestInterface $request): string { + assert($request instanceof IncomingRequest); + $tokenHeader = $request->getHeaderLine( setting('Auth.authenticatorHeader')['jwt'] ?? 'Authorization' ); From ded0ae7d12386b73c628839c5ceb304f7e6c2ce8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 31 Aug 2022 15:20:28 +0900 Subject: [PATCH 044/142] refactor: composer cs-fix --- src/Authentication/Authenticators/JWT.php | 2 ++ .../Authenticators/JWT/FirebaseAdapter.php | 2 ++ .../JWT/JWTAdapterInterface.php | 2 ++ .../TokenGenerator/JWTGenerator.php | 2 ++ src/Filters/JWTAuth.php | 6 ++-- .../Authenticators/JWTAuthenticatorTest.php | 30 ++++++++++--------- .../Authentication/Filters/JWTFilterTest.php | 12 ++++---- .../JWT/FirebaseAdapaterTest.php | 8 +++-- .../TokenGenerator/JWTGeneratorTest.php | 6 ++-- 9 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 8146584b2..1eebde06f 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -1,5 +1,7 @@ createUser(); @@ -58,7 +60,7 @@ public function testLogin() $this->assertSame($user->id, $this->auth->getUser()->id); } - public function testLogout() + public function testLogout(): void { // this one's a little odd since it's stateless, but roll with it... @@ -71,7 +73,7 @@ public function testLogout() $this->assertNull($this->auth->getUser()); } - public function testLoginById() + public function testLoginById(): void { $user = $this->createUser(); @@ -82,7 +84,7 @@ public function testLoginById() $this->assertTrue($this->auth->loggedIn()); } - public function testLoginByIdNoUser() + public function testLoginByIdNoUser(): void { $this->expectException(AuthenticationException::class); $this->expectExceptionMessage('Unable to locate the specified user.'); @@ -94,7 +96,7 @@ public function testLoginByIdNoUser() $this->auth->loginById(9999); } - public function testCheckNoToken() + public function testCheckNoToken(): void { $result = $this->auth->check([]); @@ -102,7 +104,7 @@ public function testCheckNoToken() $this->assertSame(\lang('Auth.noToken'), $result->reason()); } - public function testCheckBadSignatureToken() + public function testCheckBadSignatureToken(): void { $result = $this->auth->check(['token' => self::BAD_JWT]); @@ -110,7 +112,7 @@ public function testCheckBadSignatureToken() $this->assertSame('Invalid JWT: Signature verification failed', $result->reason()); } - public function testCheckNoSubToken() + public function testCheckNoSubToken(): void { $config = setting('Auth.jwtConfig'); $payload = [ @@ -125,7 +127,7 @@ public function testCheckNoSubToken() $this->assertSame('Invalid JWT: no user_id', $result->reason()); } - public function testCheckOldToken() + public function testCheckOldToken(): void { $currentTime = new Time('-1 hour'); $token = $this->generateJWT($currentTime); @@ -136,7 +138,7 @@ public function testCheckOldToken() $this->assertSame('Expired JWT: Expired token', $result->reason()); } - public function testCheckNoUserInDatabase() + public function testCheckNoUserInDatabase(): void { $token = $this->generateJWT(); @@ -149,7 +151,7 @@ public function testCheckNoUserInDatabase() $this->assertSame(\lang('Auth.invalidUser'), $result->reason()); } - public function testCheckSuccess() + public function testCheckSuccess(): void { $token = $this->generateJWT(); @@ -160,7 +162,7 @@ public function testCheckSuccess() $this->assertSame(1, $result->extraInfo()->id); } - public function testGetPayload() + public function testGetPayload(): void { $token = $this->generateJWT(); @@ -171,7 +173,7 @@ public function testGetPayload() $this->assertSame((\setting('Auth.jwtConfig')['claims']['iss']), $payload->iss); } - public function testAttemptBadSignatureToken() + public function testAttemptBadSignatureToken(): void { $result = $this->auth->attempt([ 'token' => self::BAD_JWT, @@ -189,7 +191,7 @@ public function testAttemptBadSignatureToken() ]); } - public function testAttemptSuccess() + public function testAttemptSuccess(): void { $token = $this->generateJWT(); @@ -213,7 +215,7 @@ public function testAttemptSuccess() ]); } - public function testRecordActiveDateNoUser() + public function testRecordActiveDateNoUser(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( diff --git a/tests/Authentication/Filters/JWTFilterTest.php b/tests/Authentication/Filters/JWTFilterTest.php index 5ed72894a..efdf228f5 100644 --- a/tests/Authentication/Filters/JWTFilterTest.php +++ b/tests/Authentication/Filters/JWTFilterTest.php @@ -1,5 +1,7 @@ group('/', ['filter' => 'jwtAuth'], static function ($routes) { - $routes->get('protected-route', static function () { + $routes->group('/', ['filter' => 'jwtAuth'], static function ($routes): void { + $routes->get('protected-route', static function (): void { echo 'Protected'; }); }); - $routes->get('open-route', static function () { + $routes->get('open-route', static function (): void { echo 'Open'; }); $routes->get('login', 'AuthController::login', ['as' => 'login']); Services::injectMock('routes', $routes); } - public function testFilterNotAuthorized() + public function testFilterNotAuthorized(): void { $result = $this->call('get', 'protected-route'); @@ -59,7 +61,7 @@ public function testFilterNotAuthorized() $result->assertSee('Open'); } - public function testFilterSuccess() + public function testFilterSuccess(): void { /** @var User $user */ $user = \fake(UserModel::class); diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index cfb863a46..02605c78a 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -1,5 +1,7 @@ generateJWT(); @@ -45,7 +47,7 @@ public static function generateJWT(?Time $currentTime = null): string return $generator->generateAccessToken($user); } - public function testDecodeSignatureInvalidException() + public function testDecodeSignatureInvalidException(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Invalid JWT: Signature verification failed'); @@ -60,7 +62,7 @@ public function testDecodeSignatureInvalidException() $jwtDecoder->decode($token, $key, $algorithm); } - public function testDecodeExpiredException() + public function testDecodeExpiredException(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Expired JWT: Expired token'); diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index fdfe7402b..c0a3ace6c 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -1,5 +1,7 @@ Date: Wed, 31 Aug 2022 15:21:31 +0900 Subject: [PATCH 045/142] fix: $userAgent type --- src/Authentication/Authenticators/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 1eebde06f..dc129632a 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -59,7 +59,7 @@ public function attempt(array $credentials): Result $request = service('request'); $ipAddress = $request->getIPAddress(); - $userAgent = $request->getUserAgent(); + $userAgent = (string) $request->getUserAgent(); $result = $this->check($credentials); From 965df733e92354b4cdb3c98078f875341cc313ad Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 31 Aug 2022 15:28:11 +0900 Subject: [PATCH 046/142] refactor: run rector --- src/Config/Auth.php | 2 +- tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 7b7b309e0..4e1b31f69 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -259,7 +259,7 @@ class Auth extends BaseConfig ], 'secretKey' => '', 'algorithm' => 'HS256', - 'timeToLive' => 1 * HOUR, + 'timeToLive' => HOUR, ]; /** diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index c0a3ace6c..faa816154 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -52,7 +52,7 @@ public function testGenerate() 'email' => 'admin@example.jp', ]; - $token = $generator->generate($payload, 1 * DAY); + $token = $generator->generate($payload, DAY); $this->assertIsString($token); $this->assertStringStartsWith('eyJ', $token); @@ -75,7 +75,7 @@ public function testTokenHasIatAndExp(array $data): void 'user_id' => '1', 'email' => 'admin@example.jp', 'iat' => $currentTime->getTimestamp(), - 'exp' => $currentTime->getTimestamp() + 1 * DAY, + 'exp' => $currentTime->getTimestamp() + DAY, ]; $this->assertSame($expected, (array) $payload); } From 02a17bff90f164cdb97dbcffa278fc3a593e365d Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Oct 2022 16:24:36 +0900 Subject: [PATCH 047/142] refactor: create AuthJWT config file --- src/Authentication/Authenticators/JWT.php | 2 +- .../TokenGenerator/JWTGenerator.php | 4 +-- src/Config/Auth.php | 27 -------------- src/Config/AuthJWT.php | 35 +++++++++++++++++++ .../Authenticators/JWTAuthenticatorTest.php | 4 +-- .../JWT/FirebaseAdapaterTest.php | 10 +++--- 6 files changed, 45 insertions(+), 37 deletions(-) create mode 100644 src/Config/AuthJWT.php diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index dc129632a..21289e584 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -227,7 +227,7 @@ public function recordActiveDate(): void */ public function decodeJWT(string $encodedToken): stdClass { - $config = setting('Auth.jwtConfig'); + $config = setting('AuthJWT.config'); $key = $config['secretKey']; $algorithm = $config['algorithm']; diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 1c4f7941a..b0a7e256c 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -25,7 +25,7 @@ public function __construct(?Time $currentTime = null, ?JWTAdapterInterface $jwt */ public function generateAccessToken(User $user): string { - $config = setting('Auth.jwtConfig'); + $config = setting('AuthJWT.config'); $iat = $this->currentTime->getTimestamp(); $exp = $iat + $config['timeToLive']; @@ -60,7 +60,7 @@ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $ 'Cannot pass $claims[\'exp\'] and $ttl at the same time.' ); - $config = setting('Auth.jwtConfig'); + $config = setting('AuthJWT.config'); $algorithm ??= $config['algorithm']; $key ??= $config['secretKey']; diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 4e1b31f69..3e87b13a7 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -235,33 +235,6 @@ class Auth extends BaseConfig 'rememberLength' => 30 * DAY, ]; - /** - * -------------------------------------------------------------------- - * JWT Authenticator Configuration - * -------------------------------------------------------------------- - * These settings only apply if you are using the JWT Authenticator - * for authentication. - * - * These are the default values when you generate and validate JWT - * - * - claims The payload items that all JWT have. - * - secretKey The secret key. Needs more than 256 bits random string. - * E.g., $ php -r 'echo base64_encode(random_bytes(32));' - * - algorithm JWT Signing Algorithms. - * - timeToLive Specifies the amount of time, in seconds, that a token is valid. - * - * @var array|bool|int|string> - */ - public array $jwtConfig = [ - 'claims' => [ - 'iss' => '', - 'aud' => '', - ], - 'secretKey' => '', - 'algorithm' => 'HS256', - 'timeToLive' => HOUR, - ]; - /** * -------------------------------------------------------------------- * Minimum Password Length diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php new file mode 100644 index 000000000..a631083d8 --- /dev/null +++ b/src/Config/AuthJWT.php @@ -0,0 +1,35 @@ +|bool|int|string> + */ + public array $config = [ + 'claims' => [ + 'iss' => '', + 'aud' => '', + ], + 'secretKey' => '', + 'algorithm' => 'HS256', + 'timeToLive' => HOUR, + ]; +} diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 01645a684..ab3645bf2 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -114,7 +114,7 @@ public function testCheckBadSignatureToken(): void public function testCheckNoSubToken(): void { - $config = setting('Auth.jwtConfig'); + $config = setting('AuthJWT.config'); $payload = [ 'iss' => $config['claims']['iss'], // issuer 'aud' => $config['claims']['aud'], // audience @@ -170,7 +170,7 @@ public function testGetPayload(): void $payload = $this->auth->getPayload(); $this->assertSame((string) $this->user->id, $payload->sub); - $this->assertSame((\setting('Auth.jwtConfig')['claims']['iss']), $payload->iss); + $this->assertSame((\setting('AuthJWT.config')['claims']['iss']), $payload->iss); } public function testAttemptBadSignatureToken(): void diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index 02605c78a..ab169b0b7 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -23,14 +23,14 @@ public function testDecode(): void $jwtDecoder = new FirebaseAdapter(); - $config = setting('Auth.jwtConfig'); + $config = setting('AuthJWT.config'); $key = $config['secretKey']; $algorithm = $config['algorithm']; $payload = $jwtDecoder->decode($token, $key, $algorithm); - $this->assertSame(setting('Auth.jwtConfig')['claims']['iss'], $payload->iss); - $this->assertSame(setting('Auth.jwtConfig')['claims']['aud'], $payload->aud); + $this->assertSame(setting('AuthJWT.config')['claims']['iss'], $payload->iss); + $this->assertSame(setting('AuthJWT.config')['claims']['aud'], $payload->aud); $this->assertSame('1', $payload->sub); } @@ -54,7 +54,7 @@ public function testDecodeSignatureInvalidException(): void $jwtDecoder = new FirebaseAdapter(); - $config = setting('Auth.jwtConfig'); + $config = setting('AuthJWT.config'); $key = $config['secretKey']; $algorithm = $config['algorithm']; @@ -72,7 +72,7 @@ public function testDecodeExpiredException(): void $currentTime = new Time('-1 hour'); $token = $this->generateJWT($currentTime); - $config = setting('Auth.jwtConfig'); + $config = setting('AuthJWT.config'); $key = $config['secretKey']; $algorithm = $config['algorithm']; From 7a8278c6206334409d395139b32923d7c63340b8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Oct 2022 16:53:02 +0900 Subject: [PATCH 048/142] refactor: change config array to class properties --- src/Authentication/Authenticators/JWT.php | 8 ++-- .../TokenGenerator/JWTGenerator.php | 21 +++++----- src/Config/AuthJWT.php | 38 +++++++++++-------- .../Authenticators/JWTAuthenticatorTest.php | 14 ++++--- .../JWT/FirebaseAdapaterTest.php | 27 +++++++------ 5 files changed, 64 insertions(+), 44 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 21289e584..d3c348a1e 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -10,6 +10,7 @@ use CodeIgniter\Shield\Authentication\AuthenticatorInterface; use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; use CodeIgniter\Shield\Authentication\Authenticators\JWT\JWTAdapterInterface; +use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; use CodeIgniter\Shield\Models\TokenLoginModel; @@ -227,10 +228,11 @@ public function recordActiveDate(): void */ public function decodeJWT(string $encodedToken): stdClass { - $config = setting('AuthJWT.config'); + /** @var AuthJWT $config */ + $config = config('AuthJWT'); - $key = $config['secretKey']; - $algorithm = $config['algorithm']; + $key = $config->secretKey; + $algorithm = $config->algorithm; return $this->jwtAdapter->decode($encodedToken, $key, $algorithm); } diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index b0a7e256c..b0e65a49d 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -7,6 +7,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; use CodeIgniter\Shield\Authentication\Authenticators\JWT\JWTAdapterInterface; +use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; class JWTGenerator @@ -25,13 +26,14 @@ public function __construct(?Time $currentTime = null, ?JWTAdapterInterface $jwt */ public function generateAccessToken(User $user): string { - $config = setting('AuthJWT.config'); + /** @var AuthJWT $config */ + $config = config('AuthJWT'); $iat = $this->currentTime->getTimestamp(); - $exp = $iat + $config['timeToLive']; + $exp = $iat + $config->timeToLive; $payload = array_merge( - $config['claims'], + $config->claims, [ 'sub' => (string) $user->id, // subject 'iat' => $iat, // issued at @@ -41,8 +43,8 @@ public function generateAccessToken(User $user): string return $this->jwtAdapter->generate( $payload, - $config['secretKey'], - $config['algorithm'] + $config->secretKey, + $config->algorithm ); } @@ -60,9 +62,10 @@ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $ 'Cannot pass $claims[\'exp\'] and $ttl at the same time.' ); - $config = setting('AuthJWT.config'); - $algorithm ??= $config['algorithm']; - $key ??= $config['secretKey']; + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + $algorithm ??= $config->algorithm; + $key ??= $config->secretKey; $payload = $claims; @@ -71,7 +74,7 @@ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $ } if (! array_key_exists('exp', $claims)) { - $payload['exp'] = $payload['iat'] + $config['timeToLive']; + $payload['exp'] = $payload['iat'] + $config->timeToLive; } if ($ttl !== null) { diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index a631083d8..31dbfc74c 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -13,23 +13,29 @@ class AuthJWT extends BaseConfig { /** - * These are the default values when you generate and validate JWT + * The payload items that all JWT have. * - * - claims The payload items that all JWT have. - * - secretKey The secret key. Needs more than 256 bits random string. - * E.g., $ php -r 'echo base64_encode(random_bytes(32));' - * - algorithm JWT Signing Algorithms. - * - timeToLive Specifies the amount of time, in seconds, that a token is valid. - * - * @var array|bool|int|string> + * @var string[] + * @phpstan-var array */ - public array $config = [ - 'claims' => [ - 'iss' => '', - 'aud' => '', - ], - 'secretKey' => '', - 'algorithm' => 'HS256', - 'timeToLive' => HOUR, + public array $claims = [ + 'iss' => '', + 'aud' => '', ]; + + /** + * The secret key. Needs more than 256 bits random string. + * E.g., $ php -r 'echo base64_encode(random_bytes(32));' + */ + public string $secretKey = ''; + + /** + * JWT Signing Algorithms. + */ + public string $algorithm = 'HS256'; + + /** + * Specifies the amount of time, in seconds, that a token is valid. + */ + public int $timeToLive = HOUR; } diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index ab3645bf2..f4232c584 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -10,6 +10,7 @@ use CodeIgniter\Shield\Authentication\Authenticators\JWT; use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; use CodeIgniter\Shield\Config\Auth; +use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Models\UserModel; use CodeIgniter\Shield\Result; @@ -114,12 +115,13 @@ public function testCheckBadSignatureToken(): void public function testCheckNoSubToken(): void { - $config = setting('AuthJWT.config'); + /** @var AuthJWT $config */ + $config = config('AuthJWT'); $payload = [ - 'iss' => $config['claims']['iss'], // issuer - 'aud' => $config['claims']['aud'], // audience + 'iss' => $config->claims['iss'], // issuer + 'aud' => $config->claims['aud'], // audience ]; - $token = FirebaseJWT::encode($payload, $config['secretKey'], $config['algorithm']); + $token = FirebaseJWT::encode($payload, $config->secretKey, $config->algorithm); $result = $this->auth->check(['token' => $token]); @@ -170,7 +172,9 @@ public function testGetPayload(): void $payload = $this->auth->getPayload(); $this->assertSame((string) $this->user->id, $payload->sub); - $this->assertSame((\setting('AuthJWT.config')['claims']['iss']), $payload->iss); + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + $this->assertSame($config->claims['iss'], $payload->iss); } public function testAttemptBadSignatureToken(): void diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index ab169b0b7..3343ef6cd 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -7,6 +7,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; +use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; use CodeIgniter\Shield\Models\UserModel; @@ -23,14 +24,16 @@ public function testDecode(): void $jwtDecoder = new FirebaseAdapter(); - $config = setting('AuthJWT.config'); - $key = $config['secretKey']; - $algorithm = $config['algorithm']; + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + + $key = $config->secretKey; + $algorithm = $config->algorithm; $payload = $jwtDecoder->decode($token, $key, $algorithm); - $this->assertSame(setting('AuthJWT.config')['claims']['iss'], $payload->iss); - $this->assertSame(setting('AuthJWT.config')['claims']['aud'], $payload->aud); + $this->assertSame($config->claims['iss'], $payload->iss); + $this->assertSame($config->claims['aud'], $payload->aud); $this->assertSame('1', $payload->sub); } @@ -54,9 +57,10 @@ public function testDecodeSignatureInvalidException(): void $jwtDecoder = new FirebaseAdapter(); - $config = setting('AuthJWT.config'); - $key = $config['secretKey']; - $algorithm = $config['algorithm']; + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + $key = $config->secretKey; + $algorithm = $config->algorithm; $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJc3N1ZXIgb2YgdGhlIEpXVCIsImF1ZCI6IkF1ZGllbmNlIG9mIHRoZSBKV1QiLCJzdWIiOiIxIiwiaWF0IjoxNjUzOTkxOTg5LCJleHAiOjE2NTM5OTU1ODl9.hgOYHEcT6RGHb3po1lspTcmjrylY1Cy1IvYmHOyx0CY'; $jwtDecoder->decode($token, $key, $algorithm); @@ -72,9 +76,10 @@ public function testDecodeExpiredException(): void $currentTime = new Time('-1 hour'); $token = $this->generateJWT($currentTime); - $config = setting('AuthJWT.config'); - $key = $config['secretKey']; - $algorithm = $config['algorithm']; + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + $key = $config->secretKey; + $algorithm = $config->algorithm; $jwtDecoder->decode($token, $key, $algorithm); } From 61228d6a360758053a2bff742372898ecb86d6a6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Oct 2022 17:04:56 +0900 Subject: [PATCH 049/142] refactor: remove unneeded auth helper loading --- src/Filters/JWTAuth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Filters/JWTAuth.php b/src/Filters/JWTAuth.php index 8fad15097..f94055d88 100644 --- a/src/Filters/JWTAuth.php +++ b/src/Filters/JWTAuth.php @@ -39,7 +39,7 @@ public function before(RequestInterface $request, $arguments = null) return; } - helper(['auth', 'setting']); + helper('setting'); /** @var JWT $authenticator */ $authenticator = auth('jwt')->getAuthenticator(); From adf04e3a9df8e668739a6737366e8a0021093cd0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Oct 2022 17:27:49 +0900 Subject: [PATCH 050/142] feat: you can configure whether to record login attempts The default is record only failure login attempts. --- src/Authentication/Authenticators/JWT.php | 41 +++++++++++-------- src/Config/Auth.php | 4 ++ src/Config/AuthJWT.php | 11 ++++- .../Authenticators/JWTAuthenticatorTest.php | 5 +++ 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index d3c348a1e..cbb9182d7 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -10,6 +10,7 @@ use CodeIgniter\Shield\Authentication\AuthenticatorInterface; use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; use CodeIgniter\Shield\Authentication\Authenticators\JWT\JWTAdapterInterface; +use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; @@ -56,6 +57,9 @@ public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtAdapte */ public function attempt(array $credentials): Result { + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + /** @var IncomingRequest $request */ $request = service('request'); @@ -65,14 +69,16 @@ public function attempt(array $credentials): Result $result = $this->check($credentials); if (! $result->isOK()) { - // Always record a login attempt, whether success or not. - $this->tokenLoginModel->recordLoginAttempt( - self::ID_TYPE_JWT, - $credentials['token'] ?? '', - false, - $ipAddress, - $userAgent - ); + if ($config->recordLoginAttempt >= Auth::RECORD_LOGIN_ATTEMPT_FAILURE) { + // Record a failed login attempt. + $this->tokenLoginModel->recordLoginAttempt( + self::ID_TYPE_JWT, + $credentials['token'] ?? '', + false, + $ipAddress, + $userAgent + ); + } return $result; } @@ -81,14 +87,17 @@ public function attempt(array $credentials): Result $this->login($user); - $this->tokenLoginModel->recordLoginAttempt( - self::ID_TYPE_JWT, - $credentials['token'] ?? '', - true, - $ipAddress, - $userAgent, - $this->user->id - ); + if ($config->recordLoginAttempt === Auth::RECORD_LOGIN_ATTEMPT_ALL) { + // Record a successful login attempt. + $this->tokenLoginModel->recordLoginAttempt( + self::ID_TYPE_JWT, + $credentials['token'] ?? '', + true, + $ipAddress, + $userAgent, + $this->user->id + ); + } return $result; } diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 3e87b13a7..ebde10252 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -19,6 +19,10 @@ class Auth extends BaseConfig { + public const RECORD_LOGIN_ATTEMPT_NONE = 0; // Do not record at all + public const RECORD_LOGIN_ATTEMPT_FAILURE = 1; // Record only failures + public const RECORD_LOGIN_ATTEMPT_ALL = 2; // Record all login attempts + /** * //////////////////////////////////////////////////////////////////// * AUTHENTICATION diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index 31dbfc74c..5397847c4 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -5,7 +5,6 @@ namespace CodeIgniter\Shield\Config; use CodeIgniter\Config\BaseConfig; -use CodeIgniter\Shield\Authentication\Authenticators\JWT; /** * JWT Authenticator Configuration @@ -38,4 +37,14 @@ class AuthJWT extends BaseConfig * Specifies the amount of time, in seconds, that a token is valid. */ public int $timeToLive = HOUR; + + /** + * Whether login attempts are recorded in the database. + * + * Valid values are: + * - Auth::RECORD_LOGIN_ATTEMPT_NONE + * - Auth::RECORD_LOGIN_ATTEMPT_FAILURE + * - Auth::RECORD_LOGIN_ATTEMPT_ALL + */ + public int $recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_FAILURE; } diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index f4232c584..15415aeb8 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -197,6 +197,11 @@ public function testAttemptBadSignatureToken(): void public function testAttemptSuccess(): void { + // Change $recordLoginAttempt in Config. + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + $config->recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_ALL; + $token = $this->generateJWT(); $result = $this->auth->attempt([ From 9c3346cd8c5e1ba600e2948347d1bfab57def5fc Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Oct 2022 17:48:14 +0900 Subject: [PATCH 051/142] refactor: move JWT authenticatorHeader setting to Config\AuthJWT --- src/Authentication/Authenticators/JWT.php | 5 ++++- src/Config/Auth.php | 1 - src/Config/AuthJWT.php | 10 ++++++++++ src/Filters/JWTAuth.php | 6 +++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index cbb9182d7..b5f507dcd 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -169,8 +169,11 @@ public function loggedIn(): bool /** @var IncomingRequest $request */ $request = service('request'); + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + return $this->attempt([ - 'token' => $request->getHeaderLine(config('Auth')->authenticatorHeader['jwt']), + 'token' => $request->getHeaderLine($config->authenticatorHeader), ])->isOK(); } diff --git a/src/Config/Auth.php b/src/Config/Auth.php index ebde10252..a90360449 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -140,7 +140,6 @@ class Auth extends BaseConfig */ public array $authenticatorHeader = [ 'tokens' => 'Authorization', - 'jwt' => 'Authorization', ]; /** diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index 5397847c4..96c70444f 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -11,6 +11,16 @@ */ class AuthJWT extends BaseConfig { + /** + * -------------------------------------------------------------------- + * Name of Authenticator Header + * -------------------------------------------------------------------- + * The name of Header that the Authorization token should be found. + * According to the specs, this should be `Authorization`, but rare + * circumstances might need a different header. + */ + public string $authenticatorHeader = 'Authorization'; + /** * The payload items that all JWT have. * diff --git a/src/Filters/JWTAuth.php b/src/Filters/JWTAuth.php index f94055d88..9ce90c588 100644 --- a/src/Filters/JWTAuth.php +++ b/src/Filters/JWTAuth.php @@ -10,6 +10,7 @@ use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Shield\Authentication\Authenticators\JWT; +use CodeIgniter\Shield\Config\AuthJWT; use Config\Services; /** @@ -65,8 +66,11 @@ private function getTokenFromHeader(RequestInterface $request): string { assert($request instanceof IncomingRequest); + /** @var AuthJWT $config */ + $config = config('AuthJWT'); + $tokenHeader = $request->getHeaderLine( - setting('Auth.authenticatorHeader')['jwt'] ?? 'Authorization' + $config->authenticatorHeader ?? 'Authorization' ); if (strpos($tokenHeader, 'Bearer') === 0) { From a98936ea174df25fcc40ca96e6732516df9e5edf Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Apr 2023 08:33:33 +0900 Subject: [PATCH 052/142] chore: update php-jwt to ^6.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 415bc738e..a0ee054ed 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "codeigniter4/framework": "^4.2.7", "mikey179/vfsstream": "^1.6.7", "mockery/mockery": "^1.0", - "firebase/php-jwt": "^6.2" + "firebase/php-jwt": "^6.4" }, "provide": { "codeigniter4/authentication-implementation": "1.0" From 58f5e79bcdb6b01ed7d5977a0fce5d62b1643901 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Apr 2023 08:39:56 +0900 Subject: [PATCH 053/142] docs: fix @return type --- src/Filters/JWTAuth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Filters/JWTAuth.php b/src/Filters/JWTAuth.php index 9ce90c588..24da9732d 100644 --- a/src/Filters/JWTAuth.php +++ b/src/Filters/JWTAuth.php @@ -32,7 +32,7 @@ class JWTAuth implements FilterInterface * * @param array|null $arguments * - * @return Response|void + * @return ResponseInterface|void */ public function before(RequestInterface $request, $arguments = null) { From 12575329d684ab709421563178070e4074601dc2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Apr 2023 11:00:28 +0900 Subject: [PATCH 054/142] config: remove "aud" in $claims "aud" is optional, and may not be a common value. https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3 --- src/Config/AuthJWT.php | 1 - tests/Authentication/Authenticators/JWTAuthenticatorTest.php | 1 - .../Authentication/Authenticators/JWT/FirebaseAdapaterTest.php | 1 - 3 files changed, 3 deletions(-) diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index 96c70444f..be93783cf 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -29,7 +29,6 @@ class AuthJWT extends BaseConfig */ public array $claims = [ 'iss' => '', - 'aud' => '', ]; /** diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 15415aeb8..ed3eb4a10 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -119,7 +119,6 @@ public function testCheckNoSubToken(): void $config = config('AuthJWT'); $payload = [ 'iss' => $config->claims['iss'], // issuer - 'aud' => $config->claims['aud'], // audience ]; $token = FirebaseJWT::encode($payload, $config->secretKey, $config->algorithm); diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index 3343ef6cd..a1cd317da 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -33,7 +33,6 @@ public function testDecode(): void $payload = $jwtDecoder->decode($token, $key, $algorithm); $this->assertSame($config->claims['iss'], $payload->iss); - $this->assertSame($config->claims['aud'], $payload->aud); $this->assertSame('1', $payload->sub); } From 5e3eb43f82fda236085d8a457e7a7d8fcf407b0e Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Apr 2023 12:13:46 +0900 Subject: [PATCH 055/142] docs: add docs --- docs/addons/jwt.md | 123 +++++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 5 ++ docs/install.md | 15 +++--- mkdocs.yml | 2 + 4 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 docs/addons/jwt.md diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md new file mode 100644 index 000000000..1f83fe39c --- /dev/null +++ b/docs/addons/jwt.md @@ -0,0 +1,123 @@ +# JWT Authentication + +To use JWT Authentication, you need additional setup and configuration. + +## Setup + +### Manual Setup + +1. Install "firebase/php-jwt" via Composer. + + ```console + composer require firebase/php-jwt:^6.4 + ``` + +2. Copy the **AuthJWT.php** from **vendor/codeigniter4/shield/src/Config/** into your project's config folder and update the namespace to `Config`. You will also need to have these classes extend the original classes. See the example below. + + ```php + // new file - app/Config/AuthJWT.php + 'https://codeigniter.com/', + ]; +``` + +This value is used by the `JWTGenerator::generateAccessToken()` method. + +### Set Secret Key + +Set yout secret key to the `$secretKey` property, or set it in your `.env` file. + +E.g.: +```dotenv +authjwt.secretKey = 8XBFsF6HThIa7OV/bSynahEch+WbKrGcuiJVYPiwqPE= +``` + +It needs more than 256 bits random string. You can get a secure random string +with the following command: + +```console +php -r 'echo base64_encode(random_bytes(32));' +``` + +## Generating JWTs + +### JWT to a Specific User + +JWTs are created through the `JWTGenerator::generateAccessToken()` method. +This takes a User object to give to the token as the first argument. + +```php +use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; + +$generator = new JWTGenerator(); + +$user = auth()->user(); +$token = $generator->generateAccessToken($user); +``` + +This creates the JWT to the user. + +It sets the `Config\AuthJWT::$claim` values to the token, and adds the user ID +in the `"sub"` (subject) claim. It also sets `"iat"` (Issued At) and `"exp"` +(Expiration Time) claims automatically. + +### Arbitrary JWT + +You can generate arbitrary JWT with the ``JWTGenerator::generate()`` method. + +It takes a JWT claims array, and can take time to live in seconds, a secret key, +and algorithm to use: + +```php +generate(array $claims, ?int $ttl = null, $key = null, ?string $algorithm = null): string +``` + +The following code generates a JWT. + +```php +use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; + +$generator = new JWTGenerator(); + +$payload = [ + 'user_id' => '1', + 'email' => 'admin@example.jp', +]; +$token = $generator->generate($payload, DAY); +``` + +It uses the `$secretKey` and `$algorithm` in the `Config\AuthJWT` if you don't +pass them. + +> **Note** +> ``JWTGenerator::generate()`` does not use `Config\AuthJWT::$claim`at all. + +It sets `"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically +even if you don't pass them. diff --git a/docs/index.md b/docs/index.md index a0fa90810..8d1103e7a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,6 +13,11 @@ * [Banning Users](banning_users.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) + +## Addons + +* [JWT Authentication](addons/jwt.md) diff --git a/docs/install.md b/docs/install.md index 6514e936b..74f5b3af4 100644 --- a/docs/install.md +++ b/docs/install.md @@ -196,18 +196,20 @@ your project. ``` ## Controller Filters + The [Controller Filters](https://codeigniter.com/user_guide/incoming/filters.html) you can use to protect your routes the shield provides are: ```php public $aliases = [ // ... - 'session' => \CodeIgniter\Shield\Filters\SessionAuth::class, - 'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class, - 'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class, - 'auth-rates' => \CodeIgniter\Shield\Filters\AuthRates::class, - 'group' => \CodeIgniter\Shield\Filters\GroupFilter::class, - 'permission' => \CodeIgniter\Shield\Filters\PermissionFilter::class, + 'session' => \CodeIgniter\Shield\Filters\SessionAuth::class, + 'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class, + 'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class, + 'auth-rates' => \CodeIgniter\Shield\Filters\AuthRates::class, + 'group' => \CodeIgniter\Shield\Filters\GroupFilter::class, + 'permission' => \CodeIgniter\Shield\Filters\PermissionFilter::class, 'force-reset' => \CodeIgniter\Shield\Filters\ForcePasswordResetFilter::class, + 'jwt' => \CodeIgniter\Shield\Filters\JWTAuth::class, ]; ``` @@ -215,6 +217,7 @@ Filters | Description --- | --- session and tokens | The `Session` and `AccessTokens` authenticators, respectively. chained | The filter will check both authenticators in sequence to see if the user is logged in through either of authenticators, allowing a single API endpoint to work for both an SPA using session auth, and a mobile app using access tokens. +jwt | The `JWT` authenticator. See [JWT Authentication](./addons/jwt.md). auth-rates | Provides a good basis for rate limiting of auth-related routes. group | Checks if the user is in one of the groups passed in. permission | Checks if the user has the passed permissions. diff --git a/mkdocs.yml b/mkdocs.yml index 023b20886..1b1752483 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,3 +54,5 @@ nav: - guides/api_tokens.md - guides/mobile_apps.md - guides/strengthen_password.md + - Addons: + - JWT Authentication: addons/jwt.md From 223031927865d53b5cbcbd10c2a566b85371113d Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Apr 2023 12:14:05 +0900 Subject: [PATCH 056/142] docs: fix @param --- src/Authentication/TokenGenerator/JWTGenerator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index b0e65a49d..12f069dcf 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -51,9 +51,9 @@ public function generateAccessToken(User $user): string /** * Issues JWT * - * @param array $claims The payload items. - * @param int|null $ttl Time to live in seconds. - * @param string $key The secret key. + * @param array $claims The payload items. + * @param int|null $ttl Time to live in seconds. + * @param string|null $key The secret key. */ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $algorithm = null): string { From 0575a5339f2dd0676589d812e1d4d6609fd63efc Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 14 Apr 2023 14:07:38 +0900 Subject: [PATCH 057/142] fix: when generating JWT, the real current time is not used for "iat" --- src/Authentication/TokenGenerator/JWTGenerator.php | 12 ++++++------ .../Authenticators/JWTAuthenticatorTest.php | 11 ++++++----- .../Authenticators/JWT/FirebaseAdapaterTest.php | 11 ++++++----- .../TokenGenerator/JWTGeneratorTest.php | 12 ++++++++++-- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 12f069dcf..1830329f0 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -12,13 +12,13 @@ class JWTGenerator { - private Time $currentTime; + private Time $clock; private JWTAdapterInterface $jwtAdapter; - public function __construct(?Time $currentTime = null, ?JWTAdapterInterface $jwtAdapter = null) + public function __construct(?Time $clock = null, ?JWTAdapterInterface $jwtAdapter = null) { - $this->currentTime = $currentTime ?? new Time(); - $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); + $this->clock = $clock ?? new Time(); + $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); } /** @@ -29,7 +29,7 @@ public function generateAccessToken(User $user): string /** @var AuthJWT $config */ $config = config('AuthJWT'); - $iat = $this->currentTime->getTimestamp(); + $iat = $this->clock->now()->getTimestamp(); $exp = $iat + $config->timeToLive; $payload = array_merge( @@ -70,7 +70,7 @@ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $ $payload = $claims; if (! array_key_exists('iat', $claims)) { - $payload['iat'] = $this->currentTime->getTimestamp(); + $payload['iat'] = $this->clock->now()->getTimestamp(); } if (! array_key_exists('exp', $claims)) { diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index ed3eb4a10..597e35441 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -130,8 +130,9 @@ public function testCheckNoSubToken(): void public function testCheckOldToken(): void { - $currentTime = new Time('-1 hour'); - $token = $this->generateJWT($currentTime); + Time::setTestNow('-1 hour'); + $token = $this->generateJWT(); + Time::setTestNow(); $result = $this->auth->check(['token' => $token]); @@ -234,13 +235,13 @@ public function testRecordActiveDateNoUser(): void } /** - * @param Time|null $currentTime The current time + * @param Time|null $clock The Time object */ - private function generateJWT(?Time $currentTime = null): string + private function generateJWT(?Time $clock = null): string { $this->user = \fake(UserModel::class, ['id' => 1, 'username' => 'John Smith']); - $generator = new JWTGenerator($currentTime); + $generator = new JWTGenerator($clock); return $generator->generateAccessToken($this->user); } diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index a1cd317da..b02efd773 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -37,14 +37,14 @@ public function testDecode(): void } /** - * @param Time|null $currentTime The current time + * @param Time|null $clock The Time object */ - public static function generateJWT(?Time $currentTime = null): string + public static function generateJWT(?Time $clock = null): string { /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); - $generator = new JWTGenerator($currentTime); + $generator = new JWTGenerator($clock); return $generator->generateAccessToken($user); } @@ -72,8 +72,9 @@ public function testDecodeExpiredException(): void $jwtDecoder = new FirebaseAdapter(); - $currentTime = new Time('-1 hour'); - $token = $this->generateJWT($currentTime); + Time::setTestNow('-1 hour'); + $token = $this->generateJWT(); + Time::setTestNow(); /** @var AuthJWT $config */ $config = config('AuthJWT'); diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index faa816154..63a3101d7 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -44,8 +44,16 @@ public function testTokenSubIsUserId(string $token): void public function testGenerate() { - $currentTime = new Time(); - $generator = new JWTGenerator($currentTime); + // Fix the current time for testing. + Time::setTestNow('now'); + + $clock = new Time(); + $generator = new JWTGenerator($clock); + + $currentTime = $clock->now(); + + // Reset the current time. + Time::setTestNow(); $payload = [ 'user_id' => '1', From a85374d1b4c37a19d1eadbe2b0beb89d206d3356 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 12:01:51 +0900 Subject: [PATCH 058/142] feat: change $claims to $defaultClaims, and all generated JWTs have the value in payload --- docs/addons/jwt.md | 11 ++---- .../TokenGenerator/JWTGenerator.php | 7 ++-- src/Config/AuthJWT.php | 7 ++-- .../Authenticators/JWTAuthenticatorTest.php | 4 +-- .../JWT/FirebaseAdapaterTest.php | 2 +- .../TokenGenerator/JWTGeneratorTest.php | 34 +++++++++++++++---- 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 1f83fe39c..198b68e41 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -37,19 +37,17 @@ To use JWT Authentication, you need additional setup and configuration. Configure **app/Config/AuthJWT.php** for your needs. -### Set Common Payload +### Set the Default Claims -Set the payload items that all JWTs have to the property `$claims`. +Set the payload items by default to the property `$defaultClaims`. E.g.: ```php - public array $claims = [ + public array $defaultClaims = [ 'iss' => 'https://codeigniter.com/', ]; ``` -This value is used by the `JWTGenerator::generateAccessToken()` method. - ### Set Secret Key Set yout secret key to the `$secretKey` property, or set it in your `.env` file. @@ -116,8 +114,5 @@ $token = $generator->generate($payload, DAY); It uses the `$secretKey` and `$algorithm` in the `Config\AuthJWT` if you don't pass them. -> **Note** -> ``JWTGenerator::generate()`` does not use `Config\AuthJWT::$claim`at all. - It sets `"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically even if you don't pass them. diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 1830329f0..e2b174a28 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -33,7 +33,7 @@ public function generateAccessToken(User $user): string $exp = $iat + $config->timeToLive; $payload = array_merge( - $config->claims, + $config->defaultClaims, [ 'sub' => (string) $user->id, // subject 'iat' => $iat, // issued at @@ -67,7 +67,10 @@ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $ $algorithm ??= $config->algorithm; $key ??= $config->secretKey; - $payload = $claims; + $payload = array_merge( + $config->defaultClaims, + $claims + ); if (! array_key_exists('iat', $claims)) { $payload['iat'] = $this->clock->now()->getTimestamp(); diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index be93783cf..206edaab9 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -22,12 +22,11 @@ class AuthJWT extends BaseConfig public string $authenticatorHeader = 'Authorization'; /** - * The payload items that all JWT have. + * The default payload items. * - * @var string[] - * @phpstan-var array + * @var array */ - public array $claims = [ + public array $defaultClaims = [ 'iss' => '', ]; diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 597e35441..42df08162 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -118,7 +118,7 @@ public function testCheckNoSubToken(): void /** @var AuthJWT $config */ $config = config('AuthJWT'); $payload = [ - 'iss' => $config->claims['iss'], // issuer + 'iss' => $config->defaultClaims['iss'], // issuer ]; $token = FirebaseJWT::encode($payload, $config->secretKey, $config->algorithm); @@ -174,7 +174,7 @@ public function testGetPayload(): void $this->assertSame((string) $this->user->id, $payload->sub); /** @var AuthJWT $config */ $config = config('AuthJWT'); - $this->assertSame($config->claims['iss'], $payload->iss); + $this->assertSame($config->defaultClaims['iss'], $payload->iss); } public function testAttemptBadSignatureToken(): void diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index b02efd773..2b15dd54c 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -32,7 +32,7 @@ public function testDecode(): void $payload = $jwtDecoder->decode($token, $key, $algorithm); - $this->assertSame($config->claims['iss'], $payload->iss); + $this->assertSame($config->defaultClaims['iss'], $payload->iss); $this->assertSame('1', $payload->sub); } diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index 63a3101d7..71d709b54 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -7,6 +7,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\JWT; use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; +use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Models\UserModel; use Tests\Support\TestCase; @@ -19,27 +20,46 @@ final class JWTGeneratorTest extends TestCase public function testGenerateAccessToken() { /** @var User $user */ - $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); - $generator = new JWTGenerator(); + $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); + + // Fix the current time for testing. + Time::setTestNow('now'); + + $clock = new Time(); + $generator = new JWTGenerator($clock); + + $currentTime = $clock->now(); + + // Reset the current time. + Time::setTestNow(); $token = $generator->generateAccessToken($user); $this->assertIsString($token); $this->assertStringStartsWith('eyJ', $token); - return $token; + return [$token, $currentTime]; } /** * @depends testGenerateAccessToken */ - public function testTokenSubIsUserId(string $token): void + public function testGenerateAccessTokenPayload(array $data): void { + [$token, $currentTime] = $data; + $auth = new JWT(new UserModel()); $payload = $auth->decodeJWT($token); - $this->assertSame('1', $payload->sub); + $config = config(AuthJWT::class); + $expected = [ + 'iss' => $config->defaultClaims['iss'], + 'sub' => '1', + 'iat' => $currentTime->getTimestamp(), + 'exp' => $currentTime->getTimestamp() + $config->timeToLive, + ]; + $this->assertSame($expected, (array) $payload); } public function testGenerate() @@ -71,7 +91,7 @@ public function testGenerate() /** * @depends testGenerate */ - public function testTokenHasIatAndExp(array $data): void + public function testGeneratePayload(array $data): void { [$token, $currentTime] = $data; @@ -79,7 +99,9 @@ public function testTokenHasIatAndExp(array $data): void $payload = $auth->decodeJWT($token); + $config = config(AuthJWT::class); $expected = [ + 'iss' => $config->defaultClaims['iss'], 'user_id' => '1', 'email' => 'admin@example.jp', 'iat' => $currentTime->getTimestamp(), From 5b828d734b537a38def5d7f03d3f423f50288dd4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 13:07:53 +0900 Subject: [PATCH 059/142] feat: change $secretKey to $keys and set $algorithm in them --- docs/addons/jwt.md | 4 ++-- src/Authentication/Authenticators/JWT.php | 4 ++-- .../TokenGenerator/JWTGenerator.php | 8 +++---- src/Config/AuthJWT.php | 23 ++++++++++++++++--- .../Authenticators/JWTAuthenticatorTest.php | 2 +- .../JWT/FirebaseAdapaterTest.php | 12 +++++----- 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 198b68e41..a4c600fda 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -50,11 +50,11 @@ E.g.: ### Set Secret Key -Set yout secret key to the `$secretKey` property, or set it in your `.env` file. +Set your secret key in the `$keys` property, or set it in your `.env` file. E.g.: ```dotenv -authjwt.secretKey = 8XBFsF6HThIa7OV/bSynahEch+WbKrGcuiJVYPiwqPE= +authjwt.keys.default.0.secret = 8XBFsF6HThIa7OV/bSynahEch+WbKrGcuiJVYPiwqPE= ``` It needs more than 256 bits random string. You can get a secure random string diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index b5f507dcd..0b95d84b8 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -243,8 +243,8 @@ public function decodeJWT(string $encodedToken): stdClass /** @var AuthJWT $config */ $config = config('AuthJWT'); - $key = $config->secretKey; - $algorithm = $config->algorithm; + $key = $config->keys['default'][0]['secret']; + $algorithm = $config->keys['default'][0]['alg']; return $this->jwtAdapter->decode($encodedToken, $key, $algorithm); } diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index e2b174a28..31cf02471 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -43,8 +43,8 @@ public function generateAccessToken(User $user): string return $this->jwtAdapter->generate( $payload, - $config->secretKey, - $config->algorithm + $config->keys['default'][0]['secret'], + $config->keys['default'][0]['alg'] ); } @@ -64,8 +64,8 @@ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $ /** @var AuthJWT $config */ $config = config('AuthJWT'); - $algorithm ??= $config->algorithm; - $key ??= $config->secretKey; + $algorithm ??= $config->keys['default'][0]['alg']; + $key ??= $config->keys['default'][0]['secret']; $payload = array_merge( $config->defaultClaims, diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index 206edaab9..62ceb4500 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -34,12 +34,29 @@ class AuthJWT extends BaseConfig * The secret key. Needs more than 256 bits random string. * E.g., $ php -r 'echo base64_encode(random_bytes(32));' */ - public string $secretKey = ''; /** - * JWT Signing Algorithms. + * The Keys */ - public string $algorithm = 'HS256'; + public array $keys = [ + 'default' => [ + // Symmetric Key + [ + 'kid' => '', // (Optional) Key ID. + 'alg' => 'HS256', // algorithm. + // Set secret random string. Needs more than 256 bits. + // E.g., $ php -r 'echo base64_encode(random_bytes(32));' + 'secret' => '', + ], + // (Not implemented) Asymmetric Key + // [ + // 'kid' => '', // (Optional) Key ID. + // 'alg' => 'RS256', // algorithm. + // 'public' => '', // Public Key + // 'private' => '', // Private Key + // ], + ], + ]; /** * Specifies the amount of time, in seconds, that a token is valid. diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 42df08162..f0f77e5c0 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -120,7 +120,7 @@ public function testCheckNoSubToken(): void $payload = [ 'iss' => $config->defaultClaims['iss'], // issuer ]; - $token = FirebaseJWT::encode($payload, $config->secretKey, $config->algorithm); + $token = FirebaseJWT::encode($payload, $config->keys['default'][0]['secret'], $config->keys['default'][0]['alg']); $result = $this->auth->check(['token' => $token]); diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index 2b15dd54c..35ebaab69 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -27,8 +27,8 @@ public function testDecode(): void /** @var AuthJWT $config */ $config = config('AuthJWT'); - $key = $config->secretKey; - $algorithm = $config->algorithm; + $key = $config->keys['default'][0]['secret']; + $algorithm = $config->keys['default'][0]['alg']; $payload = $jwtDecoder->decode($token, $key, $algorithm); @@ -58,8 +58,8 @@ public function testDecodeSignatureInvalidException(): void /** @var AuthJWT $config */ $config = config('AuthJWT'); - $key = $config->secretKey; - $algorithm = $config->algorithm; + $key = $config->keys['default'][0]['secret']; + $algorithm = $config->keys['default'][0]['alg']; $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJc3N1ZXIgb2YgdGhlIEpXVCIsImF1ZCI6IkF1ZGllbmNlIG9mIHRoZSBKV1QiLCJzdWIiOiIxIiwiaWF0IjoxNjUzOTkxOTg5LCJleHAiOjE2NTM5OTU1ODl9.hgOYHEcT6RGHb3po1lspTcmjrylY1Cy1IvYmHOyx0CY'; $jwtDecoder->decode($token, $key, $algorithm); @@ -78,8 +78,8 @@ public function testDecodeExpiredException(): void /** @var AuthJWT $config */ $config = config('AuthJWT'); - $key = $config->secretKey; - $algorithm = $config->algorithm; + $key = $config->keys['default'][0]['secret']; + $algorithm = $config->keys['default'][0]['alg']; $jwtDecoder->decode($token, $key, $algorithm); } From 3849c6f96e49f71deb410e0e9fedea91d51532de Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 13:15:03 +0900 Subject: [PATCH 060/142] refactor: use ::class in config() --- src/Authentication/Authenticators/JWT.php | 9 +++------ src/Authentication/TokenGenerator/JWTGenerator.php | 6 ++---- src/Filters/JWTAuth.php | 3 +-- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 0b95d84b8..2871beac4 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -57,8 +57,7 @@ public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtAdapte */ public function attempt(array $credentials): Result { - /** @var AuthJWT $config */ - $config = config('AuthJWT'); + $config = config(AuthJWT::class); /** @var IncomingRequest $request */ $request = service('request'); @@ -169,8 +168,7 @@ public function loggedIn(): bool /** @var IncomingRequest $request */ $request = service('request'); - /** @var AuthJWT $config */ - $config = config('AuthJWT'); + $config = config(AuthJWT::class); return $this->attempt([ 'token' => $request->getHeaderLine($config->authenticatorHeader), @@ -240,8 +238,7 @@ public function recordActiveDate(): void */ public function decodeJWT(string $encodedToken): stdClass { - /** @var AuthJWT $config */ - $config = config('AuthJWT'); + $config = config(AuthJWT::class); $key = $config->keys['default'][0]['secret']; $algorithm = $config->keys['default'][0]['alg']; diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 31cf02471..8d2a2023f 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -26,8 +26,7 @@ public function __construct(?Time $clock = null, ?JWTAdapterInterface $jwtAdapte */ public function generateAccessToken(User $user): string { - /** @var AuthJWT $config */ - $config = config('AuthJWT'); + $config = config(AuthJWT::class); $iat = $this->clock->now()->getTimestamp(); $exp = $iat + $config->timeToLive; @@ -62,8 +61,7 @@ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $ 'Cannot pass $claims[\'exp\'] and $ttl at the same time.' ); - /** @var AuthJWT $config */ - $config = config('AuthJWT'); + $config = config(AuthJWT::class); $algorithm ??= $config->keys['default'][0]['alg']; $key ??= $config->keys['default'][0]['secret']; diff --git a/src/Filters/JWTAuth.php b/src/Filters/JWTAuth.php index 24da9732d..5c52fc3a6 100644 --- a/src/Filters/JWTAuth.php +++ b/src/Filters/JWTAuth.php @@ -66,8 +66,7 @@ private function getTokenFromHeader(RequestInterface $request): string { assert($request instanceof IncomingRequest); - /** @var AuthJWT $config */ - $config = config('AuthJWT'); + $config = config(AuthJWT::class); $tokenHeader = $request->getHeaderLine( $config->authenticatorHeader ?? 'Authorization' From ac23566394d32f6401fc869eb0d1907195c32578 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 14:51:14 +0900 Subject: [PATCH 061/142] feat: change JWTGenerator::generate() signature It sets "kid" in headers if it is set in the Config. It can also set additional header items. --- docs/addons/jwt.md | 14 ++-- .../Authenticators/JWT/FirebaseAdapter.php | 21 +++--- .../JWT/JWTAdapterInterface.php | 22 +++++-- .../TokenGenerator/JWTGenerator.php | 37 +++++++---- .../TokenGenerator/JWTGeneratorTest.php | 64 +++++++++++++++++++ 5 files changed, 127 insertions(+), 31 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index a4c600fda..7da428fc4 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -90,11 +90,16 @@ in the `"sub"` (subject) claim. It also sets `"iat"` (Issued At) and `"exp"` You can generate arbitrary JWT with the ``JWTGenerator::generate()`` method. -It takes a JWT claims array, and can take time to live in seconds, a secret key, -and algorithm to use: +It takes a JWT claims array, and can take time to live in seconds, a key group +(an array key) in the `Config\AuthJWT::$keys`, and additional header array: ```php -generate(array $claims, ?int $ttl = null, $key = null, ?string $algorithm = null): string +public function generate( + array $claims, + ?int $ttl = null, + $key = 'default', + ?array $headers = null +): string ``` The following code generates a JWT. @@ -111,8 +116,7 @@ $payload = [ $token = $generator->generate($payload, DAY); ``` -It uses the `$secretKey` and `$algorithm` in the `Config\AuthJWT` if you don't -pass them. +It uses the `secret` and `alg` in the `Config\AuthJWT::$keys['default]`. It sets `"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically even if you don't pass them. diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php index 71a9c2c5d..204eb2cf5 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -18,11 +18,7 @@ class FirebaseAdapter implements JWTAdapterInterface { /** - * Decode JWT - * - * @param string $key - * - * @return stdClass Payload + * {@inheritDoc} */ public static function decode(string $encodedToken, $key, string $algorithm): stdClass { @@ -39,12 +35,15 @@ public static function decode(string $encodedToken, $key, string $algorithm): st } /** - * Issues JWT - * - * @param string $key + * {@inheritDoc} */ - public static function generate(array $payload, $key, string $algorithm): string - { - return JWT::encode($payload, $key, $algorithm); + public static function generate( + array $payload, + $key, + string $algorithm, + ?string $keyId = null, + ?array $headers = null + ): string { + return JWT::encode($payload, $key, $algorithm, $keyId, $headers); } } diff --git a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php index a12444d65..094d9adc4 100644 --- a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php +++ b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php @@ -4,19 +4,33 @@ namespace CodeIgniter\Shield\Authentication\Authenticators\JWT; +use Firebase\JWT\JWT; use stdClass; interface JWTAdapterInterface { /** - * Issues JWT + * Issues JWT (JWS) * - * @param string $key The secret key. + * @param array $payload The payload. + * @param string $key The secret key. + * @param string $algorithm Supported algorithms: + * 'ES384','ES256', 'ES256K', + * 'HS256', 'HS384', 'HS512', + * 'RS256', 'RS384', 'RS512' + * @param string|null $keyId The key ID. + * @param array|null $headers An array with header elements to attach. */ - public static function generate(array $payload, $key, string $algorithm): string; + public static function generate( + array $payload, + $key, + string $algorithm, + ?string $keyId = null, + ?array $headers = null + ): string; /** - * Decode JWT + * Decode JWT (JWS) * * @param string $key The secret key. * diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 8d2a2023f..bcebe6f43 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -22,7 +22,7 @@ public function __construct(?Time $clock = null, ?JWTAdapterInterface $jwtAdapte } /** - * Issues JWT Access Token + * Issues JWT (JWS) for a User */ public function generateAccessToken(User $user): string { @@ -48,22 +48,35 @@ public function generateAccessToken(User $user): string } /** - * Issues JWT + * Issues JWT (JWS) * - * @param array $claims The payload items. - * @param int|null $ttl Time to live in seconds. - * @param string|null $key The secret key. + * @param array $claims The payload items. + * @param int|null $ttl Time to live in seconds. + * @param string $key The key group. + * The array key of Config\AuthJWT::$keys. + * @param array|null $headers An array with header elements to attach. */ - public function generate(array $claims, ?int $ttl = null, $key = null, ?string $algorithm = null): string - { + public function generate( + array $claims, + ?int $ttl = null, + $key = 'default', + ?array $headers = null + ): string { assert( (array_key_exists('exp', $claims) && ($ttl !== null)) === false, 'Cannot pass $claims[\'exp\'] and $ttl at the same time.' ); - $config = config(AuthJWT::class); - $algorithm ??= $config->keys['default'][0]['alg']; - $key ??= $config->keys['default'][0]['secret']; + $keyGroup = $key; + + $config = config(AuthJWT::class); + $key = $config->keys[$keyGroup][0]['secret']; + $algorithm = $config->keys[$keyGroup][0]['alg']; + + $keyId = $config->keys[$keyGroup][0]['kid'] ?? null; + if ($keyId === '') { + $keyId = null; + } $payload = array_merge( $config->defaultClaims, @@ -85,7 +98,9 @@ public function generate(array $claims, ?int $ttl = null, $key = null, ?string $ return $this->jwtAdapter->generate( $payload, $key, - $algorithm + $algorithm, + $keyId, + $headers ); } } diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index 71d709b54..0c084d82d 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -109,4 +109,68 @@ public function testGeneratePayload(array $data): void ]; $this->assertSame($expected, (array) $payload); } + + public function testGenerateSetKid(): void + { + $generator = new JWTGenerator(); + + // Set kid + $config = config(AuthJWT::class); + $config->keys['default'][0]['kid'] = 'Key01'; + + $payload = [ + 'user_id' => '1', + ]; + $token = $generator->generate($payload, DAY); + + $this->assertIsString($token); + + $headers = $this->decodeJWTHeader($token); + $this->assertSame([ + 'typ' => 'JWT', + 'alg' => 'HS256', + 'kid' => 'Key01', + ], $headers); + } + + public function testGenerateAddHeader(): void + { + $generator = new JWTGenerator(); + + $payload = [ + 'user_id' => '1', + ]; + $headers = [ + 'extra_key' => 'extra_value', + ]; + $token = $generator->generate($payload, DAY, 'default', $headers); + + $this->assertIsString($token); + + $headers = $this->decodeJWTHeader($token); + $this->assertSame([ + 'extra_key' => 'extra_value', + 'typ' => 'JWT', + 'alg' => 'HS256', + ], $headers); + } + + private function decodeJWTHeader(string $token): array + { + return json_decode( + base64_decode( + str_replace( + '_', + '/', + str_replace( + '-', + '+', + explode('.', $token)[0] + ) + ), + true + ), + true + ); + } } From dde79d8e15276e6fd2c8d604e7d1ab29a394ff6d Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 15:07:34 +0900 Subject: [PATCH 062/142] docs: add PHPDoc --- src/Config/AuthJWT.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index 62ceb4500..85a8ab58a 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -37,6 +37,11 @@ class AuthJWT extends BaseConfig /** * The Keys + * + * The key of the array is the key group name. + * + * @var array>> + * @phpstan-var array>> */ public array $keys = [ 'default' => [ From a66123c31107677f661201f63417181f891be117 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 15:32:26 +0900 Subject: [PATCH 063/142] refactor: remove unused `use` --- src/Authentication/Authenticators/JWT/JWTAdapterInterface.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php index 094d9adc4..2d01f7206 100644 --- a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php +++ b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php @@ -4,7 +4,6 @@ namespace CodeIgniter\Shield\Authentication\Authenticators\JWT; -use Firebase\JWT\JWT; use stdClass; interface JWTAdapterInterface From 91b14ffc43055f53bad0046f7f89ad3e1c37cca4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 16:12:54 +0900 Subject: [PATCH 064/142] fix: JWTAdapterInterface APIs Also fix FirebaseAdapter APIs. --- src/Authentication/Authenticators/JWT.php | 20 +++++++++---- .../Authenticators/JWT/FirebaseAdapter.php | 29 ++++++++++++++----- .../JWT/JWTAdapterInterface.php | 18 ++++-------- .../TokenGenerator/JWTGenerator.php | 16 ++-------- .../JWT/FirebaseAdapaterTest.php | 21 ++++---------- 5 files changed, 49 insertions(+), 55 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 2871beac4..7197e50f1 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -41,6 +41,11 @@ class JWT implements AuthenticatorInterface protected TokenLoginModel $tokenLoginModel; protected ?stdClass $payload = null; + /** + * @var string The key group. The array key of Config\AuthJWT::$keys. + */ + protected $key = 'default'; + public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtAdapter = null) { $this->provider = $provider; @@ -233,17 +238,20 @@ public function recordActiveDate(): void $this->provider->save($this->user); } + /** + * @param string $key The key group. The array key of Config\AuthJWT::$keys. + */ + public function setKey($key): void + { + $this->key = $key; + } + /** * Returns payload of the JWT */ public function decodeJWT(string $encodedToken): stdClass { - $config = config(AuthJWT::class); - - $key = $config->keys['default'][0]['secret']; - $algorithm = $config->keys['default'][0]['alg']; - - return $this->jwtAdapter->decode($encodedToken, $key, $algorithm); + return $this->jwtAdapter->decode($encodedToken, $this->key); } /** diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php index 204eb2cf5..7ca78ef42 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -4,6 +4,7 @@ namespace CodeIgniter\Shield\Authentication\Authenticators\JWT; +use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Exceptions\RuntimeException; use DomainException; use Firebase\JWT\BeforeValidException; @@ -20,8 +21,14 @@ class FirebaseAdapter implements JWTAdapterInterface /** * {@inheritDoc} */ - public static function decode(string $encodedToken, $key, string $algorithm): stdClass + public static function decode(string $encodedToken, $key): stdClass { + $keyGroup = $key; + + $config = config(AuthJWT::class); + $key = $config->keys[$keyGroup][0]['secret']; + $algorithm = $config->keys[$keyGroup][0]['alg']; + try { return JWT::decode($encodedToken, new Key($key, $algorithm)); } catch (BeforeValidException|ExpiredException $e) { @@ -37,13 +44,19 @@ public static function decode(string $encodedToken, $key, string $algorithm): st /** * {@inheritDoc} */ - public static function generate( - array $payload, - $key, - string $algorithm, - ?string $keyId = null, - ?array $headers = null - ): string { + public static function generate(array $payload, $key, ?array $headers = null): string + { + $keyGroup = $key; + + $config = config(AuthJWT::class); + $key = $config->keys[$keyGroup][0]['secret']; + $algorithm = $config->keys[$keyGroup][0]['alg']; + + $keyId = $config->keys[$keyGroup][0]['kid'] ?? null; + if ($keyId === '') { + $keyId = null; + } + return JWT::encode($payload, $key, $algorithm, $keyId, $headers); } } diff --git a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php index 2d01f7206..c288bc67b 100644 --- a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php +++ b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php @@ -11,29 +11,23 @@ interface JWTAdapterInterface /** * Issues JWT (JWS) * - * @param array $payload The payload. - * @param string $key The secret key. - * @param string $algorithm Supported algorithms: - * 'ES384','ES256', 'ES256K', - * 'HS256', 'HS384', 'HS512', - * 'RS256', 'RS384', 'RS512' - * @param string|null $keyId The key ID. - * @param array|null $headers An array with header elements to attach. + * @param array $payload The payload. + * @param string $key The key group. + * The array key of Config\AuthJWT::$keys. + * @param array|null $headers An array with header elements to attach. */ public static function generate( array $payload, $key, - string $algorithm, - ?string $keyId = null, ?array $headers = null ): string; /** * Decode JWT (JWS) * - * @param string $key The secret key. + * @param string $key The key group. The array key of Config\AuthJWT::$keys. * * @return stdClass Payload */ - public static function decode(string $encodedToken, $key, string $algorithm): stdClass; + public static function decode(string $encodedToken, $key): stdClass; } diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index bcebe6f43..0f7538724 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -42,8 +42,7 @@ public function generateAccessToken(User $user): string return $this->jwtAdapter->generate( $payload, - $config->keys['default'][0]['secret'], - $config->keys['default'][0]['alg'] + 'default' ); } @@ -67,16 +66,7 @@ public function generate( 'Cannot pass $claims[\'exp\'] and $ttl at the same time.' ); - $keyGroup = $key; - - $config = config(AuthJWT::class); - $key = $config->keys[$keyGroup][0]['secret']; - $algorithm = $config->keys[$keyGroup][0]['alg']; - - $keyId = $config->keys[$keyGroup][0]['kid'] ?? null; - if ($keyId === '') { - $keyId = null; - } + $config = config(AuthJWT::class); $payload = array_merge( $config->defaultClaims, @@ -98,8 +88,6 @@ public function generate( return $this->jwtAdapter->generate( $payload, $key, - $algorithm, - $keyId, $headers ); } diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php index 35ebaab69..768cd6985 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php @@ -27,10 +27,9 @@ public function testDecode(): void /** @var AuthJWT $config */ $config = config('AuthJWT'); - $key = $config->keys['default'][0]['secret']; - $algorithm = $config->keys['default'][0]['alg']; + $key = 'default'; - $payload = $jwtDecoder->decode($token, $key, $algorithm); + $payload = $jwtDecoder->decode($token, $key); $this->assertSame($config->defaultClaims['iss'], $payload->iss); $this->assertSame('1', $payload->sub); @@ -56,13 +55,9 @@ public function testDecodeSignatureInvalidException(): void $jwtDecoder = new FirebaseAdapter(); - /** @var AuthJWT $config */ - $config = config('AuthJWT'); - $key = $config->keys['default'][0]['secret']; - $algorithm = $config->keys['default'][0]['alg']; - + $key = 'default'; $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJc3N1ZXIgb2YgdGhlIEpXVCIsImF1ZCI6IkF1ZGllbmNlIG9mIHRoZSBKV1QiLCJzdWIiOiIxIiwiaWF0IjoxNjUzOTkxOTg5LCJleHAiOjE2NTM5OTU1ODl9.hgOYHEcT6RGHb3po1lspTcmjrylY1Cy1IvYmHOyx0CY'; - $jwtDecoder->decode($token, $key, $algorithm); + $jwtDecoder->decode($token, $key); } public function testDecodeExpiredException(): void @@ -76,11 +71,7 @@ public function testDecodeExpiredException(): void $token = $this->generateJWT(); Time::setTestNow(); - /** @var AuthJWT $config */ - $config = config('AuthJWT'); - $key = $config->keys['default'][0]['secret']; - $algorithm = $config->keys['default'][0]['alg']; - - $jwtDecoder->decode($token, $key, $algorithm); + $key = 'default'; + $jwtDecoder->decode($token, $key); } } From a6e3ac7b05060cca917babf2acaab0549a40f673 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 16:32:04 +0900 Subject: [PATCH 065/142] feat: use multiple keys for docoding JWT --- .../Authenticators/JWT/FirebaseAdapter.php | 23 ++++++++++-- src/Config/AuthJWT.php | 5 ++- .../Authenticators/JWTAuthenticatorTest.php | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php index 7ca78ef42..5830c8a65 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -25,12 +25,27 @@ public static function decode(string $encodedToken, $key): stdClass { $keyGroup = $key; - $config = config(AuthJWT::class); - $key = $config->keys[$keyGroup][0]['secret']; - $algorithm = $config->keys[$keyGroup][0]['alg']; + $config = config(AuthJWT::class); + + $configKeys = $config->keys[$keyGroup]; + if (count($configKeys) === 1) { + $key = $configKeys[0]['secret']; + $algorithm = $configKeys[0]['alg']; + + $keys = new Key($key, $algorithm); + } else { + $keys = []; + + foreach ($config->keys[$keyGroup] as $item) { + $key = $item['secret']; + $algorithm = $item['alg']; + + $keys[$item['kid']] = new Key($key, $algorithm); + } + } try { - return JWT::decode($encodedToken, new Key($key, $algorithm)); + return JWT::decode($encodedToken, $keys); } catch (BeforeValidException|ExpiredException $e) { throw new RuntimeException('Expired JWT: ' . $e->getMessage(), 0, $e); } catch ( diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index 85a8ab58a..0b8d4b0c5 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -39,6 +39,7 @@ class AuthJWT extends BaseConfig * The Keys * * The key of the array is the key group name. + * The first key of the group is used for signing. * * @var array>> * @phpstan-var array>> @@ -47,7 +48,7 @@ class AuthJWT extends BaseConfig 'default' => [ // Symmetric Key [ - 'kid' => '', // (Optional) Key ID. + 'kid' => '', // Key ID. Optional if you have only one key. 'alg' => 'HS256', // algorithm. // Set secret random string. Needs more than 256 bits. // E.g., $ php -r 'echo base64_encode(random_bytes(32));' @@ -55,7 +56,7 @@ class AuthJWT extends BaseConfig ], // (Not implemented) Asymmetric Key // [ - // 'kid' => '', // (Optional) Key ID. + // 'kid' => '', // Key ID. Optional if you have only one key. // 'alg' => 'RS256', // algorithm. // 'public' => '', // Public Key // 'private' => '', // Private Key diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index f0f77e5c0..47891cf6d 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -245,4 +245,41 @@ private function generateJWT(?Time $clock = null): string return $generator->generateAccessToken($this->user); } + + public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void + { + $config = config(AuthJWT::class); + $config->keys['default'] = [ + [ + 'kid' => 'Key01', + 'alg' => 'HS256', // algorithm. + 'secret' => 'Key01_Secret', + ], + ]; + + // Generate token with Key01. + $generator = new JWTGenerator(); + $payload = [ + 'user_id' => '1', + ]; + $token = $generator->generate($payload, DAY, 'default'); + + // Add new Key02. + $config->keys['default'] = [ + [ + 'kid' => 'Key02', + 'alg' => 'HS256', // algorithm. + 'secret' => 'Key02_Secret', + ], + [ + 'kid' => 'Key01', + 'alg' => 'HS256', // algorithm. + 'secret' => 'Key01_Secret', + ], + ]; + + $payload = $this->auth->decodeJWT($token); + + $this->assertSame('1', $payload->user_id); + } } From 3d1b33412842d17f29feaeff5e255c0f92513844 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 16:33:32 +0900 Subject: [PATCH 066/142] docs: remove out of dated comment --- src/Config/AuthJWT.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index 0b8d4b0c5..d565b2327 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -30,11 +30,6 @@ class AuthJWT extends BaseConfig 'iss' => '', ]; - /** - * The secret key. Needs more than 256 bits random string. - * E.g., $ php -r 'echo base64_encode(random_bytes(32));' - */ - /** * The Keys * From fce67b750f27d5d419dabc0b361a27eb4762d868 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 16:39:12 +0900 Subject: [PATCH 067/142] test: add test for specifiying key to decodeJWT() --- .../Authenticators/JWTAuthenticatorTest.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 47891cf6d..b51f858a0 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -282,4 +282,28 @@ public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void $this->assertSame('1', $payload->user_id); } + + public function testDecodeJWTCanSpecifyKey(): void + { + $config = config(AuthJWT::class); + $config->keys['mobile'] = [ + [ + 'kid' => 'Key01', + 'alg' => 'HS256', // algorithm. + 'secret' => 'Key01_Secret', + ], + ]; + + // Generate token with the mobile key. + $generator = new JWTGenerator(); + $payload = [ + 'user_id' => '1', + ]; + $token = $generator->generate($payload, DAY, 'mobile'); + + $this->auth->setKey('mobile'); + $payload = $this->auth->decodeJWT($token); + + $this->assertSame('1', $payload->user_id); + } } From c58ac36d974ad00af4b0930312aff7af9f3b0c03 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 17:00:16 +0900 Subject: [PATCH 068/142] test: fix incorrect tests --- .../Authentication/TokenGenerator/JWTGeneratorTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index 0c084d82d..3762b116d 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -30,11 +30,11 @@ public function testGenerateAccessToken() $currentTime = $clock->now(); + $token = $generator->generateAccessToken($user); + // Reset the current time. Time::setTestNow(); - $token = $generator->generateAccessToken($user); - $this->assertIsString($token); $this->assertStringStartsWith('eyJ', $token); @@ -72,9 +72,6 @@ public function testGenerate() $currentTime = $clock->now(); - // Reset the current time. - Time::setTestNow(); - $payload = [ 'user_id' => '1', 'email' => 'admin@example.jp', @@ -82,6 +79,9 @@ public function testGenerate() $token = $generator->generate($payload, DAY); + // Reset the current time. + Time::setTestNow(); + $this->assertIsString($token); $this->assertStringStartsWith('eyJ', $token); From a37ef77353546c6d3003210a0c401b34afb2ef84 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 17:13:22 +0900 Subject: [PATCH 069/142] refactor: change JWTAdapterInterface method name --- src/Authentication/Authenticators/JWT/FirebaseAdapter.php | 2 +- src/Authentication/Authenticators/JWT/JWTAdapterInterface.php | 2 +- src/Authentication/TokenGenerator/JWTGenerator.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php index 5830c8a65..028bb339c 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -59,7 +59,7 @@ public static function decode(string $encodedToken, $key): stdClass /** * {@inheritDoc} */ - public static function generate(array $payload, $key, ?array $headers = null): string + public static function encode(array $payload, $key, ?array $headers = null): string { $keyGroup = $key; diff --git a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php index c288bc67b..ed06a6322 100644 --- a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php +++ b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php @@ -16,7 +16,7 @@ interface JWTAdapterInterface * The array key of Config\AuthJWT::$keys. * @param array|null $headers An array with header elements to attach. */ - public static function generate( + public static function encode( array $payload, $key, ?array $headers = null diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 0f7538724..2918bc12b 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -40,7 +40,7 @@ public function generateAccessToken(User $user): string ] ); - return $this->jwtAdapter->generate( + return $this->jwtAdapter->encode( $payload, 'default' ); @@ -85,7 +85,7 @@ public function generate( $payload['exp'] = $payload['iat'] + $ttl; } - return $this->jwtAdapter->generate( + return $this->jwtAdapter->encode( $payload, $key, $headers From af50ece8f071a399377449d73e784fc90d894ed0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 17:20:20 +0900 Subject: [PATCH 070/142] docs: add @return --- src/Authentication/Authenticators/JWT/JWTAdapterInterface.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php index ed06a6322..28768189a 100644 --- a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php +++ b/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php @@ -15,6 +15,8 @@ interface JWTAdapterInterface * @param string $key The key group. * The array key of Config\AuthJWT::$keys. * @param array|null $headers An array with header elements to attach. + * + * @return string JWT (JWS) */ public static function encode( array $payload, From 73a1d4df4e159c0821a466744b8396823a39720e Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 18:01:33 +0900 Subject: [PATCH 071/142] feat: add parameters to JWTGenerator::generateAccessToken() --- docs/addons/jwt.md | 31 +++++++++++++---- .../TokenGenerator/JWTGenerator.php | 33 +++++++++--------- .../TokenGenerator/JWTGeneratorTest.php | 34 ++++++++++++++++--- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 7da428fc4..2614d0ac8 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -70,6 +70,21 @@ php -r 'echo base64_encode(random_bytes(32));' JWTs are created through the `JWTGenerator::generateAccessToken()` method. This takes a User object to give to the token as the first argument. +It can also take optional additional claims array, time to live in seconds, +a key group (an array key) in the `Config\AuthJWT::$keys`, and additional header +array: + +```php +public function generateAccessToken( + User $user, + array $claims = [], + ?int $ttl = null, + $key = 'default', + ?array $headers = null +): string +``` + +The following code generates a JWT to the user. ```php use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; @@ -77,14 +92,16 @@ use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; $generator = new JWTGenerator(); $user = auth()->user(); -$token = $generator->generateAccessToken($user); +$claim = [ + 'email' => $user->email, +]; +$token = $generator->generateAccessToken($user, $claim); ``` -This creates the JWT to the user. - -It sets the `Config\AuthJWT::$claim` values to the token, and adds the user ID -in the `"sub"` (subject) claim. It also sets `"iat"` (Issued At) and `"exp"` -(Expiration Time) claims automatically. +It sets the `Config\AuthJWT::$defaultClaim` values to the token, and adds +the `'email'` claim and the user ID in the `"sub"` (subject) claim. +It also sets `"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically +if you don't specify. ### Arbitrary JWT @@ -116,7 +133,7 @@ $payload = [ $token = $generator->generate($payload, DAY); ``` -It uses the `secret` and `alg` in the `Config\AuthJWT::$keys['default]`. +It uses the `secret` and `alg` in the `Config\AuthJWT::$keys['default']`. It sets `"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically even if you don't pass them. diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/TokenGenerator/JWTGenerator.php index 2918bc12b..ecd137e75 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/TokenGenerator/JWTGenerator.php @@ -23,27 +23,28 @@ public function __construct(?Time $clock = null, ?JWTAdapterInterface $jwtAdapte /** * Issues JWT (JWS) for a User + * + * @param array $claims The payload items. + * @param int|null $ttl Time to live in seconds. + * @param string $key The key group. + * The array key of Config\AuthJWT::$keys. + * @param array|null $headers An array with header elements to attach. */ - public function generateAccessToken(User $user): string - { - $config = config(AuthJWT::class); - - $iat = $this->clock->now()->getTimestamp(); - $exp = $iat + $config->timeToLive; - + public function generateAccessToken( + User $user, + array $claims = [], + ?int $ttl = null, + $key = 'default', + ?array $headers = null + ): string { $payload = array_merge( - $config->defaultClaims, + $claims, [ - 'sub' => (string) $user->id, // subject - 'iat' => $iat, // issued at - 'exp' => $exp, // expiration time - ] + 'sub' => (string) $user->id, // subject + ], ); - return $this->jwtAdapter->encode( - $payload, - 'default' - ); + return $this->generate($payload, $ttl, $key, $headers); } /** diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index 3762b116d..c018ef1ab 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -62,6 +62,26 @@ public function testGenerateAccessTokenPayload(array $data): void $this->assertSame($expected, (array) $payload); } + public function testGenerateAccessTokenAddClaims(): void + { + /** @var User $user */ + $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); + + $generator = new JWTGenerator(); + + $claims = [ + 'email' => 'admin@example.jp', + ]; + $token = $generator->generateAccessToken($user, $claims); + + $this->assertIsString($token); + + $payload = $this->decodeJWT($token, 'payload'); + + $this->assertStringStartsWith('1', $payload['sub']); + $this->assertStringStartsWith('admin@example.jp', $payload['email']); + } + public function testGenerate() { // Fix the current time for testing. @@ -125,7 +145,7 @@ public function testGenerateSetKid(): void $this->assertIsString($token); - $headers = $this->decodeJWTHeader($token); + $headers = $this->decodeJWT($token, 'header'); $this->assertSame([ 'typ' => 'JWT', 'alg' => 'HS256', @@ -147,7 +167,7 @@ public function testGenerateAddHeader(): void $this->assertIsString($token); - $headers = $this->decodeJWTHeader($token); + $headers = $this->decodeJWT($token, 'header'); $this->assertSame([ 'extra_key' => 'extra_value', 'typ' => 'JWT', @@ -155,8 +175,14 @@ public function testGenerateAddHeader(): void ], $headers); } - private function decodeJWTHeader(string $token): array + private function decodeJWT(string $token, $part): array { + $map = [ + 'header' => 0, + 'payload' => 1, + ]; + $index = $map[$part]; + return json_decode( base64_decode( str_replace( @@ -165,7 +191,7 @@ private function decodeJWTHeader(string $token): array str_replace( '-', '+', - explode('.', $token)[0] + explode('.', $token)[$index] ) ), true From a93f25436b8b495a098832c972924e4a4e3af542 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 18:19:53 +0900 Subject: [PATCH 072/142] docs: small fixes --- docs/addons/jwt.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 2614d0ac8..f0caa6069 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -92,13 +92,13 @@ use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; $generator = new JWTGenerator(); $user = auth()->user(); -$claim = [ +$claims = [ 'email' => $user->email, ]; -$token = $generator->generateAccessToken($user, $claim); +$token = $generator->generateAccessToken($user, $claims); ``` -It sets the `Config\AuthJWT::$defaultClaim` values to the token, and adds +It sets the `Config\AuthJWT::$defaultClaims` to the token, and adds the `'email'` claim and the user ID in the `"sub"` (subject) claim. It also sets `"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically if you don't specify. @@ -135,5 +135,6 @@ $token = $generator->generate($payload, DAY); It uses the `secret` and `alg` in the `Config\AuthJWT::$keys['default']`. -It sets `"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically -even if you don't pass them. +It sets the `Config\AuthJWT::$defaultClaims` to the token, and sets +`"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically even if +you don't pass them. From c041d85d1faa220685840239a9387336756997f2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 19:38:40 +0900 Subject: [PATCH 073/142] feat: support Asymmetric Key --- .../Authenticators/JWT/FirebaseAdapter.php | 24 +++++-- src/Config/AuthJWT.php | 11 ++-- .../Authenticators/JWTAuthenticatorTest.php | 65 +++++++++++++++++++ .../TokenGenerator/JWTGeneratorTest.php | 53 +++++++++++++++ 4 files changed, 144 insertions(+), 9 deletions(-) diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php index 028bb339c..b3d1adac1 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/Authenticators/JWT/FirebaseAdapter.php @@ -28,8 +28,9 @@ public static function decode(string $encodedToken, $key): stdClass $config = config(AuthJWT::class); $configKeys = $config->keys[$keyGroup]; + if (count($configKeys) === 1) { - $key = $configKeys[0]['secret']; + $key = $configKeys[0]['secret'] ?? $configKeys[0]['public']; $algorithm = $configKeys[0]['alg']; $keys = new Key($key, $algorithm); @@ -37,7 +38,7 @@ public static function decode(string $encodedToken, $key): stdClass $keys = []; foreach ($config->keys[$keyGroup] as $item) { - $key = $item['secret']; + $key = $item['secret'] ?? $item['public']; $algorithm = $item['alg']; $keys[$item['kid']] = new Key($key, $algorithm); @@ -63,8 +64,23 @@ public static function encode(array $payload, $key, ?array $headers = null): str { $keyGroup = $key; - $config = config(AuthJWT::class); - $key = $config->keys[$keyGroup][0]['secret']; + $config = config(AuthJWT::class); + + if (isset($config->keys[$keyGroup][0]['secret'])) { + $key = $config->keys[$keyGroup][0]['secret']; + } else { + $passphrase = $config->keys[$keyGroup][0]['passphrase'] ?? ''; + + if ($passphrase !== '') { + $key = openssl_pkey_get_private( + $config->keys[$keyGroup][0]['private'], + $passphrase + ); + } else { + $key = $config->keys[$keyGroup][0]['private']; + } + } + $algorithm = $config->keys[$keyGroup][0]['alg']; $keyId = $config->keys[$keyGroup][0]['kid'] ?? null; diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index d565b2327..eba822bcd 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -49,12 +49,13 @@ class AuthJWT extends BaseConfig // E.g., $ php -r 'echo base64_encode(random_bytes(32));' 'secret' => '', ], - // (Not implemented) Asymmetric Key + // Asymmetric Key // [ - // 'kid' => '', // Key ID. Optional if you have only one key. - // 'alg' => 'RS256', // algorithm. - // 'public' => '', // Public Key - // 'private' => '', // Private Key + // 'kid' => '', // Key ID. Optional if you have only one key. + // 'alg' => 'RS256', // algorithm. + // 'public' => '', // Public Key + // 'private' => '', // Private Key + // 'passphrase' => '' // Passphrase // ], ], ]; diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index b51f858a0..943b79bb9 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -306,4 +306,69 @@ public function testDecodeJWTCanSpecifyKey(): void $this->assertSame('1', $payload->user_id); } + + public function testDecodeJWTCanDecodeWithAsymmetricKey(): void + { + $token = $this->generateJWTWithAsymmetricKey(); + + $payload = $this->auth->decodeJWT($token); + + $this->assertSame('1', $payload->user_id); + } + + private function generateJWTWithAsymmetricKey(): string + { + $generator = new JWTGenerator(); + + $config = config(AuthJWT::class); + $config->keys['default'][0] = [ + 'alg' => 'RS256', // algorithm. + 'public' => <<<'EOD' + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzWHNM5f+amCjQztc5QT + fJfzCC5J4nuW+L/aOxZ4f8J3FrewM2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJ + hzkPYLae7bTVro3hok0zDITR8F6SJGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548t + u4czCuqU8BGVOlnp6IqBHhAswNMM78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vS + opcT51koWOgiTf3C7nJUoMWZHZI5HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTz + TTqo1SCSH2pooJl9O8at6kkRYsrZWwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/B + wQIDAQAB + -----END PUBLIC KEY----- + EOD, + 'private' => <<<'EOD' + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAuzWHNM5f+amCjQztc5QTfJfzCC5J4nuW+L/aOxZ4f8J3Frew + M2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJhzkPYLae7bTVro3hok0zDITR8F6S + JGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548tu4czCuqU8BGVOlnp6IqBHhAswNMM + 78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vSopcT51koWOgiTf3C7nJUoMWZHZI5 + HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTzTTqo1SCSH2pooJl9O8at6kkRYsrZ + WwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/BwQIDAQABAoIBAFtGaOqNKGwggn9k + 6yzr6GhZ6Wt2rh1Xpq8XUz514UBhPxD7dFRLpbzCrLVpzY80LbmVGJ9+1pJozyWc + VKeCeUdNwbqkr240Oe7GTFmGjDoxU+5/HX/SJYPpC8JZ9oqgEA87iz+WQX9hVoP2 + oF6EB4ckDvXmk8FMwVZW2l2/kd5mrEVbDaXKxhvUDf52iVD+sGIlTif7mBgR99/b + c3qiCnxCMmfYUnT2eh7Vv2LhCR/G9S6C3R4lA71rEyiU3KgsGfg0d82/XWXbegJW + h3QbWNtQLxTuIvLq5aAryV3PfaHlPgdgK0ft6ocU2de2FagFka3nfVEyC7IUsNTK + bq6nhAECgYEA7d/0DPOIaItl/8BWKyCuAHMss47j0wlGbBSHdJIiS55akMvnAG0M + 39y22Qqfzh1at9kBFeYeFIIU82ZLF3xOcE3z6pJZ4Dyvx4BYdXH77odo9uVK9s1l + 3T3BlMcqd1hvZLMS7dviyH79jZo4CXSHiKzc7pQ2YfK5eKxKqONeXuECgYEAyXlG + vonaus/YTb1IBei9HwaccnQ/1HRn6MvfDjb7JJDIBhNClGPt6xRlzBbSZ73c2QEC + 6Fu9h36K/HZ2qcLd2bXiNyhIV7b6tVKk+0Psoj0dL9EbhsD1OsmE1nTPyAc9XZbb + OPYxy+dpBCUA8/1U9+uiFoCa7mIbWcSQ+39gHuECgYAz82pQfct30aH4JiBrkNqP + nJfRq05UY70uk5k1u0ikLTRoVS/hJu/d4E1Kv4hBMqYCavFSwAwnvHUo51lVCr/y + xQOVYlsgnwBg2MX4+GjmIkqpSVCC8D7j/73MaWb746OIYZervQ8dbKahi2HbpsiG + 8AHcVSA/agxZr38qvWV54QKBgCD5TlDE8x18AuTGQ9FjxAAd7uD0kbXNz2vUYg9L + hFL5tyL3aAAtUrUUw4xhd9IuysRhW/53dU+FsG2dXdJu6CxHjlyEpUJl2iZu/j15 + YnMzGWHIEX8+eWRDsw/+Ujtko/B7TinGcWPz3cYl4EAOiCeDUyXnqnO1btCEUU44 + DJ1BAoGBAJuPD27ErTSVtId90+M4zFPNibFP50KprVdc8CR37BE7r8vuGgNYXmnI + RLnGP9p3pVgFCktORuYS2J/6t84I3+A17nEoB4xvhTLeAinAW/uTQOUmNicOP4Ek + 2MsLL2kHgL8bLTmvXV4FX+PXphrDKg1XxzOYn0otuoqdAQrkK4og + -----END RSA PRIVATE KEY----- + EOD, + ]; + + $payload = [ + 'user_id' => '1', + ]; + + return $generator->generate($payload, DAY); + } } diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php index c018ef1ab..8df34aab2 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php @@ -175,6 +175,59 @@ public function testGenerateAddHeader(): void ], $headers); } + public function testGenerateWithAsymmetricKey(): void + { + $generator = new JWTGenerator(); + + $config = config(AuthJWT::class); + $config->keys['default'][0] = [ + 'alg' => 'RS256', // algorithm. + 'public' => '', // Public Key + 'private' => <<<'EOD' + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAuzWHNM5f+amCjQztc5QTfJfzCC5J4nuW+L/aOxZ4f8J3Frew + M2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJhzkPYLae7bTVro3hok0zDITR8F6S + JGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548tu4czCuqU8BGVOlnp6IqBHhAswNMM + 78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vSopcT51koWOgiTf3C7nJUoMWZHZI5 + HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTzTTqo1SCSH2pooJl9O8at6kkRYsrZ + WwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/BwQIDAQABAoIBAFtGaOqNKGwggn9k + 6yzr6GhZ6Wt2rh1Xpq8XUz514UBhPxD7dFRLpbzCrLVpzY80LbmVGJ9+1pJozyWc + VKeCeUdNwbqkr240Oe7GTFmGjDoxU+5/HX/SJYPpC8JZ9oqgEA87iz+WQX9hVoP2 + oF6EB4ckDvXmk8FMwVZW2l2/kd5mrEVbDaXKxhvUDf52iVD+sGIlTif7mBgR99/b + c3qiCnxCMmfYUnT2eh7Vv2LhCR/G9S6C3R4lA71rEyiU3KgsGfg0d82/XWXbegJW + h3QbWNtQLxTuIvLq5aAryV3PfaHlPgdgK0ft6ocU2de2FagFka3nfVEyC7IUsNTK + bq6nhAECgYEA7d/0DPOIaItl/8BWKyCuAHMss47j0wlGbBSHdJIiS55akMvnAG0M + 39y22Qqfzh1at9kBFeYeFIIU82ZLF3xOcE3z6pJZ4Dyvx4BYdXH77odo9uVK9s1l + 3T3BlMcqd1hvZLMS7dviyH79jZo4CXSHiKzc7pQ2YfK5eKxKqONeXuECgYEAyXlG + vonaus/YTb1IBei9HwaccnQ/1HRn6MvfDjb7JJDIBhNClGPt6xRlzBbSZ73c2QEC + 6Fu9h36K/HZ2qcLd2bXiNyhIV7b6tVKk+0Psoj0dL9EbhsD1OsmE1nTPyAc9XZbb + OPYxy+dpBCUA8/1U9+uiFoCa7mIbWcSQ+39gHuECgYAz82pQfct30aH4JiBrkNqP + nJfRq05UY70uk5k1u0ikLTRoVS/hJu/d4E1Kv4hBMqYCavFSwAwnvHUo51lVCr/y + xQOVYlsgnwBg2MX4+GjmIkqpSVCC8D7j/73MaWb746OIYZervQ8dbKahi2HbpsiG + 8AHcVSA/agxZr38qvWV54QKBgCD5TlDE8x18AuTGQ9FjxAAd7uD0kbXNz2vUYg9L + hFL5tyL3aAAtUrUUw4xhd9IuysRhW/53dU+FsG2dXdJu6CxHjlyEpUJl2iZu/j15 + YnMzGWHIEX8+eWRDsw/+Ujtko/B7TinGcWPz3cYl4EAOiCeDUyXnqnO1btCEUU44 + DJ1BAoGBAJuPD27ErTSVtId90+M4zFPNibFP50KprVdc8CR37BE7r8vuGgNYXmnI + RLnGP9p3pVgFCktORuYS2J/6t84I3+A17nEoB4xvhTLeAinAW/uTQOUmNicOP4Ek + 2MsLL2kHgL8bLTmvXV4FX+PXphrDKg1XxzOYn0otuoqdAQrkK4og + -----END RSA PRIVATE KEY----- + EOD, + ]; + + $payload = [ + 'user_id' => '1', + ]; + $token = $generator->generate($payload, DAY); + + $this->assertIsString($token); + + $headers = $this->decodeJWT($token, 'header'); + $this->assertSame([ + 'typ' => 'JWT', + 'alg' => 'RS256', + ], $headers); + } + private function decodeJWT(string $token, $part): array { $map = [ From f043dadb85299891efd6c8e1d980fee65c530f66 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 16 Apr 2023 19:44:03 +0900 Subject: [PATCH 074/142] chore: add ext-openssl to suggest --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a0ee054ed..42ee9f2d5 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "codeigniter4/authentication-implementation": "1.0" }, "suggest": { - "ext-curl": "Required to use the password validation rule via PwnedValidator class." + "ext-curl": "Required to use the password validation rule via PwnedValidator class.", + "ext-openssl": "Required to use the JWT Authenticator." }, "minimum-stability": "dev", "prefer-stable": true, From 6a8efe688cc11cbe816ad817e5dcccd984119bf2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 08:12:05 +0900 Subject: [PATCH 075/142] refactor: change folder structure --- docs/addons/jwt.md | 4 ++-- src/Authentication/Authenticators/JWT.php | 4 ++-- .../JWT => JWT/Adapters}/FirebaseAdapter.php | 3 ++- .../{Authenticators => }/JWT/JWTAdapterInterface.php | 2 +- src/Authentication/{TokenGenerator => JWT}/JWTGenerator.php | 5 ++--- .../Authentication/Authenticators/JWTAuthenticatorTest.php | 2 +- tests/Authentication/Filters/JWTFilterTest.php | 2 +- .../JWT => JWT/Adapters}/FirebaseAdapaterTest.php | 6 +++--- .../{TokenGenerator => JWT}/JWTGeneratorTest.php | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) rename src/Authentication/{Authenticators/JWT => JWT/Adapters}/FirebaseAdapter.php (95%) rename src/Authentication/{Authenticators => }/JWT/JWTAdapterInterface.php (92%) rename src/Authentication/{TokenGenerator => JWT}/JWTGenerator.php (92%) rename tests/Unit/Authentication/{Authenticators/JWT => JWT/Adapters}/FirebaseAdapaterTest.php (91%) rename tests/Unit/Authentication/{TokenGenerator => JWT}/JWTGeneratorTest.php (98%) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index f0caa6069..b68f19628 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -87,7 +87,7 @@ public function generateAccessToken( The following code generates a JWT to the user. ```php -use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; $generator = new JWTGenerator(); @@ -122,7 +122,7 @@ public function generate( The following code generates a JWT. ```php -use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; $generator = new JWTGenerator(); diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 7197e50f1..d94fbd680 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -8,8 +8,8 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; -use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\Authenticators\JWT\JWTAdapterInterface; +use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; +use CodeIgniter\Shield\Authentication\JWT\JWTAdapterInterface; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; diff --git a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php similarity index 95% rename from src/Authentication/Authenticators/JWT/FirebaseAdapter.php rename to src/Authentication/JWT/Adapters/FirebaseAdapter.php index b3d1adac1..6c362b8b8 100644 --- a/src/Authentication/Authenticators/JWT/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace CodeIgniter\Shield\Authentication\Authenticators\JWT; +namespace CodeIgniter\Shield\Authentication\JWT\Adapters; +use CodeIgniter\Shield\Authentication\JWT\JWTAdapterInterface; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Exceptions\RuntimeException; use DomainException; diff --git a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php b/src/Authentication/JWT/JWTAdapterInterface.php similarity index 92% rename from src/Authentication/Authenticators/JWT/JWTAdapterInterface.php rename to src/Authentication/JWT/JWTAdapterInterface.php index 28768189a..57ce4ec4b 100644 --- a/src/Authentication/Authenticators/JWT/JWTAdapterInterface.php +++ b/src/Authentication/JWT/JWTAdapterInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace CodeIgniter\Shield\Authentication\Authenticators\JWT; +namespace CodeIgniter\Shield\Authentication\JWT; use stdClass; diff --git a/src/Authentication/TokenGenerator/JWTGenerator.php b/src/Authentication/JWT/JWTGenerator.php similarity index 92% rename from src/Authentication/TokenGenerator/JWTGenerator.php rename to src/Authentication/JWT/JWTGenerator.php index ecd137e75..4f239d677 100644 --- a/src/Authentication/TokenGenerator/JWTGenerator.php +++ b/src/Authentication/JWT/JWTGenerator.php @@ -2,11 +2,10 @@ declare(strict_types=1); -namespace CodeIgniter\Shield\Authentication\TokenGenerator; +namespace CodeIgniter\Shield\Authentication\JWT; use CodeIgniter\I18n\Time; -use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\Authenticators\JWT\JWTAdapterInterface; +use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 943b79bb9..d56c03243 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -8,7 +8,7 @@ use CodeIgniter\Shield\Authentication\Authentication; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\Authenticators\JWT; -use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; diff --git a/tests/Authentication/Filters/JWTFilterTest.php b/tests/Authentication/Filters/JWTFilterTest.php index efdf228f5..9b8a6dd98 100644 --- a/tests/Authentication/Filters/JWTFilterTest.php +++ b/tests/Authentication/Filters/JWTFilterTest.php @@ -5,7 +5,7 @@ namespace Tests\Authentication\Filters; use CodeIgniter\Config\Factories; -use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Filters\JWTAuth; use CodeIgniter\Shield\Models\UserModel; diff --git a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php similarity index 91% rename from tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php rename to tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index 768cd6985..062fecbea 100644 --- a/tests/Unit/Authentication/Authenticators/JWT/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Tests\Unit\Authentication\Authenticators\JWT; +namespace Tests\Unit\Authentication\JWT\Adapters; use CodeIgniter\I18n\Time; -use CodeIgniter\Shield\Authentication\Authenticators\JWT\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; +use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; diff --git a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php b/tests/Unit/Authentication/JWT/JWTGeneratorTest.php similarity index 98% rename from tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php rename to tests/Unit/Authentication/JWT/JWTGeneratorTest.php index 8df34aab2..63779d400 100644 --- a/tests/Unit/Authentication/TokenGenerator/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/JWT/JWTGeneratorTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Tests\Unit\Authentication\TokenGenerator; +namespace Tests\Unit\Authentication\JWT; use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\JWT; -use CodeIgniter\Shield\Authentication\TokenGenerator\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Models\UserModel; From f71eaa029c7d29fa6210a2a2682e8f5b024a9ba3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 11:03:08 +0900 Subject: [PATCH 076/142] refactor: rename $key to $keyset --- docs/addons/jwt.md | 4 +-- src/Authentication/Authenticators/JWT.php | 10 +++---- .../JWT/Adapters/FirebaseAdapter.php | 26 ++++++++----------- .../JWT/JWTAdapterInterface.php | 8 +++--- src/Authentication/JWT/JWTGenerator.php | 12 ++++----- .../Authenticators/JWTAuthenticatorTest.php | 2 +- 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index b68f19628..62ecd715a 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -79,7 +79,7 @@ public function generateAccessToken( User $user, array $claims = [], ?int $ttl = null, - $key = 'default', + $keyset = 'default', ?array $headers = null ): string ``` @@ -114,7 +114,7 @@ It takes a JWT claims array, and can take time to live in seconds, a key group public function generate( array $claims, ?int $ttl = null, - $key = 'default', + $keyset = 'default', ?array $headers = null ): string ``` diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index d94fbd680..891a724b9 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -44,7 +44,7 @@ class JWT implements AuthenticatorInterface /** * @var string The key group. The array key of Config\AuthJWT::$keys. */ - protected $key = 'default'; + protected $keyset = 'default'; public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtAdapter = null) { @@ -239,11 +239,11 @@ public function recordActiveDate(): void } /** - * @param string $key The key group. The array key of Config\AuthJWT::$keys. + * @param string $keyset The key group. The array key of Config\AuthJWT::$keys. */ - public function setKey($key): void + public function setKeyset($keyset): void { - $this->key = $key; + $this->keyset = $keyset; } /** @@ -251,7 +251,7 @@ public function setKey($key): void */ public function decodeJWT(string $encodedToken): stdClass { - return $this->jwtAdapter->decode($encodedToken, $this->key); + return $this->jwtAdapter->decode($encodedToken, $this->keyset); } /** diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index 6c362b8b8..5b3574239 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -22,13 +22,11 @@ class FirebaseAdapter implements JWTAdapterInterface /** * {@inheritDoc} */ - public static function decode(string $encodedToken, $key): stdClass + public static function decode(string $encodedToken, $keyset): stdClass { - $keyGroup = $key; - $config = config(AuthJWT::class); - $configKeys = $config->keys[$keyGroup]; + $configKeys = $config->keys[$keyset]; if (count($configKeys) === 1) { $key = $configKeys[0]['secret'] ?? $configKeys[0]['public']; @@ -38,7 +36,7 @@ public static function decode(string $encodedToken, $key): stdClass } else { $keys = []; - foreach ($config->keys[$keyGroup] as $item) { + foreach ($config->keys[$keyset] as $item) { $key = $item['secret'] ?? $item['public']; $algorithm = $item['alg']; @@ -61,30 +59,28 @@ public static function decode(string $encodedToken, $key): stdClass /** * {@inheritDoc} */ - public static function encode(array $payload, $key, ?array $headers = null): string + public static function encode(array $payload, $keyset, ?array $headers = null): string { - $keyGroup = $key; - $config = config(AuthJWT::class); - if (isset($config->keys[$keyGroup][0]['secret'])) { - $key = $config->keys[$keyGroup][0]['secret']; + if (isset($config->keys[$keyset][0]['secret'])) { + $key = $config->keys[$keyset][0]['secret']; } else { - $passphrase = $config->keys[$keyGroup][0]['passphrase'] ?? ''; + $passphrase = $config->keys[$keyset][0]['passphrase'] ?? ''; if ($passphrase !== '') { $key = openssl_pkey_get_private( - $config->keys[$keyGroup][0]['private'], + $config->keys[$keyset][0]['private'], $passphrase ); } else { - $key = $config->keys[$keyGroup][0]['private']; + $key = $config->keys[$keyset][0]['private']; } } - $algorithm = $config->keys[$keyGroup][0]['alg']; + $algorithm = $config->keys[$keyset][0]['alg']; - $keyId = $config->keys[$keyGroup][0]['kid'] ?? null; + $keyId = $config->keys[$keyset][0]['kid'] ?? null; if ($keyId === '') { $keyId = null; } diff --git a/src/Authentication/JWT/JWTAdapterInterface.php b/src/Authentication/JWT/JWTAdapterInterface.php index 57ce4ec4b..dc7f263e2 100644 --- a/src/Authentication/JWT/JWTAdapterInterface.php +++ b/src/Authentication/JWT/JWTAdapterInterface.php @@ -12,7 +12,7 @@ interface JWTAdapterInterface * Issues JWT (JWS) * * @param array $payload The payload. - * @param string $key The key group. + * @param string $keyset The key group. * The array key of Config\AuthJWT::$keys. * @param array|null $headers An array with header elements to attach. * @@ -20,16 +20,16 @@ interface JWTAdapterInterface */ public static function encode( array $payload, - $key, + $keyset, ?array $headers = null ): string; /** * Decode JWT (JWS) * - * @param string $key The key group. The array key of Config\AuthJWT::$keys. + * @param string $keyset The key group. The array key of Config\AuthJWT::$keys. * * @return stdClass Payload */ - public static function decode(string $encodedToken, $key): stdClass; + public static function decode(string $encodedToken, $keyset): stdClass; } diff --git a/src/Authentication/JWT/JWTGenerator.php b/src/Authentication/JWT/JWTGenerator.php index 4f239d677..12b038c99 100644 --- a/src/Authentication/JWT/JWTGenerator.php +++ b/src/Authentication/JWT/JWTGenerator.php @@ -25,7 +25,7 @@ public function __construct(?Time $clock = null, ?JWTAdapterInterface $jwtAdapte * * @param array $claims The payload items. * @param int|null $ttl Time to live in seconds. - * @param string $key The key group. + * @param string $keyset The key group. * The array key of Config\AuthJWT::$keys. * @param array|null $headers An array with header elements to attach. */ @@ -33,7 +33,7 @@ public function generateAccessToken( User $user, array $claims = [], ?int $ttl = null, - $key = 'default', + $keyset = 'default', ?array $headers = null ): string { $payload = array_merge( @@ -43,7 +43,7 @@ public function generateAccessToken( ], ); - return $this->generate($payload, $ttl, $key, $headers); + return $this->generate($payload, $ttl, $keyset, $headers); } /** @@ -51,14 +51,14 @@ public function generateAccessToken( * * @param array $claims The payload items. * @param int|null $ttl Time to live in seconds. - * @param string $key The key group. + * @param string $keyset The key group. * The array key of Config\AuthJWT::$keys. * @param array|null $headers An array with header elements to attach. */ public function generate( array $claims, ?int $ttl = null, - $key = 'default', + $keyset = 'default', ?array $headers = null ): string { assert( @@ -87,7 +87,7 @@ public function generate( return $this->jwtAdapter->encode( $payload, - $key, + $keyset, $headers ); } diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index d56c03243..fb4bb69ce 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -301,7 +301,7 @@ public function testDecodeJWTCanSpecifyKey(): void ]; $token = $generator->generate($payload, DAY, 'mobile'); - $this->auth->setKey('mobile'); + $this->auth->setKeyset('mobile'); $payload = $this->auth->decodeJWT($token); $this->assertSame('1', $payload->user_id); From 340b1441290e98c22601fedff107a1a0c623e3a3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 11:11:46 +0900 Subject: [PATCH 077/142] refactor: rename JWT to JWS in classnames It means the class is used for Signed JWT. --- docs/addons/jwt.md | 8 ++++---- src/Authentication/Authenticators/JWT.php | 6 +++--- .../JWT/Adapters/FirebaseAdapter.php | 4 ++-- ...pterInterface.php => JWSAdapterInterface.php} | 6 +++--- .../JWT/{JWTGenerator.php => JWSGenerator.php} | 13 ++++++++----- .../Authenticators/JWTAuthenticatorTest.php | 10 +++++----- tests/Authentication/Filters/JWTFilterTest.php | 4 ++-- .../JWT/Adapters/FirebaseAdapaterTest.php | 4 ++-- ...JWTGeneratorTest.php => JWSGeneratorTest.php} | 16 ++++++++-------- 9 files changed, 37 insertions(+), 34 deletions(-) rename src/Authentication/JWT/{JWTAdapterInterface.php => JWSAdapterInterface.php} (89%) rename src/Authentication/JWT/{JWTGenerator.php => JWSGenerator.php} (91%) rename tests/Unit/Authentication/JWT/{JWTGeneratorTest.php => JWSGeneratorTest.php} (95%) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 62ecd715a..37ac7e94e 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -87,9 +87,9 @@ public function generateAccessToken( The following code generates a JWT to the user. ```php -use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; -$generator = new JWTGenerator(); +$generator = new JWSGenerator(); $user = auth()->user(); $claims = [ @@ -122,9 +122,9 @@ public function generate( The following code generates a JWT. ```php -use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; -$generator = new JWTGenerator(); +$generator = new JWSGenerator(); $payload = [ 'user_id' => '1', diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 891a724b9..a1185bc51 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -9,7 +9,7 @@ use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\JWT\JWTAdapterInterface; +use CodeIgniter\Shield\Authentication\JWT\JWSAdapterInterface; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; @@ -37,7 +37,7 @@ class JWT implements AuthenticatorInterface protected UserModel $provider; protected ?User $user = null; - protected JWTAdapterInterface $jwtAdapter; + protected JWSAdapterInterface $jwtAdapter; protected TokenLoginModel $tokenLoginModel; protected ?stdClass $payload = null; @@ -46,7 +46,7 @@ class JWT implements AuthenticatorInterface */ protected $keyset = 'default'; - public function __construct(UserModel $provider, ?JWTAdapterInterface $jwtAdapter = null) + public function __construct(UserModel $provider, ?JWSAdapterInterface $jwtAdapter = null) { $this->provider = $provider; $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index 5b3574239..c2e518868 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -4,7 +4,7 @@ namespace CodeIgniter\Shield\Authentication\JWT\Adapters; -use CodeIgniter\Shield\Authentication\JWT\JWTAdapterInterface; +use CodeIgniter\Shield\Authentication\JWT\JWSAdapterInterface; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Exceptions\RuntimeException; use DomainException; @@ -17,7 +17,7 @@ use stdClass; use UnexpectedValueException; -class FirebaseAdapter implements JWTAdapterInterface +class FirebaseAdapter implements JWSAdapterInterface { /** * {@inheritDoc} diff --git a/src/Authentication/JWT/JWTAdapterInterface.php b/src/Authentication/JWT/JWSAdapterInterface.php similarity index 89% rename from src/Authentication/JWT/JWTAdapterInterface.php rename to src/Authentication/JWT/JWSAdapterInterface.php index dc7f263e2..5cdc03171 100644 --- a/src/Authentication/JWT/JWTAdapterInterface.php +++ b/src/Authentication/JWT/JWSAdapterInterface.php @@ -6,10 +6,10 @@ use stdClass; -interface JWTAdapterInterface +interface JWSAdapterInterface { /** - * Issues JWT (JWS) + * Issues Signed JWT (JWS) * * @param array $payload The payload. * @param string $keyset The key group. @@ -25,7 +25,7 @@ public static function encode( ): string; /** - * Decode JWT (JWS) + * Decode Signed JWT (JWS) * * @param string $keyset The key group. The array key of Config\AuthJWT::$keys. * diff --git a/src/Authentication/JWT/JWTGenerator.php b/src/Authentication/JWT/JWSGenerator.php similarity index 91% rename from src/Authentication/JWT/JWTGenerator.php rename to src/Authentication/JWT/JWSGenerator.php index 12b038c99..66ef6dd9e 100644 --- a/src/Authentication/JWT/JWTGenerator.php +++ b/src/Authentication/JWT/JWSGenerator.php @@ -9,19 +9,22 @@ use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; -class JWTGenerator +/** + * Issues Signed JWT + */ +class JWSGenerator { private Time $clock; - private JWTAdapterInterface $jwtAdapter; + private JWSAdapterInterface $jwtAdapter; - public function __construct(?Time $clock = null, ?JWTAdapterInterface $jwtAdapter = null) + public function __construct(?Time $clock = null, ?JWSAdapterInterface $jwtAdapter = null) { $this->clock = $clock ?? new Time(); $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); } /** - * Issues JWT (JWS) for a User + * Issues Signed JWT (JWS) for a User * * @param array $claims The payload items. * @param int|null $ttl Time to live in seconds. @@ -47,7 +50,7 @@ public function generateAccessToken( } /** - * Issues JWT (JWS) + * Issues Signed JWT (JWS) * * @param array $claims The payload items. * @param int|null $ttl Time to live in seconds. diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index fb4bb69ce..7d5e47263 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -8,7 +8,7 @@ use CodeIgniter\Shield\Authentication\Authentication; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\Authenticators\JWT; -use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; @@ -241,7 +241,7 @@ private function generateJWT(?Time $clock = null): string { $this->user = \fake(UserModel::class, ['id' => 1, 'username' => 'John Smith']); - $generator = new JWTGenerator($clock); + $generator = new JWSGenerator($clock); return $generator->generateAccessToken($this->user); } @@ -258,7 +258,7 @@ public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void ]; // Generate token with Key01. - $generator = new JWTGenerator(); + $generator = new JWSGenerator(); $payload = [ 'user_id' => '1', ]; @@ -295,7 +295,7 @@ public function testDecodeJWTCanSpecifyKey(): void ]; // Generate token with the mobile key. - $generator = new JWTGenerator(); + $generator = new JWSGenerator(); $payload = [ 'user_id' => '1', ]; @@ -318,7 +318,7 @@ public function testDecodeJWTCanDecodeWithAsymmetricKey(): void private function generateJWTWithAsymmetricKey(): string { - $generator = new JWTGenerator(); + $generator = new JWSGenerator(); $config = config(AuthJWT::class); $config->keys['default'][0] = [ diff --git a/tests/Authentication/Filters/JWTFilterTest.php b/tests/Authentication/Filters/JWTFilterTest.php index 9b8a6dd98..5de36d5cf 100644 --- a/tests/Authentication/Filters/JWTFilterTest.php +++ b/tests/Authentication/Filters/JWTFilterTest.php @@ -5,7 +5,7 @@ namespace Tests\Authentication\Filters; use CodeIgniter\Config\Factories; -use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Filters\JWTAuth; use CodeIgniter\Shield\Models\UserModel; @@ -66,7 +66,7 @@ public function testFilterSuccess(): void /** @var User $user */ $user = \fake(UserModel::class); - $generator = new JWTGenerator(); + $generator = new JWSGenerator(); $token = $generator->generateAccessToken($user); $result = $this->withHeaders(['Authorization' => 'Bearer ' . $token]) diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index 062fecbea..5e7bf32c3 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -6,7 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; @@ -43,7 +43,7 @@ public static function generateJWT(?Time $clock = null): string /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); - $generator = new JWTGenerator($clock); + $generator = new JWSGenerator($clock); return $generator->generateAccessToken($user); } diff --git a/tests/Unit/Authentication/JWT/JWTGeneratorTest.php b/tests/Unit/Authentication/JWT/JWSGeneratorTest.php similarity index 95% rename from tests/Unit/Authentication/JWT/JWTGeneratorTest.php rename to tests/Unit/Authentication/JWT/JWSGeneratorTest.php index 63779d400..2afb18f8f 100644 --- a/tests/Unit/Authentication/JWT/JWTGeneratorTest.php +++ b/tests/Unit/Authentication/JWT/JWSGeneratorTest.php @@ -6,7 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\JWT; -use CodeIgniter\Shield\Authentication\JWT\JWTGenerator; +use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Models\UserModel; @@ -15,7 +15,7 @@ /** * @internal */ -final class JWTGeneratorTest extends TestCase +final class JWSGeneratorTest extends TestCase { public function testGenerateAccessToken() { @@ -26,7 +26,7 @@ public function testGenerateAccessToken() Time::setTestNow('now'); $clock = new Time(); - $generator = new JWTGenerator($clock); + $generator = new JWSGenerator($clock); $currentTime = $clock->now(); @@ -67,7 +67,7 @@ public function testGenerateAccessTokenAddClaims(): void /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); - $generator = new JWTGenerator(); + $generator = new JWSGenerator(); $claims = [ 'email' => 'admin@example.jp', @@ -88,7 +88,7 @@ public function testGenerate() Time::setTestNow('now'); $clock = new Time(); - $generator = new JWTGenerator($clock); + $generator = new JWSGenerator($clock); $currentTime = $clock->now(); @@ -132,7 +132,7 @@ public function testGeneratePayload(array $data): void public function testGenerateSetKid(): void { - $generator = new JWTGenerator(); + $generator = new JWSGenerator(); // Set kid $config = config(AuthJWT::class); @@ -155,7 +155,7 @@ public function testGenerateSetKid(): void public function testGenerateAddHeader(): void { - $generator = new JWTGenerator(); + $generator = new JWSGenerator(); $payload = [ 'user_id' => '1', @@ -177,7 +177,7 @@ public function testGenerateAddHeader(): void public function testGenerateWithAsymmetricKey(): void { - $generator = new JWTGenerator(); + $generator = new JWSGenerator(); $config = config(AuthJWT::class); $config->keys['default'][0] = [ From cb1b22781385dc26c5e587cc33b4408d10be3331 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 11:41:13 +0900 Subject: [PATCH 078/142] refactor: move JWSGenerator up --- docs/addons/jwt.md | 4 ++-- src/Authentication/{JWT => }/JWSGenerator.php | 3 ++- tests/Authentication/Authenticators/JWTAuthenticatorTest.php | 2 +- tests/Authentication/Filters/JWTFilterTest.php | 2 +- .../Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php | 2 +- tests/Unit/Authentication/JWT/JWSGeneratorTest.php | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) rename src/Authentication/{JWT => }/JWSGenerator.php (96%) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 37ac7e94e..b04459626 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -87,7 +87,7 @@ public function generateAccessToken( The following code generates a JWT to the user. ```php -use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWSGenerator; $generator = new JWSGenerator(); @@ -122,7 +122,7 @@ public function generate( The following code generates a JWT. ```php -use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWSGenerator; $generator = new JWSGenerator(); diff --git a/src/Authentication/JWT/JWSGenerator.php b/src/Authentication/JWSGenerator.php similarity index 96% rename from src/Authentication/JWT/JWSGenerator.php rename to src/Authentication/JWSGenerator.php index 66ef6dd9e..c31e26cb0 100644 --- a/src/Authentication/JWT/JWSGenerator.php +++ b/src/Authentication/JWSGenerator.php @@ -2,10 +2,11 @@ declare(strict_types=1); -namespace CodeIgniter\Shield\Authentication\JWT; +namespace CodeIgniter\Shield\Authentication; use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; +use CodeIgniter\Shield\Authentication\JWT\JWSAdapterInterface; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 7d5e47263..9ad262764 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -8,7 +8,7 @@ use CodeIgniter\Shield\Authentication\Authentication; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\Authenticators\JWT; -use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWSGenerator; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; diff --git a/tests/Authentication/Filters/JWTFilterTest.php b/tests/Authentication/Filters/JWTFilterTest.php index 5de36d5cf..1cf28c0b4 100644 --- a/tests/Authentication/Filters/JWTFilterTest.php +++ b/tests/Authentication/Filters/JWTFilterTest.php @@ -5,7 +5,7 @@ namespace Tests\Authentication\Filters; use CodeIgniter\Config\Factories; -use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWSGenerator; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Filters\JWTAuth; use CodeIgniter\Shield\Models\UserModel; diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index 5e7bf32c3..db5dcd718 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -5,8 +5,8 @@ namespace Tests\Unit\Authentication\JWT\Adapters; use CodeIgniter\I18n\Time; +use CodeIgniter\Shield\Authentication\JWSGenerator; use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; diff --git a/tests/Unit/Authentication/JWT/JWSGeneratorTest.php b/tests/Unit/Authentication/JWT/JWSGeneratorTest.php index 2afb18f8f..16c5d2c29 100644 --- a/tests/Unit/Authentication/JWT/JWSGeneratorTest.php +++ b/tests/Unit/Authentication/JWT/JWSGeneratorTest.php @@ -6,7 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\JWT; -use CodeIgniter\Shield\Authentication\JWT\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWSGenerator; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Models\UserModel; From 0c9fc38a654e86e66019178113af4e0082fed2d3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 11:52:35 +0900 Subject: [PATCH 079/142] refactor: update peremeter/property name --- src/Authentication/JWSGenerator.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Authentication/JWSGenerator.php b/src/Authentication/JWSGenerator.php index c31e26cb0..50af08c16 100644 --- a/src/Authentication/JWSGenerator.php +++ b/src/Authentication/JWSGenerator.php @@ -16,12 +16,12 @@ class JWSGenerator { private Time $clock; - private JWSAdapterInterface $jwtAdapter; + private JWSAdapterInterface $jwsAdapter; - public function __construct(?Time $clock = null, ?JWSAdapterInterface $jwtAdapter = null) + public function __construct(?Time $clock = null, ?JWSAdapterInterface $jwsAdapter = null) { $this->clock = $clock ?? new Time(); - $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); + $this->jwsAdapter = $jwsAdapter ?? new FirebaseAdapter(); } /** @@ -89,7 +89,7 @@ public function generate( $payload['exp'] = $payload['iat'] + $ttl; } - return $this->jwtAdapter->encode( + return $this->jwsAdapter->encode( $payload, $keyset, $headers From 79dbac21c4ee9ed40d0355054f44a490e2f97d43 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 12:13:42 +0900 Subject: [PATCH 080/142] refactor: rename JWSGenerator to JWTManager --- docs/addons/jwt.md | 16 ++++++++-------- .../{JWSGenerator.php => JWTManager.php} | 4 ++-- .../Authenticators/JWTAuthenticatorTest.php | 10 +++++----- tests/Authentication/Filters/JWTFilterTest.php | 4 ++-- .../JWT/Adapters/FirebaseAdapaterTest.php | 4 ++-- .../{JWSGeneratorTest.php => JWTManagerTest.php} | 16 ++++++++-------- 6 files changed, 27 insertions(+), 27 deletions(-) rename src/Authentication/{JWSGenerator.php => JWTManager.php} (98%) rename tests/Unit/Authentication/JWT/{JWSGeneratorTest.php => JWTManagerTest.php} (95%) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index b04459626..4df3c7747 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -68,7 +68,7 @@ php -r 'echo base64_encode(random_bytes(32));' ### JWT to a Specific User -JWTs are created through the `JWTGenerator::generateAccessToken()` method. +JWTs are created through the `JWTManager::generateAccessToken()` method. This takes a User object to give to the token as the first argument. It can also take optional additional claims array, time to live in seconds, a key group (an array key) in the `Config\AuthJWT::$keys`, and additional header @@ -87,15 +87,15 @@ public function generateAccessToken( The following code generates a JWT to the user. ```php -use CodeIgniter\Shield\Authentication\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWTManager; -$generator = new JWSGenerator(); +$jwt = new JWTManager(); $user = auth()->user(); $claims = [ 'email' => $user->email, ]; -$token = $generator->generateAccessToken($user, $claims); +$token = $jwt->generateAccessToken($user, $claims); ``` It sets the `Config\AuthJWT::$defaultClaims` to the token, and adds @@ -105,7 +105,7 @@ if you don't specify. ### Arbitrary JWT -You can generate arbitrary JWT with the ``JWTGenerator::generate()`` method. +You can generate arbitrary JWT with the ``JWTManager::generate()`` method. It takes a JWT claims array, and can take time to live in seconds, a key group (an array key) in the `Config\AuthJWT::$keys`, and additional header array: @@ -122,15 +122,15 @@ public function generate( The following code generates a JWT. ```php -use CodeIgniter\Shield\Authentication\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWTManager; -$generator = new JWSGenerator(); +$jwt = new JWTManager(); $payload = [ 'user_id' => '1', 'email' => 'admin@example.jp', ]; -$token = $generator->generate($payload, DAY); +$token = $jwt->generate($payload, DAY); ``` It uses the `secret` and `alg` in the `Config\AuthJWT::$keys['default']`. diff --git a/src/Authentication/JWSGenerator.php b/src/Authentication/JWTManager.php similarity index 98% rename from src/Authentication/JWSGenerator.php rename to src/Authentication/JWTManager.php index 50af08c16..1759f883a 100644 --- a/src/Authentication/JWSGenerator.php +++ b/src/Authentication/JWTManager.php @@ -11,9 +11,9 @@ use CodeIgniter\Shield\Entities\User; /** - * Issues Signed JWT + * JWT Manager */ -class JWSGenerator +class JWTManager { private Time $clock; private JWSAdapterInterface $jwsAdapter; diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 9ad262764..63b1e7a16 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -8,7 +8,7 @@ use CodeIgniter\Shield\Authentication\Authentication; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\Authenticators\JWT; -use CodeIgniter\Shield\Authentication\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; @@ -241,7 +241,7 @@ private function generateJWT(?Time $clock = null): string { $this->user = \fake(UserModel::class, ['id' => 1, 'username' => 'John Smith']); - $generator = new JWSGenerator($clock); + $generator = new JWTManager($clock); return $generator->generateAccessToken($this->user); } @@ -258,7 +258,7 @@ public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void ]; // Generate token with Key01. - $generator = new JWSGenerator(); + $generator = new JWTManager(); $payload = [ 'user_id' => '1', ]; @@ -295,7 +295,7 @@ public function testDecodeJWTCanSpecifyKey(): void ]; // Generate token with the mobile key. - $generator = new JWSGenerator(); + $generator = new JWTManager(); $payload = [ 'user_id' => '1', ]; @@ -318,7 +318,7 @@ public function testDecodeJWTCanDecodeWithAsymmetricKey(): void private function generateJWTWithAsymmetricKey(): string { - $generator = new JWSGenerator(); + $generator = new JWTManager(); $config = config(AuthJWT::class); $config->keys['default'][0] = [ diff --git a/tests/Authentication/Filters/JWTFilterTest.php b/tests/Authentication/Filters/JWTFilterTest.php index 1cf28c0b4..49855cb5a 100644 --- a/tests/Authentication/Filters/JWTFilterTest.php +++ b/tests/Authentication/Filters/JWTFilterTest.php @@ -5,7 +5,7 @@ namespace Tests\Authentication\Filters; use CodeIgniter\Config\Factories; -use CodeIgniter\Shield\Authentication\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Filters\JWTAuth; use CodeIgniter\Shield\Models\UserModel; @@ -66,7 +66,7 @@ public function testFilterSuccess(): void /** @var User $user */ $user = \fake(UserModel::class); - $generator = new JWSGenerator(); + $generator = new JWTManager(); $token = $generator->generateAccessToken($user); $result = $this->withHeaders(['Authorization' => 'Bearer ' . $token]) diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index db5dcd718..176d3e886 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -5,8 +5,8 @@ namespace Tests\Unit\Authentication\JWT\Adapters; use CodeIgniter\I18n\Time; -use CodeIgniter\Shield\Authentication\JWSGenerator; use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; +use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\RuntimeException; @@ -43,7 +43,7 @@ public static function generateJWT(?Time $clock = null): string /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); - $generator = new JWSGenerator($clock); + $generator = new JWTManager($clock); return $generator->generateAccessToken($user); } diff --git a/tests/Unit/Authentication/JWT/JWSGeneratorTest.php b/tests/Unit/Authentication/JWT/JWTManagerTest.php similarity index 95% rename from tests/Unit/Authentication/JWT/JWSGeneratorTest.php rename to tests/Unit/Authentication/JWT/JWTManagerTest.php index 16c5d2c29..5c21871d8 100644 --- a/tests/Unit/Authentication/JWT/JWSGeneratorTest.php +++ b/tests/Unit/Authentication/JWT/JWTManagerTest.php @@ -6,7 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\JWT; -use CodeIgniter\Shield\Authentication\JWSGenerator; +use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Models\UserModel; @@ -15,7 +15,7 @@ /** * @internal */ -final class JWSGeneratorTest extends TestCase +final class JWTManagerTest extends TestCase { public function testGenerateAccessToken() { @@ -26,7 +26,7 @@ public function testGenerateAccessToken() Time::setTestNow('now'); $clock = new Time(); - $generator = new JWSGenerator($clock); + $generator = new JWTManager($clock); $currentTime = $clock->now(); @@ -67,7 +67,7 @@ public function testGenerateAccessTokenAddClaims(): void /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); - $generator = new JWSGenerator(); + $generator = new JWTManager(); $claims = [ 'email' => 'admin@example.jp', @@ -88,7 +88,7 @@ public function testGenerate() Time::setTestNow('now'); $clock = new Time(); - $generator = new JWSGenerator($clock); + $generator = new JWTManager($clock); $currentTime = $clock->now(); @@ -132,7 +132,7 @@ public function testGeneratePayload(array $data): void public function testGenerateSetKid(): void { - $generator = new JWSGenerator(); + $generator = new JWTManager(); // Set kid $config = config(AuthJWT::class); @@ -155,7 +155,7 @@ public function testGenerateSetKid(): void public function testGenerateAddHeader(): void { - $generator = new JWSGenerator(); + $generator = new JWTManager(); $payload = [ 'user_id' => '1', @@ -177,7 +177,7 @@ public function testGenerateAddHeader(): void public function testGenerateWithAsymmetricKey(): void { - $generator = new JWSGenerator(); + $generator = new JWTManager(); $config = config(AuthJWT::class); $config->keys['default'][0] = [ From b9fdc6626529e8ecc99c80263340e702b2afe3bb Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 13:10:51 +0900 Subject: [PATCH 081/142] refactor: extract JWSEncoder class --- src/Authentication/JWT/JWSEncoder.php | 67 +++++++++++++++++++++++++++ src/Authentication/JWTManager.php | 44 ++++-------------- 2 files changed, 75 insertions(+), 36 deletions(-) create mode 100644 src/Authentication/JWT/JWSEncoder.php diff --git a/src/Authentication/JWT/JWSEncoder.php b/src/Authentication/JWT/JWSEncoder.php new file mode 100644 index 000000000..8407d5d12 --- /dev/null +++ b/src/Authentication/JWT/JWSEncoder.php @@ -0,0 +1,67 @@ +clock = $clock ?? new Time(); + $this->jwsAdapter = $jwsAdapter ?? new FirebaseAdapter(); + } + + /** + * Issues Signed JWT (JWS) + * + * @param array $claims The payload items. + * @param int|null $ttl Time to live in seconds. + * @param string $keyset The key group. + * The array key of Config\AuthJWT::$keys. + * @param array|null $headers An array with header elements to attach. + */ + public function encode( + array $claims, + ?int $ttl = null, + $keyset = 'default', + ?array $headers = null + ): string { + assert( + (array_key_exists('exp', $claims) && ($ttl !== null)) === false, + 'Cannot pass $claims[\'exp\'] and $ttl at the same time.' + ); + + $config = config(AuthJWT::class); + + $payload = array_merge( + $config->defaultClaims, + $claims + ); + + if (! array_key_exists('iat', $claims)) { + $payload['iat'] = $this->clock->now()->getTimestamp(); + } + + if (! array_key_exists('exp', $claims)) { + $payload['exp'] = $payload['iat'] + $config->timeToLive; + } + + if ($ttl !== null) { + $payload['exp'] = $payload['iat'] + $ttl; + } + + return $this->jwsAdapter->encode( + $payload, + $keyset, + $headers + ); + } +} diff --git a/src/Authentication/JWTManager.php b/src/Authentication/JWTManager.php index 1759f883a..790b2c7a9 100644 --- a/src/Authentication/JWTManager.php +++ b/src/Authentication/JWTManager.php @@ -5,9 +5,7 @@ namespace CodeIgniter\Shield\Authentication; use CodeIgniter\I18n\Time; -use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\JWT\JWSAdapterInterface; -use CodeIgniter\Shield\Config\AuthJWT; +use CodeIgniter\Shield\Authentication\JWT\JWSEncoder; use CodeIgniter\Shield\Entities\User; /** @@ -16,12 +14,14 @@ class JWTManager { private Time $clock; - private JWSAdapterInterface $jwsAdapter; + private JWSEncoder $jwsEncoder; - public function __construct(?Time $clock = null, ?JWSAdapterInterface $jwsAdapter = null) - { + public function __construct( + ?Time $clock = null, + ?JWSEncoder $jwsEncoder = null + ) { $this->clock = $clock ?? new Time(); - $this->jwsAdapter = $jwsAdapter ?? new FirebaseAdapter(); + $this->jwsEncoder = $jwsEncoder ?? new JWSEncoder($this->clock); } /** @@ -65,34 +65,6 @@ public function generate( $keyset = 'default', ?array $headers = null ): string { - assert( - (array_key_exists('exp', $claims) && ($ttl !== null)) === false, - 'Cannot pass $claims[\'exp\'] and $ttl at the same time.' - ); - - $config = config(AuthJWT::class); - - $payload = array_merge( - $config->defaultClaims, - $claims - ); - - if (! array_key_exists('iat', $claims)) { - $payload['iat'] = $this->clock->now()->getTimestamp(); - } - - if (! array_key_exists('exp', $claims)) { - $payload['exp'] = $payload['iat'] + $config->timeToLive; - } - - if ($ttl !== null) { - $payload['exp'] = $payload['iat'] + $ttl; - } - - return $this->jwsAdapter->encode( - $payload, - $keyset, - $headers - ); + return $this->jwsEncoder->encode($claims, $ttl, $keyset, $headers); } } From d3cf8162419f828377b4ac495787f7906ac4e37b Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 13:43:24 +0900 Subject: [PATCH 082/142] feat: add JWSDecoder --- src/Authentication/JWT/JWSDecoder.php | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/Authentication/JWT/JWSDecoder.php diff --git a/src/Authentication/JWT/JWSDecoder.php b/src/Authentication/JWT/JWSDecoder.php new file mode 100644 index 000000000..3ba548ac6 --- /dev/null +++ b/src/Authentication/JWT/JWSDecoder.php @@ -0,0 +1,33 @@ +jwsAdapter = $jwsAdapter ?? new FirebaseAdapter(); + } + + /** + * Returns payload of the JWT + * + * @param string $keyset The key group. The array key of Config\AuthJWT::$keys. + */ + public function decode(string $encodedToken, $keyset = 'default'): stdClass + { + return $this->jwsAdapter->decode($encodedToken, $keyset); + } +} From 7ab3021cc082d76a08454132ffb2207c9657c0a3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 13:44:02 +0900 Subject: [PATCH 083/142] feat: add JWTManager::decode() --- src/Authentication/JWTManager.php | 17 ++- .../Authenticators/JWTAuthenticatorTest.php | 126 ------------------ .../Authentication/JWT/JWTManagerTest.php | 126 ++++++++++++++++++ 3 files changed, 142 insertions(+), 127 deletions(-) diff --git a/src/Authentication/JWTManager.php b/src/Authentication/JWTManager.php index 790b2c7a9..a6931e383 100644 --- a/src/Authentication/JWTManager.php +++ b/src/Authentication/JWTManager.php @@ -5,8 +5,10 @@ namespace CodeIgniter\Shield\Authentication; use CodeIgniter\I18n\Time; +use CodeIgniter\Shield\Authentication\JWT\JWSDecoder; use CodeIgniter\Shield\Authentication\JWT\JWSEncoder; use CodeIgniter\Shield\Entities\User; +use stdClass; /** * JWT Manager @@ -15,13 +17,16 @@ class JWTManager { private Time $clock; private JWSEncoder $jwsEncoder; + private JWSDecoder $jwsDecoder; public function __construct( ?Time $clock = null, - ?JWSEncoder $jwsEncoder = null + ?JWSEncoder $jwsEncoder = null, + ?JWSDecoder $jwsDecoder = null ) { $this->clock = $clock ?? new Time(); $this->jwsEncoder = $jwsEncoder ?? new JWSEncoder($this->clock); + $this->jwsDecoder = $jwsDecoder ?? new JWSDecoder(); } /** @@ -67,4 +72,14 @@ public function generate( ): string { return $this->jwsEncoder->encode($claims, $ttl, $keyset, $headers); } + + /** + * Returns payload of the JWT + * + * @param string $keyset The key group. The array key of Config\AuthJWT::$keys. + */ + public function decode(string $encodedToken, $keyset = 'default'): stdClass + { + return $this->jwsDecoder->decode($encodedToken, $keyset); + } } diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 63b1e7a16..2ba844327 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -245,130 +245,4 @@ private function generateJWT(?Time $clock = null): string return $generator->generateAccessToken($this->user); } - - public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void - { - $config = config(AuthJWT::class); - $config->keys['default'] = [ - [ - 'kid' => 'Key01', - 'alg' => 'HS256', // algorithm. - 'secret' => 'Key01_Secret', - ], - ]; - - // Generate token with Key01. - $generator = new JWTManager(); - $payload = [ - 'user_id' => '1', - ]; - $token = $generator->generate($payload, DAY, 'default'); - - // Add new Key02. - $config->keys['default'] = [ - [ - 'kid' => 'Key02', - 'alg' => 'HS256', // algorithm. - 'secret' => 'Key02_Secret', - ], - [ - 'kid' => 'Key01', - 'alg' => 'HS256', // algorithm. - 'secret' => 'Key01_Secret', - ], - ]; - - $payload = $this->auth->decodeJWT($token); - - $this->assertSame('1', $payload->user_id); - } - - public function testDecodeJWTCanSpecifyKey(): void - { - $config = config(AuthJWT::class); - $config->keys['mobile'] = [ - [ - 'kid' => 'Key01', - 'alg' => 'HS256', // algorithm. - 'secret' => 'Key01_Secret', - ], - ]; - - // Generate token with the mobile key. - $generator = new JWTManager(); - $payload = [ - 'user_id' => '1', - ]; - $token = $generator->generate($payload, DAY, 'mobile'); - - $this->auth->setKeyset('mobile'); - $payload = $this->auth->decodeJWT($token); - - $this->assertSame('1', $payload->user_id); - } - - public function testDecodeJWTCanDecodeWithAsymmetricKey(): void - { - $token = $this->generateJWTWithAsymmetricKey(); - - $payload = $this->auth->decodeJWT($token); - - $this->assertSame('1', $payload->user_id); - } - - private function generateJWTWithAsymmetricKey(): string - { - $generator = new JWTManager(); - - $config = config(AuthJWT::class); - $config->keys['default'][0] = [ - 'alg' => 'RS256', // algorithm. - 'public' => <<<'EOD' - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzWHNM5f+amCjQztc5QT - fJfzCC5J4nuW+L/aOxZ4f8J3FrewM2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJ - hzkPYLae7bTVro3hok0zDITR8F6SJGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548t - u4czCuqU8BGVOlnp6IqBHhAswNMM78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vS - opcT51koWOgiTf3C7nJUoMWZHZI5HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTz - TTqo1SCSH2pooJl9O8at6kkRYsrZWwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/B - wQIDAQAB - -----END PUBLIC KEY----- - EOD, - 'private' => <<<'EOD' - -----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAuzWHNM5f+amCjQztc5QTfJfzCC5J4nuW+L/aOxZ4f8J3Frew - M2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJhzkPYLae7bTVro3hok0zDITR8F6S - JGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548tu4czCuqU8BGVOlnp6IqBHhAswNMM - 78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vSopcT51koWOgiTf3C7nJUoMWZHZI5 - HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTzTTqo1SCSH2pooJl9O8at6kkRYsrZ - WwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/BwQIDAQABAoIBAFtGaOqNKGwggn9k - 6yzr6GhZ6Wt2rh1Xpq8XUz514UBhPxD7dFRLpbzCrLVpzY80LbmVGJ9+1pJozyWc - VKeCeUdNwbqkr240Oe7GTFmGjDoxU+5/HX/SJYPpC8JZ9oqgEA87iz+WQX9hVoP2 - oF6EB4ckDvXmk8FMwVZW2l2/kd5mrEVbDaXKxhvUDf52iVD+sGIlTif7mBgR99/b - c3qiCnxCMmfYUnT2eh7Vv2LhCR/G9S6C3R4lA71rEyiU3KgsGfg0d82/XWXbegJW - h3QbWNtQLxTuIvLq5aAryV3PfaHlPgdgK0ft6ocU2de2FagFka3nfVEyC7IUsNTK - bq6nhAECgYEA7d/0DPOIaItl/8BWKyCuAHMss47j0wlGbBSHdJIiS55akMvnAG0M - 39y22Qqfzh1at9kBFeYeFIIU82ZLF3xOcE3z6pJZ4Dyvx4BYdXH77odo9uVK9s1l - 3T3BlMcqd1hvZLMS7dviyH79jZo4CXSHiKzc7pQ2YfK5eKxKqONeXuECgYEAyXlG - vonaus/YTb1IBei9HwaccnQ/1HRn6MvfDjb7JJDIBhNClGPt6xRlzBbSZ73c2QEC - 6Fu9h36K/HZ2qcLd2bXiNyhIV7b6tVKk+0Psoj0dL9EbhsD1OsmE1nTPyAc9XZbb - OPYxy+dpBCUA8/1U9+uiFoCa7mIbWcSQ+39gHuECgYAz82pQfct30aH4JiBrkNqP - nJfRq05UY70uk5k1u0ikLTRoVS/hJu/d4E1Kv4hBMqYCavFSwAwnvHUo51lVCr/y - xQOVYlsgnwBg2MX4+GjmIkqpSVCC8D7j/73MaWb746OIYZervQ8dbKahi2HbpsiG - 8AHcVSA/agxZr38qvWV54QKBgCD5TlDE8x18AuTGQ9FjxAAd7uD0kbXNz2vUYg9L - hFL5tyL3aAAtUrUUw4xhd9IuysRhW/53dU+FsG2dXdJu6CxHjlyEpUJl2iZu/j15 - YnMzGWHIEX8+eWRDsw/+Ujtko/B7TinGcWPz3cYl4EAOiCeDUyXnqnO1btCEUU44 - DJ1BAoGBAJuPD27ErTSVtId90+M4zFPNibFP50KprVdc8CR37BE7r8vuGgNYXmnI - RLnGP9p3pVgFCktORuYS2J/6t84I3+A17nEoB4xvhTLeAinAW/uTQOUmNicOP4Ek - 2MsLL2kHgL8bLTmvXV4FX+PXphrDKg1XxzOYn0otuoqdAQrkK4og - -----END RSA PRIVATE KEY----- - EOD, - ]; - - $payload = [ - 'user_id' => '1', - ]; - - return $generator->generate($payload, DAY); - } } diff --git a/tests/Unit/Authentication/JWT/JWTManagerTest.php b/tests/Unit/Authentication/JWT/JWTManagerTest.php index 5c21871d8..1b5c6fc10 100644 --- a/tests/Unit/Authentication/JWT/JWTManagerTest.php +++ b/tests/Unit/Authentication/JWT/JWTManagerTest.php @@ -252,4 +252,130 @@ private function decodeJWT(string $token, $part): array true ); } + + public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void + { + $config = config(AuthJWT::class); + $config->keys['default'] = [ + [ + 'kid' => 'Key01', + 'alg' => 'HS256', // algorithm. + 'secret' => 'Key01_Secret', + ], + ]; + + // Generate token with Key01. + $manager = new JWTManager(); + $payload = [ + 'user_id' => '1', + ]; + $token = $manager->generate($payload, DAY, 'default'); + + // Add new Key02. + $config->keys['default'] = [ + [ + 'kid' => 'Key02', + 'alg' => 'HS256', // algorithm. + 'secret' => 'Key02_Secret', + ], + [ + 'kid' => 'Key01', + 'alg' => 'HS256', // algorithm. + 'secret' => 'Key01_Secret', + ], + ]; + + $payload = $manager->decode($token); + + $this->assertSame('1', $payload->user_id); + } + + public function testDecodeJWTCanSpecifyKey(): void + { + $config = config(AuthJWT::class); + $config->keys['mobile'] = [ + [ + 'kid' => 'Key01', + 'alg' => 'HS256', // algorithm. + 'secret' => 'Key01_Secret', + ], + ]; + + // Generate token with the mobile key. + $manager = new JWTManager(); + $payload = [ + 'user_id' => '1', + ]; + $token = $manager->generate($payload, DAY, 'mobile'); + + $payload = $manager->decode($token, 'mobile'); + + $this->assertSame('1', $payload->user_id); + } + + public function testDecodeJWTCanDecodeWithAsymmetricKey(): void + { + $token = $this->generateJWTWithAsymmetricKey(); + + $manager = new JWTManager(); + $payload = $manager->decode($token); + + $this->assertSame('1', $payload->user_id); + } + + private function generateJWTWithAsymmetricKey(): string + { + $manager = new JWTManager(); + + $config = config(AuthJWT::class); + $config->keys['default'][0] = [ + 'alg' => 'RS256', // algorithm. + 'public' => <<<'EOD' + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzWHNM5f+amCjQztc5QT + fJfzCC5J4nuW+L/aOxZ4f8J3FrewM2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJ + hzkPYLae7bTVro3hok0zDITR8F6SJGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548t + u4czCuqU8BGVOlnp6IqBHhAswNMM78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vS + opcT51koWOgiTf3C7nJUoMWZHZI5HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTz + TTqo1SCSH2pooJl9O8at6kkRYsrZWwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/B + wQIDAQAB + -----END PUBLIC KEY----- + EOD, + 'private' => <<<'EOD' + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAuzWHNM5f+amCjQztc5QTfJfzCC5J4nuW+L/aOxZ4f8J3Frew + M2c/dufrnmedsApb0By7WhaHlcqCh/ScAPyJhzkPYLae7bTVro3hok0zDITR8F6S + JGL42JAEUk+ILkPI+DONM0+3vzk6Kvfe548tu4czCuqU8BGVOlnp6IqBHhAswNMM + 78pos/2z0CjPM4tbeXqSTTbNkXRboxjU29vSopcT51koWOgiTf3C7nJUoMWZHZI5 + HqnIhPAG9yv8HAgNk6CMk2CadVHDo4IxjxTzTTqo1SCSH2pooJl9O8at6kkRYsrZ + WwsKlOFE2LUce7ObnXsYihStBUDoeBQlGG/BwQIDAQABAoIBAFtGaOqNKGwggn9k + 6yzr6GhZ6Wt2rh1Xpq8XUz514UBhPxD7dFRLpbzCrLVpzY80LbmVGJ9+1pJozyWc + VKeCeUdNwbqkr240Oe7GTFmGjDoxU+5/HX/SJYPpC8JZ9oqgEA87iz+WQX9hVoP2 + oF6EB4ckDvXmk8FMwVZW2l2/kd5mrEVbDaXKxhvUDf52iVD+sGIlTif7mBgR99/b + c3qiCnxCMmfYUnT2eh7Vv2LhCR/G9S6C3R4lA71rEyiU3KgsGfg0d82/XWXbegJW + h3QbWNtQLxTuIvLq5aAryV3PfaHlPgdgK0ft6ocU2de2FagFka3nfVEyC7IUsNTK + bq6nhAECgYEA7d/0DPOIaItl/8BWKyCuAHMss47j0wlGbBSHdJIiS55akMvnAG0M + 39y22Qqfzh1at9kBFeYeFIIU82ZLF3xOcE3z6pJZ4Dyvx4BYdXH77odo9uVK9s1l + 3T3BlMcqd1hvZLMS7dviyH79jZo4CXSHiKzc7pQ2YfK5eKxKqONeXuECgYEAyXlG + vonaus/YTb1IBei9HwaccnQ/1HRn6MvfDjb7JJDIBhNClGPt6xRlzBbSZ73c2QEC + 6Fu9h36K/HZ2qcLd2bXiNyhIV7b6tVKk+0Psoj0dL9EbhsD1OsmE1nTPyAc9XZbb + OPYxy+dpBCUA8/1U9+uiFoCa7mIbWcSQ+39gHuECgYAz82pQfct30aH4JiBrkNqP + nJfRq05UY70uk5k1u0ikLTRoVS/hJu/d4E1Kv4hBMqYCavFSwAwnvHUo51lVCr/y + xQOVYlsgnwBg2MX4+GjmIkqpSVCC8D7j/73MaWb746OIYZervQ8dbKahi2HbpsiG + 8AHcVSA/agxZr38qvWV54QKBgCD5TlDE8x18AuTGQ9FjxAAd7uD0kbXNz2vUYg9L + hFL5tyL3aAAtUrUUw4xhd9IuysRhW/53dU+FsG2dXdJu6CxHjlyEpUJl2iZu/j15 + YnMzGWHIEX8+eWRDsw/+Ujtko/B7TinGcWPz3cYl4EAOiCeDUyXnqnO1btCEUU44 + DJ1BAoGBAJuPD27ErTSVtId90+M4zFPNibFP50KprVdc8CR37BE7r8vuGgNYXmnI + RLnGP9p3pVgFCktORuYS2J/6t84I3+A17nEoB4xvhTLeAinAW/uTQOUmNicOP4Ek + 2MsLL2kHgL8bLTmvXV4FX+PXphrDKg1XxzOYn0otuoqdAQrkK4og + -----END RSA PRIVATE KEY----- + EOD, + ]; + + $payload = [ + 'user_id' => '1', + ]; + + return $manager->generate($payload, DAY); + } } From 07c346baa14df978cb1f6e14a35ccc88bb209718 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 13:58:21 +0900 Subject: [PATCH 084/142] refactor: JWT uses JWTManager and remove JWT::decodeJWT(). --- src/Authentication/Authenticators/JWT.php | 19 +++------ .../Authentication/JWT/JWTManagerTest.php | 39 +++++++++---------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index a1185bc51..2d4d90e53 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -8,8 +8,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; -use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; -use CodeIgniter\Shield\Authentication\JWT\JWSAdapterInterface; +use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; @@ -37,7 +36,7 @@ class JWT implements AuthenticatorInterface protected UserModel $provider; protected ?User $user = null; - protected JWSAdapterInterface $jwtAdapter; + protected JWTManager $jwtManager; protected TokenLoginModel $tokenLoginModel; protected ?stdClass $payload = null; @@ -46,10 +45,10 @@ class JWT implements AuthenticatorInterface */ protected $keyset = 'default'; - public function __construct(UserModel $provider, ?JWSAdapterInterface $jwtAdapter = null) + public function __construct(UserModel $provider, ?JWTManager $jwtManager = null) { $this->provider = $provider; - $this->jwtAdapter = $jwtAdapter ?? new FirebaseAdapter(); + $this->jwtManager = $jwtManager ?? new JWTManager(); $this->tokenLoginModel = model(TokenLoginModel::class); } @@ -126,7 +125,7 @@ public function check(array $credentials): Result // Check JWT try { - $this->payload = $this->decodeJWT($credentials['token']); + $this->payload = $this->jwtManager->decode($credentials['token']); } catch (RuntimeException $e) { return new Result([ 'success' => false, @@ -246,14 +245,6 @@ public function setKeyset($keyset): void $this->keyset = $keyset; } - /** - * Returns payload of the JWT - */ - public function decodeJWT(string $encodedToken): stdClass - { - return $this->jwtAdapter->decode($encodedToken, $this->keyset); - } - /** * Returns payload */ diff --git a/tests/Unit/Authentication/JWT/JWTManagerTest.php b/tests/Unit/Authentication/JWT/JWTManagerTest.php index 1b5c6fc10..ea4922f0f 100644 --- a/tests/Unit/Authentication/JWT/JWTManagerTest.php +++ b/tests/Unit/Authentication/JWT/JWTManagerTest.php @@ -5,7 +5,6 @@ namespace Tests\Unit\Authentication\JWT; use CodeIgniter\I18n\Time; -use CodeIgniter\Shield\Authentication\Authenticators\JWT; use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; @@ -25,12 +24,12 @@ public function testGenerateAccessToken() // Fix the current time for testing. Time::setTestNow('now'); - $clock = new Time(); - $generator = new JWTManager($clock); + $clock = new Time(); + $manager = new JWTManager($clock); $currentTime = $clock->now(); - $token = $generator->generateAccessToken($user); + $token = $manager->generateAccessToken($user); // Reset the current time. Time::setTestNow(); @@ -48,9 +47,8 @@ public function testGenerateAccessTokenPayload(array $data): void { [$token, $currentTime] = $data; - $auth = new JWT(new UserModel()); - - $payload = $auth->decodeJWT($token); + $manager = new JWTManager(); + $payload = $manager->decode($token); $config = config(AuthJWT::class); $expected = [ @@ -67,12 +65,12 @@ public function testGenerateAccessTokenAddClaims(): void /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); - $generator = new JWTManager(); + $manager = new JWTManager(); $claims = [ 'email' => 'admin@example.jp', ]; - $token = $generator->generateAccessToken($user, $claims); + $token = $manager->generateAccessToken($user, $claims); $this->assertIsString($token); @@ -87,8 +85,8 @@ public function testGenerate() // Fix the current time for testing. Time::setTestNow('now'); - $clock = new Time(); - $generator = new JWTManager($clock); + $clock = new Time(); + $manager = new JWTManager($clock); $currentTime = $clock->now(); @@ -97,7 +95,7 @@ public function testGenerate() 'email' => 'admin@example.jp', ]; - $token = $generator->generate($payload, DAY); + $token = $manager->generate($payload, DAY); // Reset the current time. Time::setTestNow(); @@ -115,9 +113,8 @@ public function testGeneratePayload(array $data): void { [$token, $currentTime] = $data; - $auth = new JWT(new UserModel()); - - $payload = $auth->decodeJWT($token); + $manager = new JWTManager(); + $payload = $manager->decode($token); $config = config(AuthJWT::class); $expected = [ @@ -132,7 +129,7 @@ public function testGeneratePayload(array $data): void public function testGenerateSetKid(): void { - $generator = new JWTManager(); + $manager = new JWTManager(); // Set kid $config = config(AuthJWT::class); @@ -141,7 +138,7 @@ public function testGenerateSetKid(): void $payload = [ 'user_id' => '1', ]; - $token = $generator->generate($payload, DAY); + $token = $manager->generate($payload, DAY); $this->assertIsString($token); @@ -155,7 +152,7 @@ public function testGenerateSetKid(): void public function testGenerateAddHeader(): void { - $generator = new JWTManager(); + $manager = new JWTManager(); $payload = [ 'user_id' => '1', @@ -163,7 +160,7 @@ public function testGenerateAddHeader(): void $headers = [ 'extra_key' => 'extra_value', ]; - $token = $generator->generate($payload, DAY, 'default', $headers); + $token = $manager->generate($payload, DAY, 'default', $headers); $this->assertIsString($token); @@ -177,7 +174,7 @@ public function testGenerateAddHeader(): void public function testGenerateWithAsymmetricKey(): void { - $generator = new JWTManager(); + $manager = new JWTManager(); $config = config(AuthJWT::class); $config->keys['default'][0] = [ @@ -217,7 +214,7 @@ public function testGenerateWithAsymmetricKey(): void $payload = [ 'user_id' => '1', ]; - $token = $generator->generate($payload, DAY); + $token = $manager->generate($payload, DAY); $this->assertIsString($token); From fd080bf3937b40bf291841ca0c1e5800915c0bf7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 14:05:23 +0900 Subject: [PATCH 085/142] test: extract method --- .../Authentication/JWT/JWTManagerTest.php | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/Unit/Authentication/JWT/JWTManagerTest.php b/tests/Unit/Authentication/JWT/JWTManagerTest.php index ea4922f0f..fdb1ba2a9 100644 --- a/tests/Unit/Authentication/JWT/JWTManagerTest.php +++ b/tests/Unit/Authentication/JWT/JWTManagerTest.php @@ -16,6 +16,11 @@ */ final class JWTManagerTest extends TestCase { + private function createJWTManager(?Time $clock = null): JWTManager + { + return new JWTManager($clock); + } + public function testGenerateAccessToken() { /** @var User $user */ @@ -25,7 +30,7 @@ public function testGenerateAccessToken() Time::setTestNow('now'); $clock = new Time(); - $manager = new JWTManager($clock); + $manager = $this->createJWTManager($clock); $currentTime = $clock->now(); @@ -47,7 +52,7 @@ public function testGenerateAccessTokenPayload(array $data): void { [$token, $currentTime] = $data; - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $payload = $manager->decode($token); $config = config(AuthJWT::class); @@ -65,7 +70,7 @@ public function testGenerateAccessTokenAddClaims(): void /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $claims = [ 'email' => 'admin@example.jp', @@ -86,7 +91,7 @@ public function testGenerate() Time::setTestNow('now'); $clock = new Time(); - $manager = new JWTManager($clock); + $manager = $this->createJWTManager($clock); $currentTime = $clock->now(); @@ -113,7 +118,7 @@ public function testGeneratePayload(array $data): void { [$token, $currentTime] = $data; - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $payload = $manager->decode($token); $config = config(AuthJWT::class); @@ -129,7 +134,7 @@ public function testGeneratePayload(array $data): void public function testGenerateSetKid(): void { - $manager = new JWTManager(); + $manager = $this->createJWTManager(); // Set kid $config = config(AuthJWT::class); @@ -152,7 +157,7 @@ public function testGenerateSetKid(): void public function testGenerateAddHeader(): void { - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $payload = [ 'user_id' => '1', @@ -174,7 +179,7 @@ public function testGenerateAddHeader(): void public function testGenerateWithAsymmetricKey(): void { - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $config = config(AuthJWT::class); $config->keys['default'][0] = [ @@ -262,7 +267,7 @@ public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void ]; // Generate token with Key01. - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $payload = [ 'user_id' => '1', ]; @@ -299,7 +304,7 @@ public function testDecodeJWTCanSpecifyKey(): void ]; // Generate token with the mobile key. - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $payload = [ 'user_id' => '1', ]; @@ -314,7 +319,7 @@ public function testDecodeJWTCanDecodeWithAsymmetricKey(): void { $token = $this->generateJWTWithAsymmetricKey(); - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $payload = $manager->decode($token); $this->assertSame('1', $payload->user_id); @@ -322,7 +327,7 @@ public function testDecodeJWTCanDecodeWithAsymmetricKey(): void private function generateJWTWithAsymmetricKey(): string { - $manager = new JWTManager(); + $manager = $this->createJWTManager(); $config = config(AuthJWT::class); $config->keys['default'][0] = [ From 15f11df852e568a596798d5ff5a0c40c410163c4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 14:25:11 +0900 Subject: [PATCH 086/142] fix: add missing keyset argument --- src/Authentication/Authenticators/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 2d4d90e53..3f0bbe18e 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -125,7 +125,7 @@ public function check(array $credentials): Result // Check JWT try { - $this->payload = $this->jwtManager->decode($credentials['token']); + $this->payload = $this->jwtManager->decode($credentials['token'], $this->keyset); } catch (RuntimeException $e) { return new Result([ 'success' => false, From 08dadcc172d50051e04c7883b14a609ac81d5374 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 15:00:08 +0900 Subject: [PATCH 087/142] refactor: change JWTManager method names --- docs/addons/jwt.md | 6 +-- src/Authentication/Authenticators/JWT.php | 2 +- src/Authentication/JWTManager.php | 6 +-- .../Authentication/JWT/JWTManagerTest.php | 42 +++++++++---------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 4df3c7747..e1f01c2e4 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -105,13 +105,13 @@ if you don't specify. ### Arbitrary JWT -You can generate arbitrary JWT with the ``JWTManager::generate()`` method. +You can generate arbitrary JWT with the ``JWTManager::issue()`` method. It takes a JWT claims array, and can take time to live in seconds, a key group (an array key) in the `Config\AuthJWT::$keys`, and additional header array: ```php -public function generate( +public function issue( array $claims, ?int $ttl = null, $keyset = 'default', @@ -130,7 +130,7 @@ $payload = [ 'user_id' => '1', 'email' => 'admin@example.jp', ]; -$token = $jwt->generate($payload, DAY); +$token = $jwt->issue($payload, DAY); ``` It uses the `secret` and `alg` in the `Config\AuthJWT::$keys['default']`. diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 3f0bbe18e..fb242f85b 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -125,7 +125,7 @@ public function check(array $credentials): Result // Check JWT try { - $this->payload = $this->jwtManager->decode($credentials['token'], $this->keyset); + $this->payload = $this->jwtManager->parse($credentials['token'], $this->keyset); } catch (RuntimeException $e) { return new Result([ 'success' => false, diff --git a/src/Authentication/JWTManager.php b/src/Authentication/JWTManager.php index a6931e383..3e0edcba9 100644 --- a/src/Authentication/JWTManager.php +++ b/src/Authentication/JWTManager.php @@ -52,7 +52,7 @@ public function generateAccessToken( ], ); - return $this->generate($payload, $ttl, $keyset, $headers); + return $this->issue($payload, $ttl, $keyset, $headers); } /** @@ -64,7 +64,7 @@ public function generateAccessToken( * The array key of Config\AuthJWT::$keys. * @param array|null $headers An array with header elements to attach. */ - public function generate( + public function issue( array $claims, ?int $ttl = null, $keyset = 'default', @@ -78,7 +78,7 @@ public function generate( * * @param string $keyset The key group. The array key of Config\AuthJWT::$keys. */ - public function decode(string $encodedToken, $keyset = 'default'): stdClass + public function parse(string $encodedToken, $keyset = 'default'): stdClass { return $this->jwsDecoder->decode($encodedToken, $keyset); } diff --git a/tests/Unit/Authentication/JWT/JWTManagerTest.php b/tests/Unit/Authentication/JWT/JWTManagerTest.php index fdb1ba2a9..4bc557eec 100644 --- a/tests/Unit/Authentication/JWT/JWTManagerTest.php +++ b/tests/Unit/Authentication/JWT/JWTManagerTest.php @@ -53,7 +53,7 @@ public function testGenerateAccessTokenPayload(array $data): void [$token, $currentTime] = $data; $manager = $this->createJWTManager(); - $payload = $manager->decode($token); + $payload = $manager->parse($token); $config = config(AuthJWT::class); $expected = [ @@ -85,7 +85,7 @@ public function testGenerateAccessTokenAddClaims(): void $this->assertStringStartsWith('admin@example.jp', $payload['email']); } - public function testGenerate() + public function testIssue() { // Fix the current time for testing. Time::setTestNow('now'); @@ -100,7 +100,7 @@ public function testGenerate() 'email' => 'admin@example.jp', ]; - $token = $manager->generate($payload, DAY); + $token = $manager->issue($payload, DAY); // Reset the current time. Time::setTestNow(); @@ -112,14 +112,14 @@ public function testGenerate() } /** - * @depends testGenerate + * @depends testIssue */ - public function testGeneratePayload(array $data): void + public function testIssuePayload(array $data): void { [$token, $currentTime] = $data; $manager = $this->createJWTManager(); - $payload = $manager->decode($token); + $payload = $manager->parse($token); $config = config(AuthJWT::class); $expected = [ @@ -132,7 +132,7 @@ public function testGeneratePayload(array $data): void $this->assertSame($expected, (array) $payload); } - public function testGenerateSetKid(): void + public function testIssueSetKid(): void { $manager = $this->createJWTManager(); @@ -143,7 +143,7 @@ public function testGenerateSetKid(): void $payload = [ 'user_id' => '1', ]; - $token = $manager->generate($payload, DAY); + $token = $manager->issue($payload, DAY); $this->assertIsString($token); @@ -155,7 +155,7 @@ public function testGenerateSetKid(): void ], $headers); } - public function testGenerateAddHeader(): void + public function testIssueAddHeader(): void { $manager = $this->createJWTManager(); @@ -165,7 +165,7 @@ public function testGenerateAddHeader(): void $headers = [ 'extra_key' => 'extra_value', ]; - $token = $manager->generate($payload, DAY, 'default', $headers); + $token = $manager->issue($payload, DAY, 'default', $headers); $this->assertIsString($token); @@ -177,7 +177,7 @@ public function testGenerateAddHeader(): void ], $headers); } - public function testGenerateWithAsymmetricKey(): void + public function testIssueWithAsymmetricKey(): void { $manager = $this->createJWTManager(); @@ -219,7 +219,7 @@ public function testGenerateWithAsymmetricKey(): void $payload = [ 'user_id' => '1', ]; - $token = $manager->generate($payload, DAY); + $token = $manager->issue($payload, DAY); $this->assertIsString($token); @@ -255,7 +255,7 @@ private function decodeJWT(string $token, $part): array ); } - public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void + public function testParseCanDecodeTokenSignedByOldKey(): void { $config = config(AuthJWT::class); $config->keys['default'] = [ @@ -271,7 +271,7 @@ public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void $payload = [ 'user_id' => '1', ]; - $token = $manager->generate($payload, DAY, 'default'); + $token = $manager->issue($payload, DAY, 'default'); // Add new Key02. $config->keys['default'] = [ @@ -287,12 +287,12 @@ public function testDecodeJWTCanDecodeTokenSignedByOldKey(): void ], ]; - $payload = $manager->decode($token); + $payload = $manager->parse($token); $this->assertSame('1', $payload->user_id); } - public function testDecodeJWTCanSpecifyKey(): void + public function testParseCanSpecifyKey(): void { $config = config(AuthJWT::class); $config->keys['mobile'] = [ @@ -308,19 +308,19 @@ public function testDecodeJWTCanSpecifyKey(): void $payload = [ 'user_id' => '1', ]; - $token = $manager->generate($payload, DAY, 'mobile'); + $token = $manager->issue($payload, DAY, 'mobile'); - $payload = $manager->decode($token, 'mobile'); + $payload = $manager->parse($token, 'mobile'); $this->assertSame('1', $payload->user_id); } - public function testDecodeJWTCanDecodeWithAsymmetricKey(): void + public function testParseCanDecodeWithAsymmetricKey(): void { $token = $this->generateJWTWithAsymmetricKey(); $manager = $this->createJWTManager(); - $payload = $manager->decode($token); + $payload = $manager->parse($token); $this->assertSame('1', $payload->user_id); } @@ -378,6 +378,6 @@ private function generateJWTWithAsymmetricKey(): string 'user_id' => '1', ]; - return $manager->generate($payload, DAY); + return $manager->issue($payload, DAY); } } From 9e852696cf9542ecc2c62536884702aa3662f20f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 15:16:27 +0900 Subject: [PATCH 088/142] docs: fix doc comment --- src/Filters/JWTAuth.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Filters/JWTAuth.php b/src/Filters/JWTAuth.php index 5c52fc3a6..a0a6c2a7d 100644 --- a/src/Filters/JWTAuth.php +++ b/src/Filters/JWTAuth.php @@ -21,14 +21,7 @@ class JWTAuth implements FilterInterface { /** - * Do whatever processing this filter needs to do. - * By default it should not return anything during - * normal execution. However, when an abnormal state - * is found, it should return an instance of - * CodeIgniter\HTTP\Response. If it does, script - * execution will end and that Response will be - * sent back to the client, allowing for error pages, - * redirects, etc. + * Gets the JWT from the Request header, and checks it. * * @param array|null $arguments * From d2cfe31df7526e7ab52e2963a99bc8fdeaaa0e2f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 15:28:32 +0900 Subject: [PATCH 089/142] docs: add "Signed" --- docs/addons/jwt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index e1f01c2e4..e1f1b6ebe 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -64,7 +64,7 @@ with the following command: php -r 'echo base64_encode(random_bytes(32));' ``` -## Generating JWTs +## Generating Signed JWTs ### JWT to a Specific User From 3ba4dfd7424e57f8b172758aff8b860cd4f110cc Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 16:22:46 +0900 Subject: [PATCH 090/142] docs: format the comments in the Config file --- src/Config/AuthJWT.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index eba822bcd..ca6087ad3 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -22,7 +22,10 @@ class AuthJWT extends BaseConfig public string $authenticatorHeader = 'Authorization'; /** - * The default payload items. + * -------------------------------------------------------------------- + * The Default Payload Items + * -------------------------------------------------------------------- + * All JWTs will have these claims in the payload. * * @var array */ @@ -31,8 +34,9 @@ class AuthJWT extends BaseConfig ]; /** + * -------------------------------------------------------------------- * The Keys - * + * -------------------------------------------------------------------- * The key of the array is the key group name. * The first key of the group is used for signing. * @@ -61,11 +65,17 @@ class AuthJWT extends BaseConfig ]; /** + * -------------------------------------------------------------------- + * Time To Live (in seconds) + * -------------------------------------------------------------------- * Specifies the amount of time, in seconds, that a token is valid. */ public int $timeToLive = HOUR; /** + * -------------------------------------------------------------------- + * Record Login Attempts + * -------------------------------------------------------------------- * Whether login attempts are recorded in the database. * * Valid values are: From 695b0e42fb9897913492a1b0520dcf626196269b Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 16:48:45 +0900 Subject: [PATCH 091/142] feat: add jwtmanager service and use it --- src/Authentication/Authenticators/JWT.php | 6 +++--- src/Config/Services.php | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index fb242f85b..dabfeb2bd 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -45,11 +45,11 @@ class JWT implements AuthenticatorInterface */ protected $keyset = 'default'; - public function __construct(UserModel $provider, ?JWTManager $jwtManager = null) + public function __construct(UserModel $provider) { - $this->provider = $provider; - $this->jwtManager = $jwtManager ?? new JWTManager(); + $this->provider = $provider; + $this->jwtManager = service('jwtmanager'); $this->tokenLoginModel = model(TokenLoginModel::class); } diff --git a/src/Config/Services.php b/src/Config/Services.php index 2fb696176..1e002b6a0 100644 --- a/src/Config/Services.php +++ b/src/Config/Services.php @@ -6,6 +6,7 @@ use CodeIgniter\Shield\Auth; use CodeIgniter\Shield\Authentication\Authentication; +use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Authentication\Passwords; use Config\Services as BaseService; @@ -36,4 +37,16 @@ public static function passwords(bool $getShared = true): Passwords return new Passwords(config('Auth')); } + + /** + * JWT Manager. + */ + public static function jwtmanager(bool $getShared = true): JWTManager + { + if ($getShared) { + return self::getSharedInstance('jwtmanager'); + } + + return new JWTManager(); + } } From f293a495ff9fdb9f83c3ec4dce646e6aa845a89b Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 17:19:35 +0900 Subject: [PATCH 092/142] docs: add setup instruction --- docs/addons/jwt.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index e1f1b6ebe..e4189f015 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -33,6 +33,8 @@ To use JWT Authentication, you need additional setup and configuration. } ``` +3. If your **app/Config/Auth.php** is not up-to-date, you also need to update it. Check **vendor/codeigniter4/shield/src/Config/Auth.php** and apply the differences. + ## Configuration Configure **app/Config/AuthJWT.php** for your needs. From cb6f92b2c5a08e7cb732bf95203fead372045877 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 17:19:59 +0900 Subject: [PATCH 093/142] fix: add missing argument for lang message --- src/Authentication/Authenticators/JWT.php | 5 ++++- tests/Authentication/Authenticators/JWTAuthenticatorTest.php | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index dabfeb2bd..25ba59fe0 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -119,7 +119,10 @@ public function check(array $credentials): Result if (! array_key_exists('token', $credentials) || $credentials['token'] === '') { return new Result([ 'success' => false, - 'reason' => lang('Auth.noToken'), + 'reason' => lang( + 'Auth.noToken', + [config(AuthJWT::class)->authenticatorHeader] + ), ]); } diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 2ba844327..56972f4fb 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -102,7 +102,10 @@ public function testCheckNoToken(): void $result = $this->auth->check([]); $this->assertFalse($result->isOK()); - $this->assertSame(\lang('Auth.noToken'), $result->reason()); + $this->assertSame( + \lang('Auth.noToken', [config(AuthJWT::class)->authenticatorHeader]), + $result->reason() + ); } public function testCheckBadSignatureToken(): void From 7b727afa7ebd3b5c35fd8e3dcddbb94f3ecc7495 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 18:38:29 +0900 Subject: [PATCH 094/142] docs: use service('jwtmanager') --- docs/addons/jwt.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index e4189f015..dcbb39767 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -91,7 +91,8 @@ The following code generates a JWT to the user. ```php use CodeIgniter\Shield\Authentication\JWTManager; -$jwt = new JWTManager(); +/** @var JWTManager $jwt */ +$jwt = service('jwtmanager'); $user = auth()->user(); $claims = [ @@ -126,7 +127,8 @@ The following code generates a JWT. ```php use CodeIgniter\Shield\Authentication\JWTManager; -$jwt = new JWTManager(); +/** @var JWTManager $jwt */ +$jwt = service('jwtmanager'); $payload = [ 'user_id' => '1', From cb35a1747854fc73429b315f8964878e19eafc65 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 18:43:43 +0900 Subject: [PATCH 095/142] fix: change private to protected for properties --- src/Authentication/JWT/JWSEncoder.php | 4 ++-- src/Authentication/JWTManager.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Authentication/JWT/JWSEncoder.php b/src/Authentication/JWT/JWSEncoder.php index 8407d5d12..a1cdc05bf 100644 --- a/src/Authentication/JWT/JWSEncoder.php +++ b/src/Authentication/JWT/JWSEncoder.php @@ -10,8 +10,8 @@ class JWSEncoder { - private Time $clock; - private JWSAdapterInterface $jwsAdapter; + protected Time $clock; + protected JWSAdapterInterface $jwsAdapter; public function __construct(?Time $clock = null, ?JWSAdapterInterface $jwsAdapter = null) { diff --git a/src/Authentication/JWTManager.php b/src/Authentication/JWTManager.php index 3e0edcba9..ab4ffd9be 100644 --- a/src/Authentication/JWTManager.php +++ b/src/Authentication/JWTManager.php @@ -15,9 +15,9 @@ */ class JWTManager { - private Time $clock; - private JWSEncoder $jwsEncoder; - private JWSDecoder $jwsDecoder; + protected Time $clock; + protected JWSEncoder $jwsEncoder; + protected JWSDecoder $jwsDecoder; public function __construct( ?Time $clock = null, From e8c0e45af6131a375a3237d8991ff5c5eaf55d64 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 17 Apr 2023 18:47:18 +0900 Subject: [PATCH 096/142] docs: change variable names --- docs/addons/jwt.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index dcbb39767..5e63fa5fe 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -91,14 +91,14 @@ The following code generates a JWT to the user. ```php use CodeIgniter\Shield\Authentication\JWTManager; -/** @var JWTManager $jwt */ -$jwt = service('jwtmanager'); +/** @var JWTManager $manager */ +$manager = service('jwtmanager'); $user = auth()->user(); $claims = [ 'email' => $user->email, ]; -$token = $jwt->generateAccessToken($user, $claims); +$jwt = $manager->generateAccessToken($user, $claims); ``` It sets the `Config\AuthJWT::$defaultClaims` to the token, and adds @@ -127,14 +127,14 @@ The following code generates a JWT. ```php use CodeIgniter\Shield\Authentication\JWTManager; -/** @var JWTManager $jwt */ -$jwt = service('jwtmanager'); +/** @var JWTManager $manager */ +$manager = service('jwtmanager'); $payload = [ 'user_id' => '1', 'email' => 'admin@example.jp', ]; -$token = $jwt->issue($payload, DAY); +$jwt = $manager->issue($payload, DAY); ``` It uses the `secret` and `alg` in the `Config\AuthJWT::$keys['default']`. From 61f06fa870f4eb9c6042644622296f3f23108443 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 09:43:40 +0900 Subject: [PATCH 097/142] refactor: add comment for each exception --- .../JWT/Adapters/FirebaseAdapter.php | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index c2e518868..44311281c 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -46,12 +46,30 @@ public static function decode(string $encodedToken, $keyset): stdClass try { return JWT::decode($encodedToken, $keys); - } catch (BeforeValidException|ExpiredException $e) { + } catch (InvalidArgumentException $e) { + // provided key/key-array is empty or malformed. + throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); + } catch (DomainException $e) { + // provided algorithm is unsupported OR + // provided key is invalid OR + // unknown error thrown in openSSL or libsodium OR + // libsodium is required but not available. + throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); + } catch (SignatureInvalidException $e) { + // provided JWT signature verification failed. + throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); + } catch (BeforeValidException $e) { + // provided JWT is trying to be used before "nbf" claim OR + // provided JWT is trying to be used before "iat" claim. + throw new RuntimeException('Expired JWT: ' . $e->getMessage(), 0, $e); + } catch (ExpiredException $e) { + // provided JWT is trying to be used after "exp" claim. throw new RuntimeException('Expired JWT: ' . $e->getMessage(), 0, $e); - } catch ( - InvalidArgumentException|DomainException|UnexpectedValueException - |SignatureInvalidException $e - ) { + } catch (UnexpectedValueException $e) { + // provided JWT is malformed OR + // provided JWT is missing an algorithm / using an unsupported algorithm OR + // provided JWT algorithm does not match provided key OR + // provided key ID in key/key-array is empty or invalid. throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); } } From 2dc12527acc30f3ad10ddc5e41173679a78ecbb5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 10:47:46 +0900 Subject: [PATCH 098/142] feat: improve Exception handling for FirebaseAdapter - add InvalidTokenException and use it - change Exception classes - add lang items for error messages --- .../JWT/Adapters/FirebaseAdapter.php | 27 ++++++++++++++----- .../JWT/Exceptions/InvalidTokenException.php | 26 ++++++++++++++++++ src/Language/de/Auth.php | 4 +++ src/Language/en/Auth.php | 4 +++ src/Language/es/Auth.php | 4 +++ src/Language/fa/Auth.php | 4 +++ src/Language/fr/Auth.php | 4 +++ src/Language/id/Auth.php | 4 +++ src/Language/it/Auth.php | 4 +++ src/Language/ja/Auth.php | 4 +++ src/Language/pt-BR/Auth.php | 4 +++ src/Language/pt/Auth.php | 4 +++ src/Language/sk/Auth.php | 4 +++ src/Language/sr/Auth.php | 4 +++ src/Language/sv-SE/Auth.php | 4 +++ src/Language/tr/Auth.php | 4 +++ .../Authenticators/JWTAuthenticatorTest.php | 6 ++--- .../JWT/Adapters/FirebaseAdapaterTest.php | 10 +++---- 18 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 src/Authentication/JWT/Exceptions/InvalidTokenException.php diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index 44311281c..27b88fd37 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -4,9 +4,11 @@ namespace CodeIgniter\Shield\Authentication\JWT\Adapters; +use CodeIgniter\Shield\Authentication\JWT\Exceptions\InvalidTokenException; use CodeIgniter\Shield\Authentication\JWT\JWSAdapterInterface; use CodeIgniter\Shield\Config\AuthJWT; -use CodeIgniter\Shield\Exceptions\RuntimeException; +use CodeIgniter\Shield\Exceptions\InvalidArgumentException as ShieldInvalidArgumentException; +use CodeIgniter\Shield\Exceptions\LogicException; use DomainException; use Firebase\JWT\BeforeValidException; use Firebase\JWT\ExpiredException; @@ -48,29 +50,40 @@ public static function decode(string $encodedToken, $keyset): stdClass return JWT::decode($encodedToken, $keys); } catch (InvalidArgumentException $e) { // provided key/key-array is empty or malformed. - throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); + throw new ShieldInvalidArgumentException( + 'Invalid Keyset: "' . $keyset . '". ' . $e->getMessage(), + 0, + $e + ); } catch (DomainException $e) { // provided algorithm is unsupported OR // provided key is invalid OR // unknown error thrown in openSSL or libsodium OR // libsodium is required but not available. - throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); + throw new LogicException('Cannot decode JWT: ' . $e->getMessage(), 0, $e); } catch (SignatureInvalidException $e) { // provided JWT signature verification failed. - throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); + throw InvalidTokenException::forInvalidToken($e); } catch (BeforeValidException $e) { // provided JWT is trying to be used before "nbf" claim OR // provided JWT is trying to be used before "iat" claim. - throw new RuntimeException('Expired JWT: ' . $e->getMessage(), 0, $e); + throw InvalidTokenException::forBeforeValidToken($e); } catch (ExpiredException $e) { // provided JWT is trying to be used after "exp" claim. - throw new RuntimeException('Expired JWT: ' . $e->getMessage(), 0, $e); + throw InvalidTokenException::forExpiredToken($e); } catch (UnexpectedValueException $e) { // provided JWT is malformed OR // provided JWT is missing an algorithm / using an unsupported algorithm OR // provided JWT algorithm does not match provided key OR // provided key ID in key/key-array is empty or invalid. - throw new RuntimeException('Invalid JWT: ' . $e->getMessage(), 0, $e); + log_message( + 'error', + '[Shield] ' . class_basename(self::class) . '::' . __FUNCTION__ + . '(' . __LINE__ . ') ' + . get_class($e) . ': ' . $e->getMessage() + ); + + throw InvalidTokenException::forInvalidToken($e); } } diff --git a/src/Authentication/JWT/Exceptions/InvalidTokenException.php b/src/Authentication/JWT/Exceptions/InvalidTokenException.php new file mode 100644 index 000000000..b455dcfd0 --- /dev/null +++ b/src/Authentication/JWT/Exceptions/InvalidTokenException.php @@ -0,0 +1,26 @@ + 'Leider gab es ein Problem beim Senden der E-Mail. Wir konnten keine E-Mail an "{0}" senden.', 'throttled' => 'Es wurden zu viele Anfragen von dieser IP-Adresse gestellt. Sie können es in {0} Sekunden erneut versuchen.', 'notEnoughPrivilege' => 'Sie haben nicht die erforderliche Berechtigung, um den gewünschten Vorgang auszuführen.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'E-Mail-Adresse', 'username' => 'Benutzername', diff --git a/src/Language/en/Auth.php b/src/Language/en/Auth.php index 306c233d5..363fd4af7 100644 --- a/src/Language/en/Auth.php +++ b/src/Language/en/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Sorry, there was a problem sending the email. We could not send an email to "{0}".', 'throttled' => 'Too many requests made from this IP address. You may try again in {0} seconds.', 'notEnoughPrivilege' => 'You do not have the necessary permission to perform the desired operation.', + // JWT Exceptions + 'invalidJWT' => 'The token is invalid.', + 'expiredJWT' => 'The token has expired.', + 'beforeValidJWT' => 'The token is not yet available.', 'email' => 'Email Address', 'username' => 'Username', diff --git a/src/Language/es/Auth.php b/src/Language/es/Auth.php index bd99cb003..2cf2c6211 100644 --- a/src/Language/es/Auth.php +++ b/src/Language/es/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Lo siento, hubo un problema al enviar el correo electrónico. No pudimos enviar un correo electrónico a "{0}".', 'throttled' => 'Se han realizado demasiadas solicitudes desde esta dirección IP. Puedes intentarlo de nuevo en {0} segundos.', 'notEnoughPrivilege' => 'No tienes los permisos necesarios para realizar la operación deseada.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'Correo Electrónico', 'username' => 'Nombre de usuario', diff --git a/src/Language/fa/Auth.php b/src/Language/fa/Auth.php index b3d8e4fe5..ba2fe9c7d 100644 --- a/src/Language/fa/Auth.php +++ b/src/Language/fa/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'متاسفانه, در ارسال ایمیل مشکلی پیش آمد. ما نتوانستیم ایمیلی را به "{0}" ارسال کنیم.', 'throttled' => 'درخواست های بسیار زیادی از این آدرس IP انجام شده است. می توانید بعد از {0} ثانیه دوباره امتحان کنید.', 'notEnoughPrivilege' => 'شما مجوز لازم برای انجام عملیات مورد نظر را ندارید.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'آدرس ایمیل', 'username' => 'نام کاربری', diff --git a/src/Language/fr/Auth.php b/src/Language/fr/Auth.php index 50c56881a..b43a354b0 100644 --- a/src/Language/fr/Auth.php +++ b/src/Language/fr/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Désolé, il y a eu un problème lors de l\'envoi de l\'email. Nous ne pouvons pas envoyer un email à "{0}".', 'throttled' => 'Trop de requêtes faites depuis cette adresse IP. Vous pouvez réessayer dans {0} secondes.', 'notEnoughPrivilege' => 'Vous n\'avez pas l\'autorisation nécessaire pour effectuer l\'opération souhaitée.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'Adresse email', 'username' => 'Identifiant', diff --git a/src/Language/id/Auth.php b/src/Language/id/Auth.php index ada97cf22..f2be28a35 100644 --- a/src/Language/id/Auth.php +++ b/src/Language/id/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Maaf, ada masalah saat mengirim email. Kami tidak dapat mengirim email ke "{0}".', 'throttled' => 'Terlalu banyak permintaan yang dibuat dari alamat IP ini. Anda dapat mencoba lagi dalam {0} detik.', 'notEnoughPrivilege' => 'Anda tidak memiliki izin yang diperlukan untuk melakukan operasi yang diinginkan.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'Alamat Email', 'username' => 'Nama Pengguna', diff --git a/src/Language/it/Auth.php b/src/Language/it/Auth.php index f517ac988..af2b41e24 100644 --- a/src/Language/it/Auth.php +++ b/src/Language/it/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Spiacente, c\'è stato un problema inviando l\'email. Non possiamo inviare un\'email a "{0}".', 'throttled' => 'Troppe richieste effettuate da questo indirizzo IP. Potrai riprovare tra {0} secondi.', 'notEnoughPrivilege' => 'Non si dispone dell\'autorizzazione necessaria per eseguire l\'operazione desiderata.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'Indirizzo Email', 'username' => 'Nome Utente', diff --git a/src/Language/ja/Auth.php b/src/Language/ja/Auth.php index e1d4870be..8d1ec4574 100644 --- a/src/Language/ja/Auth.php +++ b/src/Language/ja/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => '申し訳ありませんが、メールの送信に問題がありました。 "{0}"にメールを送信できませんでした。', // 'Sorry, there was a problem sending the email. We could not send an email to "{0}".', 'throttled' => 'このIPアドレスからのリクエストが多すぎます。 {0}秒後に再試行できます。', // Too many requests made from this IP address. You may try again in {0} seconds. 'notEnoughPrivilege' => '目的の操作を実行するために必要な権限がありません。', // You do not have the necessary permission to perform the desired operation. + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'メールアドレス', // 'Email Address', 'username' => 'ユーザー名', // 'Username', diff --git a/src/Language/pt-BR/Auth.php b/src/Language/pt-BR/Auth.php index 4a9c6cbf4..e98ff1745 100644 --- a/src/Language/pt-BR/Auth.php +++ b/src/Language/pt-BR/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Desculpe, houve um problema ao enviar o email. Não pudemos enviar um email para {0}.', 'throttled' => 'Muitas solicitações feitas a partir deste endereço IP. Você pode tentar novamente em {0} segundos.', 'notEnoughPrivilege' => 'Você não tem a permissão necessária para realizar a operação desejada.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'Endereço de Email', 'username' => 'Nome de usuário', diff --git a/src/Language/pt/Auth.php b/src/Language/pt/Auth.php index 4a9057c3e..a5cfa8492 100644 --- a/src/Language/pt/Auth.php +++ b/src/Language/pt/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Desculpe, houve um problema ao enviar o email. Não pudemos enviar um email para {0}.', 'throttled' => 'Muitas solicitações feitas a partir deste endereço IP. Pode tentar novamente em {0} segundos.', 'notEnoughPrivilege' => 'Não tem a permissão necessária para realizar a operação desejada.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'Endereço de Email', 'username' => 'Nome de utilizador', diff --git a/src/Language/sk/Auth.php b/src/Language/sk/Auth.php index 3617e0560..3424e88c4 100644 --- a/src/Language/sk/Auth.php +++ b/src/Language/sk/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Ľutujeme, pri odosielaní e-mailu sa vyskytol problém. Nepodarilo sa nám odoslať e-mail na adresu „{0}".', 'throttled' => 'Z tejto adresy IP bolo odoslaných príliš veľa žiadostí. Môžete to skúsiť znova o {0} sekúnd.', 'notEnoughPrivilege' => 'Nemáte potrebné povolenie na vykonanie požadovanej operácie.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'Emailová adresa', 'username' => 'Používateľské meno', diff --git a/src/Language/sr/Auth.php b/src/Language/sr/Auth.php index 22b2ecb5d..6c8e71f01 100644 --- a/src/Language/sr/Auth.php +++ b/src/Language/sr/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Žao nam je ali slanje email poruke nije moguće. Nismo u mogućnosti poslati poruku na "{0}".', 'throttled' => 'Preveliki broj zahteva sa vaše IP adrese. Možete pokušati ponovo za {0} secondi.', 'notEnoughPrivilege' => 'Nemate dovoljan nivo autorizacije za zahtevanu akciju.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'E-mail Adresa', 'username' => 'Korisničko ime', diff --git a/src/Language/sv-SE/Auth.php b/src/Language/sv-SE/Auth.php index 7176f650a..a37b09665 100644 --- a/src/Language/sv-SE/Auth.php +++ b/src/Language/sv-SE/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Det var inte möjligt att skicka epost. Det gick inte att skicka till "{0}".', 'throttled' => 'För många anrop från denna IP-adress. Du kan försöka igen om {0} sekunder.', 'notEnoughPrivilege' => 'Du har inte nödvändiga rättigheter för detta kommando.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'Epostadress', 'username' => 'Användarnamn', diff --git a/src/Language/tr/Auth.php b/src/Language/tr/Auth.php index 48c7a247f..57f515e31 100644 --- a/src/Language/tr/Auth.php +++ b/src/Language/tr/Auth.php @@ -20,6 +20,10 @@ 'unableSendEmailToUser' => 'Üzgünüz, e-posta gönderilirken bir sorun oluştu. "{0}" adresine e-posta gönderemedik.', 'throttled' => 'Bu IP adresinden çok fazla istek yapıldı. {0} saniye sonra tekrar deneyebilirsiniz.', 'notEnoughPrivilege' => 'İstediğiniz işlemi gerçekleştirmek için gerekli izne sahip değilsiniz.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', 'email' => 'E-posta Adresi', 'username' => 'Kullanıcı Adı', diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 56972f4fb..f9be47533 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -113,7 +113,7 @@ public function testCheckBadSignatureToken(): void $result = $this->auth->check(['token' => self::BAD_JWT]); $this->assertFalse($result->isOK()); - $this->assertSame('Invalid JWT: Signature verification failed', $result->reason()); + $this->assertSame(lang('Auth.invalidJWT'), $result->reason()); } public function testCheckNoSubToken(): void @@ -140,7 +140,7 @@ public function testCheckOldToken(): void $result = $this->auth->check(['token' => $token]); $this->assertFalse($result->isOK()); - $this->assertSame('Expired JWT: Expired token', $result->reason()); + $this->assertSame(lang('Auth.expiredJWT'), $result->reason()); } public function testCheckNoUserInDatabase(): void @@ -188,7 +188,7 @@ public function testAttemptBadSignatureToken(): void $this->assertInstanceOf(Result::class, $result); $this->assertFalse($result->isOK()); - $this->assertSame('Invalid JWT: Signature verification failed', $result->reason()); + $this->assertSame(lang('Auth.invalidJWT'), $result->reason()); // A login attempt should have always been recorded $this->seeInDatabase('auth_token_logins', [ diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index 176d3e886..06c8d69d4 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -6,10 +6,10 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\JWT\Adapters\FirebaseAdapter; +use CodeIgniter\Shield\Authentication\JWT\Exceptions\InvalidTokenException; use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; -use CodeIgniter\Shield\Exceptions\RuntimeException; use CodeIgniter\Shield\Models\UserModel; use Tests\Support\TestCase; @@ -50,8 +50,8 @@ public static function generateJWT(?Time $clock = null): string public function testDecodeSignatureInvalidException(): void { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Invalid JWT: Signature verification failed'); + $this->expectException(InvalidTokenException::class); + $this->expectExceptionMessage(lang('Auth.invalidJWT')); $jwtDecoder = new FirebaseAdapter(); @@ -62,8 +62,8 @@ public function testDecodeSignatureInvalidException(): void public function testDecodeExpiredException(): void { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Expired JWT: Expired token'); + $this->expectException(InvalidTokenException::class); + $this->expectExceptionMessage(lang('Auth.expiredJWT')); $jwtDecoder = new FirebaseAdapter(); From 399ab22b7e966d06b4ba29e10de6cf6e6e985f48 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 12:05:09 +0900 Subject: [PATCH 099/142] refactor: extract method --- .../JWT/Adapters/FirebaseAdapter.php | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index 27b88fd37..4de537b90 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -26,25 +26,7 @@ class FirebaseAdapter implements JWSAdapterInterface */ public static function decode(string $encodedToken, $keyset): stdClass { - $config = config(AuthJWT::class); - - $configKeys = $config->keys[$keyset]; - - if (count($configKeys) === 1) { - $key = $configKeys[0]['secret'] ?? $configKeys[0]['public']; - $algorithm = $configKeys[0]['alg']; - - $keys = new Key($key, $algorithm); - } else { - $keys = []; - - foreach ($config->keys[$keyset] as $item) { - $key = $item['secret'] ?? $item['public']; - $algorithm = $item['alg']; - - $keys[$item['kid']] = new Key($key, $algorithm); - } - } + $keys = self::createKeys($keyset); try { return JWT::decode($encodedToken, $keys); @@ -87,6 +69,38 @@ public static function decode(string $encodedToken, $keyset): stdClass } } + /** + * Creates keys for Firebase php-jwt + * + * @param string $keyset + * + * @return array|Key key or key array + */ + private static function createKeys($keyset) + { + $config = config(AuthJWT::class); + + $configKeys = $config->keys[$keyset]; + + if (count($configKeys) === 1) { + $key = $configKeys[0]['secret'] ?? $configKeys[0]['public']; + $algorithm = $configKeys[0]['alg']; + + $keys = new Key($key, $algorithm); + } else { + $keys = []; + + foreach ($config->keys[$keyset] as $item) { + $key = $item['secret'] ?? $item['public']; + $algorithm = $item['alg']; + + $keys[$item['kid']] = new Key($key, $algorithm); + } + } + + return $keys; + } + /** * {@inheritDoc} */ From c9fd280135fb107b0cd068bda4afa2f10836df39 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 12:06:33 +0900 Subject: [PATCH 100/142] refactor: early return --- .../JWT/Adapters/FirebaseAdapter.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index 4de537b90..2e37cec19 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -86,16 +86,16 @@ private static function createKeys($keyset) $key = $configKeys[0]['secret'] ?? $configKeys[0]['public']; $algorithm = $configKeys[0]['alg']; - $keys = new Key($key, $algorithm); - } else { - $keys = []; + return new Key($key, $algorithm); + } - foreach ($config->keys[$keyset] as $item) { - $key = $item['secret'] ?? $item['public']; - $algorithm = $item['alg']; + $keys = []; - $keys[$item['kid']] = new Key($key, $algorithm); - } + foreach ($config->keys[$keyset] as $item) { + $key = $item['secret'] ?? $item['public']; + $algorithm = $item['alg']; + + $keys[$item['kid']] = new Key($key, $algorithm); } return $keys; From 58eb3dd1fb0dcc59a0bf3bd937984b96b26acf6d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 12:59:25 +0900 Subject: [PATCH 101/142] refactor: rename generateAccessToken() to generateToken() Since we have Access Tokens in Shield already. --- docs/addons/jwt.md | 6 +++--- src/Authentication/JWTManager.php | 2 +- .../Authentication/Authenticators/JWTAuthenticatorTest.php | 2 +- tests/Authentication/Filters/JWTFilterTest.php | 2 +- .../Authentication/JWT/Adapters/FirebaseAdapaterTest.php | 2 +- tests/Unit/Authentication/JWT/JWTManagerTest.php | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 5e63fa5fe..87f0ccc38 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -70,14 +70,14 @@ php -r 'echo base64_encode(random_bytes(32));' ### JWT to a Specific User -JWTs are created through the `JWTManager::generateAccessToken()` method. +JWTs are created through the `JWTManager::generateToken()` method. This takes a User object to give to the token as the first argument. It can also take optional additional claims array, time to live in seconds, a key group (an array key) in the `Config\AuthJWT::$keys`, and additional header array: ```php -public function generateAccessToken( +public function generateToken( User $user, array $claims = [], ?int $ttl = null, @@ -98,7 +98,7 @@ $user = auth()->user(); $claims = [ 'email' => $user->email, ]; -$jwt = $manager->generateAccessToken($user, $claims); +$jwt = $manager->generateToken($user, $claims); ``` It sets the `Config\AuthJWT::$defaultClaims` to the token, and adds diff --git a/src/Authentication/JWTManager.php b/src/Authentication/JWTManager.php index ab4ffd9be..145bcc584 100644 --- a/src/Authentication/JWTManager.php +++ b/src/Authentication/JWTManager.php @@ -38,7 +38,7 @@ public function __construct( * The array key of Config\AuthJWT::$keys. * @param array|null $headers An array with header elements to attach. */ - public function generateAccessToken( + public function generateToken( User $user, array $claims = [], ?int $ttl = null, diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index f9be47533..d8a29a184 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -246,6 +246,6 @@ private function generateJWT(?Time $clock = null): string $generator = new JWTManager($clock); - return $generator->generateAccessToken($this->user); + return $generator->generateToken($this->user); } } diff --git a/tests/Authentication/Filters/JWTFilterTest.php b/tests/Authentication/Filters/JWTFilterTest.php index 49855cb5a..e4ba69530 100644 --- a/tests/Authentication/Filters/JWTFilterTest.php +++ b/tests/Authentication/Filters/JWTFilterTest.php @@ -67,7 +67,7 @@ public function testFilterSuccess(): void $user = \fake(UserModel::class); $generator = new JWTManager(); - $token = $generator->generateAccessToken($user); + $token = $generator->generateToken($user); $result = $this->withHeaders(['Authorization' => 'Bearer ' . $token]) ->get('protected-route'); diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index 06c8d69d4..94a75a29c 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -45,7 +45,7 @@ public static function generateJWT(?Time $clock = null): string $generator = new JWTManager($clock); - return $generator->generateAccessToken($user); + return $generator->generateToken($user); } public function testDecodeSignatureInvalidException(): void diff --git a/tests/Unit/Authentication/JWT/JWTManagerTest.php b/tests/Unit/Authentication/JWT/JWTManagerTest.php index 4bc557eec..e746aff54 100644 --- a/tests/Unit/Authentication/JWT/JWTManagerTest.php +++ b/tests/Unit/Authentication/JWT/JWTManagerTest.php @@ -34,7 +34,7 @@ public function testGenerateAccessToken() $currentTime = $clock->now(); - $token = $manager->generateAccessToken($user); + $token = $manager->generateToken($user); // Reset the current time. Time::setTestNow(); @@ -75,7 +75,7 @@ public function testGenerateAccessTokenAddClaims(): void $claims = [ 'email' => 'admin@example.jp', ]; - $token = $manager->generateAccessToken($user, $claims); + $token = $manager->generateToken($user, $claims); $this->assertIsString($token); From e82430331d90a1fb44141ef1e419a01eec3a759e Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 13:06:09 +0900 Subject: [PATCH 102/142] refactor: change parameter order --- src/Authentication/JWT/JWSEncoder.php | 4 ++-- src/Authentication/JWTManager.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Authentication/JWT/JWSEncoder.php b/src/Authentication/JWT/JWSEncoder.php index a1cdc05bf..327d5ea03 100644 --- a/src/Authentication/JWT/JWSEncoder.php +++ b/src/Authentication/JWT/JWSEncoder.php @@ -13,10 +13,10 @@ class JWSEncoder protected Time $clock; protected JWSAdapterInterface $jwsAdapter; - public function __construct(?Time $clock = null, ?JWSAdapterInterface $jwsAdapter = null) + public function __construct(?JWSAdapterInterface $jwsAdapter = null, ?Time $clock = null) { - $this->clock = $clock ?? new Time(); $this->jwsAdapter = $jwsAdapter ?? new FirebaseAdapter(); + $this->clock = $clock ?? new Time(); } /** diff --git a/src/Authentication/JWTManager.php b/src/Authentication/JWTManager.php index 145bcc584..11f1dba93 100644 --- a/src/Authentication/JWTManager.php +++ b/src/Authentication/JWTManager.php @@ -25,7 +25,7 @@ public function __construct( ?JWSDecoder $jwsDecoder = null ) { $this->clock = $clock ?? new Time(); - $this->jwsEncoder = $jwsEncoder ?? new JWSEncoder($this->clock); + $this->jwsEncoder = $jwsEncoder ?? new JWSEncoder(null, $this->clock); $this->jwsDecoder = $jwsDecoder ?? new JWSDecoder(); } From 2b4d959d6adf0251bbeca54505bbb8b3b7b21cde Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 14:19:18 +0900 Subject: [PATCH 103/142] docs: add explanation for JWT and terms --- docs/addons/jwt.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 87f0ccc38..aad80fdcb 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -1,9 +1,26 @@ # JWT Authentication -To use JWT Authentication, you need additional setup and configuration. +> **Note** +> Shield now supports only JWS (Singed JWT). JWE (Encrypted JWT) is not supported. + +## What is JWT? + +JWT or JSON Web Token is a compact and self-contained way of securely transmitting +information between parties as a JSON object. It is commonly used for authentication +and authorization purposes in web applications. + +For example, when a user logs in to a web application, the server generates a JWT +token and sends it to the client. The client then includes this token in the header +of subsequent requests to the server. The server verifies the authenticity of the +token and grants access to protected resources accordingly. + +If you are not familiar with JWT, we recommend that you check out +[Introduction to JSON Web Tokens](https://jwt.io/introduction) before continuing. ## Setup +To use JWT Authentication, you need additional setup and configuration. + ### Manual Setup 1. Install "firebase/php-jwt" via Composer. @@ -41,7 +58,11 @@ Configure **app/Config/AuthJWT.php** for your needs. ### Set the Default Claims -Set the payload items by default to the property `$defaultClaims`. +> **Note** +> A payload contains the actual data being transmitted, such as user ID, role, +> or expiration time. Items in a payload is called *claims*. + +Set the default payload items to the property `$defaultClaims`. E.g.: ```php @@ -50,6 +71,8 @@ E.g.: ]; ``` +The default claims will be included in all tokens issued by Shield. + ### Set Secret Key Set your secret key in the `$keys` property, or set it in your `.env` file. @@ -66,6 +89,9 @@ with the following command: php -r 'echo base64_encode(random_bytes(32));' ``` +> **Note** +> The secret key is used for signing and validating tokens. + ## Generating Signed JWTs ### JWT to a Specific User From aa0147dbf57213e6da9b480bb1816937420699ee Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 14:23:35 +0900 Subject: [PATCH 104/142] docs: fix coding style --- docs/addons/jwt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index aad80fdcb..74a832bf5 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -120,7 +120,7 @@ use CodeIgniter\Shield\Authentication\JWTManager; /** @var JWTManager $manager */ $manager = service('jwtmanager'); -$user = auth()->user(); +$user = auth()->user(); $claims = [ 'email' => $user->email, ]; From ff1d04ab2f9be2b3f4a5122cd5164ba7d8c7ec85 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 15:47:52 +0900 Subject: [PATCH 105/142] fix: missing try --- .../JWT/Adapters/FirebaseAdapter.php | 8 +-- .../JWT/Adapters/FirebaseAdapaterTest.php | 49 ++++++++++++++++++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index 2e37cec19..313b138f9 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -26,9 +26,9 @@ class FirebaseAdapter implements JWSAdapterInterface */ public static function decode(string $encodedToken, $keyset): stdClass { - $keys = self::createKeys($keyset); - try { + $keys = self::createKeysForDecode($keyset); + return JWT::decode($encodedToken, $keys); } catch (InvalidArgumentException $e) { // provided key/key-array is empty or malformed. @@ -70,13 +70,13 @@ public static function decode(string $encodedToken, $keyset): stdClass } /** - * Creates keys for Firebase php-jwt + * Creates keys for Decode * * @param string $keyset * * @return array|Key key or key array */ - private static function createKeys($keyset) + private static function createKeysForDecode($keyset) { $config = config(AuthJWT::class); diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index 94a75a29c..9a3cbe7aa 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -10,8 +10,10 @@ use CodeIgniter\Shield\Authentication\JWTManager; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; +use CodeIgniter\Shield\Exceptions\InvalidArgumentException as ShieldInvalidArgumentException; use CodeIgniter\Shield\Models\UserModel; use Tests\Support\TestCase; +use UnexpectedValueException; /** * @internal @@ -60,7 +62,7 @@ public function testDecodeSignatureInvalidException(): void $jwtDecoder->decode($token, $key); } - public function testDecodeExpiredException(): void + public function testDecodeExpiredToken(): void { $this->expectException(InvalidTokenException::class); $this->expectExceptionMessage(lang('Auth.expiredJWT')); @@ -74,4 +76,49 @@ public function testDecodeExpiredException(): void $key = 'default'; $jwtDecoder->decode($token, $key); } + + public function testDecodeInvalidTokenExceptionUnexpectedValueException(): void + { + $token = $this->generateJWT(); + + // Change algorithm and it makes the key invalid. + $config = config(AuthJWT::class); + $config->keys['default'][0]['alg'] = 'ES256'; + + $jwtDecoder = new FirebaseAdapter(); + + try { + $key = 'default'; + $jwtDecoder->decode($token, $key); + } catch (InvalidTokenException $e) { + $prevException = $e->getPrevious(); + + $this->assertInstanceOf(UnexpectedValueException::class, $prevException); + $this->assertSame('Incorrect key for this algorithm', $prevException->getMessage()); + + return; + } + + $this->fail('InvalidTokenException is not thrown.'); + } + + public function testDecodeInvalidArgumentException(): void + { + $this->expectException(ShieldInvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Keyset: "default". Key material must not be empty'); + + $token = $this->generateJWT(); + + // Set invalid key. + $config = config(AuthJWT::class); + $config->keys['default'][0] = [ + 'alg' => '', + 'secret' => '', + ]; + + $jwtDecoder = new FirebaseAdapter(); + + $key = 'default'; + $jwtDecoder->decode($token, $key); + } } From cfb62637a17aec2d378bb7b59eb7f69f8a9eae5f Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 16:11:54 +0900 Subject: [PATCH 106/142] fix: add missing try/catch --- .../JWT/Adapters/FirebaseAdapter.php | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index 313b138f9..6ee2b6b90 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -8,7 +8,7 @@ use CodeIgniter\Shield\Authentication\JWT\JWSAdapterInterface; use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Exceptions\InvalidArgumentException as ShieldInvalidArgumentException; -use CodeIgniter\Shield\Exceptions\LogicException; +use CodeIgniter\Shield\Exceptions\LogicException as ShieldLogicException; use DomainException; use Firebase\JWT\BeforeValidException; use Firebase\JWT\ExpiredException; @@ -16,6 +16,7 @@ use Firebase\JWT\Key; use Firebase\JWT\SignatureInvalidException; use InvalidArgumentException; +use LogicException; use stdClass; use UnexpectedValueException; @@ -42,7 +43,7 @@ public static function decode(string $encodedToken, $keyset): stdClass // provided key is invalid OR // unknown error thrown in openSSL or libsodium OR // libsodium is required but not available. - throw new LogicException('Cannot decode JWT: ' . $e->getMessage(), 0, $e); + throw new ShieldLogicException('Cannot decode JWT: ' . $e->getMessage(), 0, $e); } catch (SignatureInvalidException $e) { // provided JWT signature verification failed. throw InvalidTokenException::forInvalidToken($e); @@ -106,30 +107,38 @@ private static function createKeysForDecode($keyset) */ public static function encode(array $payload, $keyset, ?array $headers = null): string { - $config = config(AuthJWT::class); - - if (isset($config->keys[$keyset][0]['secret'])) { - $key = $config->keys[$keyset][0]['secret']; - } else { - $passphrase = $config->keys[$keyset][0]['passphrase'] ?? ''; + try { + $config = config(AuthJWT::class); - if ($passphrase !== '') { - $key = openssl_pkey_get_private( - $config->keys[$keyset][0]['private'], - $passphrase - ); + if (isset($config->keys[$keyset][0]['secret'])) { + $key = $config->keys[$keyset][0]['secret']; } else { - $key = $config->keys[$keyset][0]['private']; + $passphrase = $config->keys[$keyset][0]['passphrase'] ?? ''; + + if ($passphrase !== '') { + $key = openssl_pkey_get_private( + $config->keys[$keyset][0]['private'], + $passphrase + ); + } else { + $key = $config->keys[$keyset][0]['private']; + } } - } - $algorithm = $config->keys[$keyset][0]['alg']; + $algorithm = $config->keys[$keyset][0]['alg']; - $keyId = $config->keys[$keyset][0]['kid'] ?? null; - if ($keyId === '') { - $keyId = null; - } + $keyId = $config->keys[$keyset][0]['kid'] ?? null; + if ($keyId === '') { + $keyId = null; + } - return JWT::encode($payload, $key, $algorithm, $keyId, $headers); + return JWT::encode($payload, $key, $algorithm, $keyId, $headers); + } catch (LogicException $e) { + // errors having to do with environmental setup or malformed JWT Keys + throw new ShieldLogicException('Cannot encode JWT: ' . $e->getMessage(), 0, $e); + } catch (UnexpectedValueException $e) { + // errors having to do with JWT signature and claims + throw new ShieldLogicException('Cannot encode JWT: ' . $e->getMessage(), 0, $e); + } } } From 900408277575bb1e431e503315770a3f92316740 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 16:16:31 +0900 Subject: [PATCH 107/142] refactor: extract method --- .../JWT/Adapters/FirebaseAdapter.php | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index 6ee2b6b90..c6474bd18 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -108,29 +108,7 @@ private static function createKeysForDecode($keyset) public static function encode(array $payload, $keyset, ?array $headers = null): string { try { - $config = config(AuthJWT::class); - - if (isset($config->keys[$keyset][0]['secret'])) { - $key = $config->keys[$keyset][0]['secret']; - } else { - $passphrase = $config->keys[$keyset][0]['passphrase'] ?? ''; - - if ($passphrase !== '') { - $key = openssl_pkey_get_private( - $config->keys[$keyset][0]['private'], - $passphrase - ); - } else { - $key = $config->keys[$keyset][0]['private']; - } - } - - $algorithm = $config->keys[$keyset][0]['alg']; - - $keyId = $config->keys[$keyset][0]['kid'] ?? null; - if ($keyId === '') { - $keyId = null; - } + [$key, $keyId, $algorithm] = self::createKeysForEncode($keyset); return JWT::encode($payload, $key, $algorithm, $keyId, $headers); } catch (LogicException $e) { @@ -141,4 +119,38 @@ public static function encode(array $payload, $keyset, ?array $headers = null): throw new ShieldLogicException('Cannot encode JWT: ' . $e->getMessage(), 0, $e); } } + + /** + * Creates keys for Encode + * + * @param string $keyset + */ + private static function createKeysForEncode($keyset): array + { + $config = config(AuthJWT::class); + + if (isset($config->keys[$keyset][0]['secret'])) { + $key = $config->keys[$keyset][0]['secret']; + } else { + $passphrase = $config->keys[$keyset][0]['passphrase'] ?? ''; + + if ($passphrase !== '') { + $key = openssl_pkey_get_private( + $config->keys[$keyset][0]['private'], + $passphrase + ); + } else { + $key = $config->keys[$keyset][0]['private']; + } + } + + $algorithm = $config->keys[$keyset][0]['alg']; + + $keyId = $config->keys[$keyset][0]['kid'] ?? null; + if ($keyId === '') { + $keyId = null; + } + + return [$key, $keyId, $algorithm]; + } } From 48f3a514f7ae89c6ad9073c7c122b61eed7d3db6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 16:31:23 +0900 Subject: [PATCH 108/142] test: add test for encode --- .../JWT/Adapters/FirebaseAdapaterTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index 9a3cbe7aa..2b6e0185a 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -11,6 +11,7 @@ use CodeIgniter\Shield\Config\AuthJWT; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\InvalidArgumentException as ShieldInvalidArgumentException; +use CodeIgniter\Shield\Exceptions\LogicException as ShieldLogicException; use CodeIgniter\Shield\Models\UserModel; use Tests\Support\TestCase; use UnexpectedValueException; @@ -120,5 +121,25 @@ public function testDecodeInvalidArgumentException(): void $key = 'default'; $jwtDecoder->decode($token, $key); + + public function testEncodeLogicExceptionLogicException(): void + { + $this->expectException(ShieldLogicException::class); + $this->expectExceptionMessage('Cannot encode JWT: Algorithm not supported'); + + // Set unsupported algorithm. + $config = config(AuthJWT::class); + $config->keys['default'][0]['alg'] = 'PS256'; + + $adapter = new FirebaseAdapter(); + + $key = 'default'; + $payload = [ + 'iss' => 'http://example.org', + 'aud' => 'http://example.com', + 'iat' => 1356999524, + 'nbf' => 1357000000, + ]; + $adapter->encode($payload, $key); } } From 9179d8c1ef45d0dfcb0296c5507a2bfe63ce30f4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 16:31:45 +0900 Subject: [PATCH 109/142] refactor: update variable name --- .../JWT/Adapters/FirebaseAdapaterTest.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index 2b6e0185a..b7c109269 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -25,14 +25,13 @@ public function testDecode(): void { $token = $this->generateJWT(); - $jwtDecoder = new FirebaseAdapter(); + $adapter = new FirebaseAdapter(); /** @var AuthJWT $config */ $config = config('AuthJWT'); - $key = 'default'; - - $payload = $jwtDecoder->decode($token, $key); + $key = 'default'; + $payload = $adapter->decode($token, $key); $this->assertSame($config->defaultClaims['iss'], $payload->iss); $this->assertSame('1', $payload->sub); @@ -56,11 +55,11 @@ public function testDecodeSignatureInvalidException(): void $this->expectException(InvalidTokenException::class); $this->expectExceptionMessage(lang('Auth.invalidJWT')); - $jwtDecoder = new FirebaseAdapter(); + $adapter = new FirebaseAdapter(); $key = 'default'; $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJc3N1ZXIgb2YgdGhlIEpXVCIsImF1ZCI6IkF1ZGllbmNlIG9mIHRoZSBKV1QiLCJzdWIiOiIxIiwiaWF0IjoxNjUzOTkxOTg5LCJleHAiOjE2NTM5OTU1ODl9.hgOYHEcT6RGHb3po1lspTcmjrylY1Cy1IvYmHOyx0CY'; - $jwtDecoder->decode($token, $key); + $adapter->decode($token, $key); } public function testDecodeExpiredToken(): void @@ -68,14 +67,14 @@ public function testDecodeExpiredToken(): void $this->expectException(InvalidTokenException::class); $this->expectExceptionMessage(lang('Auth.expiredJWT')); - $jwtDecoder = new FirebaseAdapter(); + $adapter = new FirebaseAdapter(); Time::setTestNow('-1 hour'); $token = $this->generateJWT(); Time::setTestNow(); $key = 'default'; - $jwtDecoder->decode($token, $key); + $adapter->decode($token, $key); } public function testDecodeInvalidTokenExceptionUnexpectedValueException(): void @@ -86,11 +85,11 @@ public function testDecodeInvalidTokenExceptionUnexpectedValueException(): void $config = config(AuthJWT::class); $config->keys['default'][0]['alg'] = 'ES256'; - $jwtDecoder = new FirebaseAdapter(); + $adapter = new FirebaseAdapter(); try { $key = 'default'; - $jwtDecoder->decode($token, $key); + $adapter->decode($token, $key); } catch (InvalidTokenException $e) { $prevException = $e->getPrevious(); @@ -117,10 +116,11 @@ public function testDecodeInvalidArgumentException(): void 'secret' => '', ]; - $jwtDecoder = new FirebaseAdapter(); + $adapter = new FirebaseAdapter(); $key = 'default'; - $jwtDecoder->decode($token, $key); + $adapter->decode($token, $key); + } public function testEncodeLogicExceptionLogicException(): void { From 1acb4499902fd3df36fce6717dce44e39aa2a8d1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 16:36:23 +0900 Subject: [PATCH 110/142] refactor: by rector --- .../Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php index b7c109269..9618c151e 100644 --- a/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php +++ b/tests/Unit/Authentication/JWT/Adapters/FirebaseAdapaterTest.php @@ -137,8 +137,8 @@ public function testEncodeLogicExceptionLogicException(): void $payload = [ 'iss' => 'http://example.org', 'aud' => 'http://example.com', - 'iat' => 1356999524, - 'nbf' => 1357000000, + 'iat' => 1_356_999_524, + 'nbf' => 1_357_000_000, ]; $adapter->encode($payload, $key); } From 168aaef4d46395a7beb59c1dd717d7539236081b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 17:25:41 +0900 Subject: [PATCH 111/142] docs: add sample login controller and filter settings --- docs/addons/jwt.md | 132 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 3 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 74a832bf5..f1b1e2547 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -92,9 +92,135 @@ php -r 'echo base64_encode(random_bytes(32));' > **Note** > The secret key is used for signing and validating tokens. -## Generating Signed JWTs +## Issuing JWTs -### JWT to a Specific User +To use JWT Authentication, you need a controller that issues JWTs. + +Here is a sample controller. When a client posts valid credentials (email/password), +it returns a new JWT. + +```php +// Routes.php +$routes->post('auth/jwt', '\App\Controllers\Auth\LoginController::jwtLogin'); + +// LoginController.php +namespace App\Controllers\Auth; + +use App\Controllers\BaseController; +use CodeIgniter\API\ResponseTrait; +use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\Shield\Authentication\Authenticators\Session; +use CodeIgniter\Shield\Authentication\JWTManager; +use CodeIgniter\Shield\Authentication\Passwords; +use CodeIgniter\Shield\Config\AuthSession; + +class LoginController extends BaseController +{ + use ResponseTrait; + + /** + * Authenticate Existing User and Issue JWT. + */ + public function jwtLogin(): ResponseInterface + { + // Get the validation rules + $rules = $this->getValidationRules(); + + // Validate credentials + if (! $this->validateData($this->request->getPost(), $rules)) { + return $this->failValidationErrors($this->validator->getErrors(), 422); + } + + // Get the credentials for login + $credentials = $this->request->getPost(setting('Auth.validFields')); + $credentials = array_filter($credentials); + $credentials['password'] = $this->request->getPost('password'); + + /** @var Session $authenticator */ + $authenticator = auth('session')->getAuthenticator(); + + // Check the credentials + $result = $authenticator->check($credentials); + if (! $result->isOK()) { + return $this->failUnauthorized($result->reason()); + } + + // Login is successful. + $user = $result->extraInfo(); + + /** @var JWTManager $manager */ + $manager = service('jwtmanager'); + + // Generate JWT and return to client + $jwt = $manager->generateToken($user); + + return $this->respond([ + 'access_token' => $jwt, + ]); + } + + /** + * Returns the rules that should be used for validation. + * + * @return array|string>> + * @phpstan-return array>> + */ + protected function getValidationRules(): array + { + return setting('Validation.login') ?? [ + 'email' => [ + 'label' => 'Auth.email', + 'rules' => config(AuthSession::class)->emailValidationRules, + ], + 'password' => [ + 'label' => 'Auth.password', + 'rules' => 'required|' . Passwords::getMaxLenghtRule(), + 'errors' => [ + 'max_byte' => 'Auth.errorPasswordTooLongBytes', + ], + ], + ]; + } +} +``` + +When making all future requests to the API, the client should send the JWT in +the `Authorization` header as a `Bearer` token. + +## Protecting Routes + +The first way to specify which routes are protected is to use the `jwt` controller +filter. + +For example, to ensure it protects all routes under the `/api` route group, you +would use the `$filters` setting on **app/Config/Filters.php**. + +```php +public $filters = [ + 'jwt' => ['before' => ['api/*']], +]; +``` + +You can also specify the filter should run on one or more routes within the routes +file itself: + +```php +$routes->group('api', ['filter' => 'jwt'], static function ($routes) { + // ... +}); +$routes->get('users', 'UserController::list', ['filter' => 'jwt']); +``` + +When the filter runs, it checks the `Authorization` header for a `Bearer` value +that has the JWT. It then validates the token. If the token is valid, it can +determine the correct user, which will then be available through an `auth()->user()` +call. + +## Method References + +### Generating Signed JWTs + +#### JWT to a Specific User JWTs are created through the `JWTManager::generateToken()` method. This takes a User object to give to the token as the first argument. @@ -132,7 +258,7 @@ the `'email'` claim and the user ID in the `"sub"` (subject) claim. It also sets `"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically if you don't specify. -### Arbitrary JWT +#### Arbitrary JWT You can generate arbitrary JWT with the ``JWTManager::issue()`` method. From ce3e0d0721aed838f72b62d103606d30d27ca8d7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 17:39:55 +0900 Subject: [PATCH 112/142] docs: add sample commands by curl --- docs/addons/jwt.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index f1b1e2547..e65c68444 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -184,9 +184,25 @@ class LoginController extends BaseController } ``` +You could send a request with the existing user's credentials by curl like this: + +```console +curl --location 'http://localhost:8080/auth/jwt' \ +--header 'Content-Type: application/x-www-form-urlencoded' \ +--data-urlencode 'email=user1@example.jp' \ +--data-urlencode 'password=passw0rd!' +``` + When making all future requests to the API, the client should send the JWT in the `Authorization` header as a `Bearer` token. +You could send a request with the `Authorization` header by curl like this: + +```console +curl --location --request GET 'http://localhost:8080/api/users' \ +--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTaGllbGQgVGVzdCBBcHAiLCJzdWIiOiIxIiwiaWF0IjoxNjgxODA1OTMwLCJleHAiOjE2ODE4MDk1MzB9.DGpOmRPOBe45whVtEOSt53qJTw_CpH0V8oMoI_gm2XI' +``` + ## Protecting Routes The first way to specify which routes are protected is to use the `jwt` controller From 565e81ca61ba26133c9518481cbf65d5e06cc3a9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 18 Apr 2023 17:51:54 +0900 Subject: [PATCH 113/142] refactor: remove `static` from JWSAdapterInterface.php --- .../JWT/Adapters/FirebaseAdapter.php | 14 +++++++------- src/Authentication/JWT/JWSAdapterInterface.php | 8 ++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Authentication/JWT/Adapters/FirebaseAdapter.php b/src/Authentication/JWT/Adapters/FirebaseAdapter.php index c6474bd18..3a246ed3f 100644 --- a/src/Authentication/JWT/Adapters/FirebaseAdapter.php +++ b/src/Authentication/JWT/Adapters/FirebaseAdapter.php @@ -25,10 +25,10 @@ class FirebaseAdapter implements JWSAdapterInterface /** * {@inheritDoc} */ - public static function decode(string $encodedToken, $keyset): stdClass + public function decode(string $encodedToken, $keyset): stdClass { try { - $keys = self::createKeysForDecode($keyset); + $keys = $this->createKeysForDecode($keyset); return JWT::decode($encodedToken, $keys); } catch (InvalidArgumentException $e) { @@ -61,7 +61,7 @@ public static function decode(string $encodedToken, $keyset): stdClass // provided key ID in key/key-array is empty or invalid. log_message( 'error', - '[Shield] ' . class_basename(self::class) . '::' . __FUNCTION__ + '[Shield] ' . class_basename($this) . '::' . __FUNCTION__ . '(' . __LINE__ . ') ' . get_class($e) . ': ' . $e->getMessage() ); @@ -77,7 +77,7 @@ public static function decode(string $encodedToken, $keyset): stdClass * * @return array|Key key or key array */ - private static function createKeysForDecode($keyset) + private function createKeysForDecode($keyset) { $config = config(AuthJWT::class); @@ -105,10 +105,10 @@ private static function createKeysForDecode($keyset) /** * {@inheritDoc} */ - public static function encode(array $payload, $keyset, ?array $headers = null): string + public function encode(array $payload, $keyset, ?array $headers = null): string { try { - [$key, $keyId, $algorithm] = self::createKeysForEncode($keyset); + [$key, $keyId, $algorithm] = $this->createKeysForEncode($keyset); return JWT::encode($payload, $key, $algorithm, $keyId, $headers); } catch (LogicException $e) { @@ -125,7 +125,7 @@ public static function encode(array $payload, $keyset, ?array $headers = null): * * @param string $keyset */ - private static function createKeysForEncode($keyset): array + private function createKeysForEncode($keyset): array { $config = config(AuthJWT::class); diff --git a/src/Authentication/JWT/JWSAdapterInterface.php b/src/Authentication/JWT/JWSAdapterInterface.php index 5cdc03171..991fdabba 100644 --- a/src/Authentication/JWT/JWSAdapterInterface.php +++ b/src/Authentication/JWT/JWSAdapterInterface.php @@ -18,11 +18,7 @@ interface JWSAdapterInterface * * @return string JWT (JWS) */ - public static function encode( - array $payload, - $keyset, - ?array $headers = null - ): string; + public function encode(array $payload, $keyset, ?array $headers = null): string; /** * Decode Signed JWT (JWS) @@ -31,5 +27,5 @@ public static function encode( * * @return stdClass Payload */ - public static function decode(string $encodedToken, $keyset): stdClass; + public function decode(string $encodedToken, $keyset): stdClass; } From 04d66455f7fd1a471ac8e6082fa135a5d1f30a1c Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 19 Apr 2023 12:51:35 +0900 Subject: [PATCH 114/142] refactor: use class constants for exception code --- .../JWT/Exceptions/InvalidTokenException.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Authentication/JWT/Exceptions/InvalidTokenException.php b/src/Authentication/JWT/Exceptions/InvalidTokenException.php index b455dcfd0..b3f9b5569 100644 --- a/src/Authentication/JWT/Exceptions/InvalidTokenException.php +++ b/src/Authentication/JWT/Exceptions/InvalidTokenException.php @@ -9,18 +9,22 @@ class InvalidTokenException extends ValidationException { + public const INVALID_TOKEN = 1; + public const EXPIRED_TOKEN = 2; + public const BEFORE_VALID_TOKEN = 3; + public static function forInvalidToken(Exception $e): self { - return new self(lang('Auth.invalidJWT'), 1, $e); + return new self(lang('Auth.invalidJWT'), self::INVALID_TOKEN, $e); } public static function forExpiredToken(Exception $e): self { - return new self(lang('Auth.expiredJWT'), 2, $e); + return new self(lang('Auth.expiredJWT'), self::EXPIRED_TOKEN, $e); } public static function forBeforeValidToken(Exception $e): self { - return new self(lang('Auth.beforeValidJWT'), 3, $e); + return new self(lang('Auth.beforeValidJWT'), self::BEFORE_VALID_TOKEN, $e); } } From 47546bb3d9c8477c870d724d33e1b03fda57037f Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 19 Apr 2023 12:53:02 +0900 Subject: [PATCH 115/142] test: update test method names --- tests/Unit/Authentication/JWT/JWTManagerTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Authentication/JWT/JWTManagerTest.php b/tests/Unit/Authentication/JWT/JWTManagerTest.php index e746aff54..7f5adfc1a 100644 --- a/tests/Unit/Authentication/JWT/JWTManagerTest.php +++ b/tests/Unit/Authentication/JWT/JWTManagerTest.php @@ -21,7 +21,7 @@ private function createJWTManager(?Time $clock = null): JWTManager return new JWTManager($clock); } - public function testGenerateAccessToken() + public function testGenerateToken() { /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); @@ -46,9 +46,9 @@ public function testGenerateAccessToken() } /** - * @depends testGenerateAccessToken + * @depends testGenerateToken */ - public function testGenerateAccessTokenPayload(array $data): void + public function testGenerateTokenPayload(array $data): void { [$token, $currentTime] = $data; @@ -65,7 +65,7 @@ public function testGenerateAccessTokenPayload(array $data): void $this->assertSame($expected, (array) $payload); } - public function testGenerateAccessTokenAddClaims(): void + public function testGenerateTokenAddClaims(): void { /** @var User $user */ $user = fake(UserModel::class, ['id' => 1, 'username' => 'John Smith'], false); From 64b6f0000b70f5f081a04e1861358096b556e548 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 19 Apr 2023 14:59:35 +0900 Subject: [PATCH 116/142] docs: improve explanation Co-authored-by: Michal Sniatala --- docs/addons/jwt.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index e65c68444..e7d2ceb8a 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -82,7 +82,10 @@ E.g.: authjwt.keys.default.0.secret = 8XBFsF6HThIa7OV/bSynahEch+WbKrGcuiJVYPiwqPE= ``` -It needs more than 256 bits random string. You can get a secure random string +It needs at least 256 bits random string. The length of the secret depends on the +algorithm we use. The default one is `HS256`, so to ensure that the hash value is +secure and not easily guessable, the secret key should be at least as long as the +hash function's output - 256 bits (32 bytes). You can get a secure random string with the following command: ```console From 28453566f9bd9a9a389576fef9f67135df4b3b4b Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 19 Apr 2023 15:01:10 +0900 Subject: [PATCH 117/142] docs: fix ambiguous descriptions Co-authored-by: Michal Sniatala --- src/Config/AuthJWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/AuthJWT.php b/src/Config/AuthJWT.php index ca6087ad3..adf9bf404 100644 --- a/src/Config/AuthJWT.php +++ b/src/Config/AuthJWT.php @@ -49,7 +49,7 @@ class AuthJWT extends BaseConfig [ 'kid' => '', // Key ID. Optional if you have only one key. 'alg' => 'HS256', // algorithm. - // Set secret random string. Needs more than 256 bits. + // Set secret random string. Needs at least 256 bits for HS256 algorithm. // E.g., $ php -r 'echo base64_encode(random_bytes(32));' 'secret' => '', ], From a3efa8269f2a6edb749fe5f78399266140ded266 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 08:36:36 +0900 Subject: [PATCH 118/142] docs: split code block --- docs/addons/jwt.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index e7d2ceb8a..ec3246166 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -103,10 +103,12 @@ Here is a sample controller. When a client posts valid credentials (email/passwo it returns a new JWT. ```php -// Routes.php +// app/Config/Routes.php $routes->post('auth/jwt', '\App\Controllers\Auth\LoginController::jwtLogin'); +``` -// LoginController.php +```php +// app/Controllers/Auth/LoginController.php namespace App\Controllers\Auth; use App\Controllers\BaseController; From 4aa454c1e98209752b10e07205e4d2d982f7f042 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 08:38:38 +0900 Subject: [PATCH 119/142] docs: remove spaces at the end of lines --- docs/addons/jwt.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index ec3246166..e9f2df09a 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -82,10 +82,10 @@ E.g.: authjwt.keys.default.0.secret = 8XBFsF6HThIa7OV/bSynahEch+WbKrGcuiJVYPiwqPE= ``` -It needs at least 256 bits random string. The length of the secret depends on the -algorithm we use. The default one is `HS256`, so to ensure that the hash value is -secure and not easily guessable, the secret key should be at least as long as the -hash function's output - 256 bits (32 bytes). You can get a secure random string +It needs at least 256 bits random string. The length of the secret depends on the +algorithm we use. The default one is `HS256`, so to ensure that the hash value is +secure and not easily guessable, the secret key should be at least as long as the +hash function's output - 256 bits (32 bytes). You can get a secure random string with the following command: ```console From 63a6fdf0a24a7e3600df795e94f401c95bca5b3c Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 08:53:28 +0900 Subject: [PATCH 120/142] docs: add declare(strict_types=1) to controller --- docs/addons/jwt.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index e9f2df09a..3ad257468 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -109,6 +109,8 @@ $routes->post('auth/jwt', '\App\Controllers\Auth\LoginController::jwtLogin'); ```php // app/Controllers/Auth/LoginController.php +declare(strict_types=1); + namespace App\Controllers\Auth; use App\Controllers\BaseController; From 37d8c71d9fbee223e5b849c4d49832d20dc87444 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 09:06:17 +0900 Subject: [PATCH 121/142] docs: fix sample controller code --- docs/addons/jwt.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 3ad257468..701f886ec 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -135,7 +135,10 @@ class LoginController extends BaseController // Validate credentials if (! $this->validateData($this->request->getPost(), $rules)) { - return $this->failValidationErrors($this->validator->getErrors(), 422); + return $this->fail( + ['errors' => $this->validator->getErrors()], + $this->codes['unauthorized'] + ); } // Get the credentials for login From 54ee18b891f0f2f9139bdc512a82afaf15ad344b Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 09:45:25 +0900 Subject: [PATCH 122/142] docs: add @TODO in sample code --- docs/addons/jwt.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 701f886ec..8fc029d1d 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -151,11 +151,17 @@ class LoginController extends BaseController // Check the credentials $result = $authenticator->check($credentials); + + // Credentials mismatch. if (! $result->isOK()) { + // @TODO Record a failed login attempt + return $this->failUnauthorized($result->reason()); } - // Login is successful. + // Credentials match. + // @TODO Record a successful login attempt + $user = $result->extraInfo(); /** @var JWTManager $manager */ From 6bf2eef35f34503cfb7cb5aec5a67049a569bfbf Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 09:58:04 +0900 Subject: [PATCH 123/142] lang: add translations Co-authored-by: Pooya Parsa Dadashi --- src/Language/fa/Auth.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Language/fa/Auth.php b/src/Language/fa/Auth.php index ba2fe9c7d..26d242525 100644 --- a/src/Language/fa/Auth.php +++ b/src/Language/fa/Auth.php @@ -21,9 +21,9 @@ 'throttled' => 'درخواست های بسیار زیادی از این آدرس IP انجام شده است. می توانید بعد از {0} ثانیه دوباره امتحان کنید.', 'notEnoughPrivilege' => 'شما مجوز لازم برای انجام عملیات مورد نظر را ندارید.', // JWT Exceptions - 'invalidJWT' => '(To be translated) The token is invalid.', - 'expiredJWT' => '(To be translated) The token has expired.', - 'beforeValidJWT' => '(To be translated) The token is not yet available.', + 'invalidJWT' => 'توکن معتبر نمی باشد.', + 'expiredJWT' => 'توکن منقضی شده است.', + 'beforeValidJWT' => 'در حال حاضر امکان استفاده از توکن وجود ندارد.', 'email' => 'آدرس ایمیل', 'username' => 'نام کاربری', From 3117c17eb5000ca040b6d4ebbbafff88a9cb0e66 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 10:15:27 +0900 Subject: [PATCH 124/142] feat: do not login banned users --- src/Authentication/Authenticators/JWT.php | 21 +++++++++++++++++ .../Authenticators/JWTAuthenticatorTest.php | 23 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Authentication/Authenticators/JWT.php b/src/Authentication/Authenticators/JWT.php index 25ba59fe0..c223bdbc7 100644 --- a/src/Authentication/Authenticators/JWT.php +++ b/src/Authentication/Authenticators/JWT.php @@ -88,6 +88,27 @@ public function attempt(array $credentials): Result $user = $result->extraInfo(); + if ($user->isBanned()) { + if ($config->recordLoginAttempt >= Auth::RECORD_LOGIN_ATTEMPT_FAILURE) { + // Record a banned login attempt. + $this->tokenLoginModel->recordLoginAttempt( + self::ID_TYPE_JWT, + $credentials['token'] ?? '', + false, + $ipAddress, + $userAgent, + $user->id + ); + } + + $this->user = null; + + return new Result([ + 'success' => false, + 'reason' => $user->getBanMessage() ?? lang('Auth.bannedUser'), + ]); + } + $this->login($user); if ($config->recordLoginAttempt === Auth::RECORD_LOGIN_ATTEMPT_ALL) { diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index d8a29a184..65300ef87 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -198,6 +198,29 @@ public function testAttemptBadSignatureToken(): void ]); } + public function testAttemptBannedUser(): void + { + $token = $this->generateJWT(); + + $this->user->ban(); + + $result = $this->auth->attempt([ + 'token' => $token, + ]); + + $this->assertInstanceOf(Result::class, $result); + $this->assertFalse($result->isOK()); + $this->assertSame(lang('Auth.bannedUser'), $result->reason()); + + // The login attempt should have been recorded + $this->seeInDatabase('auth_token_logins', [ + 'id_type' => JWT::ID_TYPE_JWT, + 'identifier' => $token, + 'success' => 0, + 'user_id' => $this->user->id, + ]); + } + public function testAttemptSuccess(): void { // Change $recordLoginAttempt in Config. From 72047b7c310f470f3b24ab15d05edda1dc2aa408 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 11:12:31 +0900 Subject: [PATCH 125/142] docs: use JSON request --- docs/addons/jwt.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 8fc029d1d..06d8e9b28 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -134,7 +134,7 @@ class LoginController extends BaseController $rules = $this->getValidationRules(); // Validate credentials - if (! $this->validateData($this->request->getPost(), $rules)) { + if (! $this->validateData($this->request->getJSON(true), $rules)) { return $this->fail( ['errors' => $this->validator->getErrors()], $this->codes['unauthorized'] @@ -142,9 +142,9 @@ class LoginController extends BaseController } // Get the credentials for login - $credentials = $this->request->getPost(setting('Auth.validFields')); + $credentials = $this->request->getJsonVar(setting('Auth.validFields')); $credentials = array_filter($credentials); - $credentials['password'] = $this->request->getPost('password'); + $credentials['password'] = $this->request->getJsonVar('password'); /** @var Session $authenticator */ $authenticator = auth('session')->getAuthenticator(); @@ -203,10 +203,9 @@ class LoginController extends BaseController You could send a request with the existing user's credentials by curl like this: ```console -curl --location 'http://localhost:8080/auth/jwt' \ ---header 'Content-Type: application/x-www-form-urlencoded' \ ---data-urlencode 'email=user1@example.jp' \ ---data-urlencode 'password=passw0rd!' +$ curl --location 'http://localhost:8080/auth/jwt' \ +--header 'Content-Type: application/json' \ +--data-raw '{"email" : "admin@example.jp" , "password" : "passw0rd!"}' ``` When making all future requests to the API, the client should send the JWT in From dcbdb2895dbe6c8fcb66438aac0cffac1090854b Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 10:57:11 +0900 Subject: [PATCH 126/142] config: comment out jwt authenticator It is an optional authenticator. --- docs/addons/jwt.md | 29 +++++++++++++++++++ src/Config/Auth.php | 3 +- .../Authenticators/JWTAuthenticatorTest.php | 6 ++-- .../Authentication/Filters/JWTFilterTest.php | 6 ++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 06d8e9b28..44bf4a198 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -52,6 +52,35 @@ To use JWT Authentication, you need additional setup and configuration. 3. If your **app/Config/Auth.php** is not up-to-date, you also need to update it. Check **vendor/codeigniter4/shield/src/Config/Auth.php** and apply the differences. + You need to add the following constants: + ```php + public const RECORD_LOGIN_ATTEMPT_NONE = 0; // Do not record at all + public const RECORD_LOGIN_ATTEMPT_FAILURE = 1; // Record only failures + public const RECORD_LOGIN_ATTEMPT_ALL = 2; // Record all login attempts + ``` + + You need to add JWT Authenticator: + ```php + use CodeIgniter\Shield\Authentication\Authenticators\JWT; + + // ... + + public array $authenticators = [ + 'tokens' => AccessTokens::class, + 'session' => Session::class, + 'jwt' => JWT::class, + ]; + ``` + + If you want to use JWT Authenticator in Authentication Chain, add `jwt`: + ```php + public array $authenticationChain = [ + 'session', + 'tokens', + 'jwt' + ]; + ``` + ## Configuration Configure **app/Config/AuthJWT.php** for your needs. diff --git a/src/Config/Auth.php b/src/Config/Auth.php index a90360449..b4d9ebae0 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -127,7 +127,7 @@ class Auth extends BaseConfig public array $authenticators = [ 'tokens' => AccessTokens::class, 'session' => Session::class, - 'jwt' => JWT::class, + // 'jwt' => JWT::class, ]; /** @@ -174,6 +174,7 @@ class Auth extends BaseConfig public array $authenticationChain = [ 'session', 'tokens', + // 'jwt', ]; /** diff --git a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php index 65300ef87..5c074df12 100644 --- a/tests/Authentication/Authenticators/JWTAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/JWTAuthenticatorTest.php @@ -34,8 +34,10 @@ protected function setUp(): void { parent::setUp(); - $config = new Auth(); - $auth = new Authentication($config); + $config = new Auth(); + $config->authenticators['jwt'] = JWT::class; + + $auth = new Authentication($config); $auth->setProvider(\model(UserModel::class)); /** @var JWT $authenticator */ diff --git a/tests/Authentication/Filters/JWTFilterTest.php b/tests/Authentication/Filters/JWTFilterTest.php index e4ba69530..082627888 100644 --- a/tests/Authentication/Filters/JWTFilterTest.php +++ b/tests/Authentication/Filters/JWTFilterTest.php @@ -5,7 +5,9 @@ namespace Tests\Authentication\Filters; use CodeIgniter\Config\Factories; +use CodeIgniter\Shield\Authentication\Authenticators\JWT; use CodeIgniter\Shield\Authentication\JWTManager; +use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Filters\JWTAuth; use CodeIgniter\Shield\Models\UserModel; @@ -30,6 +32,10 @@ protected function setUp(): void $_SESSION = []; + // Add JWT Authenticator + $config = config(Auth::class); + $config->authenticators['jwt'] = JWT::class; + // Register our filter $filterConfig = \config('Filters'); $filterConfig->aliases['jwtAuth'] = JWTAuth::class; From 1b9fdf971ab763dbc4196caf1ab3fa1b12a17f80 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 11:40:56 +0900 Subject: [PATCH 127/142] docs: add JWT in README --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b148ebdd..73b4f360c 100644 --- a/README.md +++ b/README.md @@ -15,20 +15,29 @@ The primary goals for Shield are: ## Authentication Methods -Shield provides two primary methods of authentication out of the box: +Shield provides two primary methods **Session-based** and **Personal Access Codes** +of authentication out of the box. -**Session-based** +It also provides **JSON Web Tokens** authentication. + +### Session-based This is your typical email/username/password system you see everywhere. It includes a secure "remember me" functionality. This can be used for standard web applications, as well as for single page applications. Includes full controllers and basic views for all standard functionality, like registration, login, forgot password, etc. -**Personal Access Codes** +### Personal Access Codes These are much like the access codes that GitHub uses, where they are unique to a single user, and a single user can have more than one. This can be used for API authentication of third-party users, and even for allowing access for a mobile application that you build. +### JSON Web Tokens + +JWT or JSON Web Token is a compact and self-contained way of securely transmitting +information between parties as a JSON object. It is commonly used for authentication +and authorization purposes in web applications. + ## Some Important Features * Session-based authentication (traditional email/password with remember me) From d1b67007ba8dc66a4d37a75906b2708ee3fd939a Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 21 Apr 2023 18:24:08 +0900 Subject: [PATCH 128/142] docs: change 422 to 401 There is no need to change the status code since a validation error is still an authentication failure. See https://github.com/codeigniter4/shield/pull/195#discussion_r1173193039 --- docs/guides/mobile_apps.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/mobile_apps.md b/docs/guides/mobile_apps.md index 4a22b81d5..237eeeafd 100644 --- a/docs/guides/mobile_apps.md +++ b/docs/guides/mobile_apps.md @@ -41,14 +41,14 @@ class LoginController extends BaseController if (! $this->validateData($this->request->getPost(), $rules)) { return $this->response ->setJSON(['errors' => $this->validator->getErrors()]) - ->setStatusCode(422); + ->setStatusCode(401); } // Get the credentials for login $credentials = $this->request->getPost(setting('Auth.validFields')); $credentials = array_filter($credentials); $credentials['password'] = $this->request->getPost('password'); - + // Attempt to login $result = auth()->attempt($credentials); if (! $result->isOK()) { From 422e9f751cf5c9d7a31cf456d7603c8614c8299f Mon Sep 17 00:00:00 2001 From: Robson Piere <11904136+robsonpiere@users.noreply.github.com> Date: Fri, 21 Apr 2023 19:01:26 -0300 Subject: [PATCH 129/142] lang: Add pt-BR translation for JWT auth --- src/Language/pt-BR/Auth.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Language/pt-BR/Auth.php b/src/Language/pt-BR/Auth.php index e98ff1745..b2506bcdd 100644 --- a/src/Language/pt-BR/Auth.php +++ b/src/Language/pt-BR/Auth.php @@ -21,9 +21,9 @@ 'throttled' => 'Muitas solicitações feitas a partir deste endereço IP. Você pode tentar novamente em {0} segundos.', 'notEnoughPrivilege' => 'Você não tem a permissão necessária para realizar a operação desejada.', // JWT Exceptions - 'invalidJWT' => '(To be translated) The token is invalid.', - 'expiredJWT' => '(To be translated) The token has expired.', - 'beforeValidJWT' => '(To be translated) The token is not yet available.', + 'invalidJWT' => 'O token é inválido.', + 'expiredJWT' => 'O token expirou.', + 'beforeValidJWT' => 'O token ainda não está disponível.', 'email' => 'Endereço de Email', 'username' => 'Nome de usuário', From 20f8173f3126b4ffa5c8af7d708cc90985e57763 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Apr 2023 11:32:25 +0900 Subject: [PATCH 130/142] docs: remove command prompt $ --- docs/addons/jwt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index 44bf4a198..fd8ca5210 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -232,7 +232,7 @@ class LoginController extends BaseController You could send a request with the existing user's credentials by curl like this: ```console -$ curl --location 'http://localhost:8080/auth/jwt' \ +curl --location 'http://localhost:8080/auth/jwt' \ --header 'Content-Type: application/json' \ --data-raw '{"email" : "admin@example.jp" , "password" : "passw0rd!"}' ``` From 370d6aece7afee87f29fce61ababbd52074ee616 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Apr 2023 11:31:21 +0900 Subject: [PATCH 131/142] docs: fix sample code to protect routes Because the route `/api` was not protected. --- docs/addons/jwt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/addons/jwt.md b/docs/addons/jwt.md index fd8ca5210..efcc54c8c 100644 --- a/docs/addons/jwt.md +++ b/docs/addons/jwt.md @@ -257,7 +257,7 @@ would use the `$filters` setting on **app/Config/Filters.php**. ```php public $filters = [ - 'jwt' => ['before' => ['api/*']], + 'jwt' => ['before' => ['api', 'api/*']], ]; ``` From 975b233b8cde51cade87d61addcd9e92ccccd4f4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Apr 2023 18:29:28 +0900 Subject: [PATCH 132/142] chore: add script to update en comments --- bin/update-en-comments | 107 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100755 bin/update-en-comments diff --git a/bin/update-en-comments b/bin/update-en-comments new file mode 100755 index 000000000..12d3c4f60 --- /dev/null +++ b/bin/update-en-comments @@ -0,0 +1,107 @@ +#!/usr/bin/env php + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +require __DIR__ . '/../vendor/codeigniter4/framework/system/Test/bootstrap.php'; + +use CodeIgniter\CLI\CLI; + +helper('filesystem'); + +if ($argc !== 2) { + CLI::error('Please specify a locale.'); + + exit(1); +} + +$locale = $argv[1]; + +$langDir = realpath(__DIR__ . '/../src/Language/' . $locale); + +if (! is_dir($langDir)) { + CLI::error('No such directory: "' . $langDir . '"'); + + exit(1); +} + +$enDir = realpath(__DIR__ . '/../src/Language/en'); + +if (! is_dir($enDir)) { + CLI::error('No "Language/en" directory. Please run "composer update".'); + + exit(1); +} + +$files = get_filenames( + $langDir, + true, + false, + false +); + +$enFiles = get_filenames( + $enDir, + true, + false, + false +); + +foreach ($enFiles as $enFile) { + $temp = $langDir . '/' . substr($enFile, strlen($enDir) + 1); + $langFile = realpath($temp) ?: $temp; + + if (! is_file($langFile)) { + CLI::error('No such file: "' . $langFile . '"'); + + continue; + } + + $enFileLines = file($enFile); + + $items = []; + + $pattern = '/(.*)\'([a-zA-Z0-9_]+?)\'(\s*=>\s*)([\'"].+[\'"]),/u'; + + foreach ($enFileLines as $line) { + if (preg_match($pattern, $line, $matches)) { + $items[] = [$matches[2] => $matches[4]]; + } + } + + $langFileLines = file($langFile); + + $newLangFile = ''; + + $itemNo = 0; + + foreach ($langFileLines as $line) { + // Remove en value comment. + if (preg_match('!(.*,)(\s*//.*)$!u', $line, $matches)) { + $line = $matches[1] . "\n"; + } + + if (preg_match($pattern, $line, $matches) === 0) { + $newLangFile .= $line; + } else { + $indent = $matches[1]; + $key = $matches[2]; + $arrow = $matches[3]; + $value = $matches[4]; + + $newLangFile .= $indent . "'" . $key . "'" . $arrow . $value + . ', // ' . $items[$itemNo][array_key_first($items[$itemNo])] . "\n"; + $itemNo++; + } + } + + file_put_contents($langFile, $newLangFile); + CLI::write('Updated: ' . $langFile); +} From 4a4e40ecb9c7ad8698efcb0cf44a9a4d147d2159 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Apr 2023 18:30:14 +0900 Subject: [PATCH 133/142] docs: update en comments --- src/Language/ja/Auth.php | 156 +++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/src/Language/ja/Auth.php b/src/Language/ja/Auth.php index 8d1ec4574..5b4a27806 100644 --- a/src/Language/ja/Auth.php +++ b/src/Language/ja/Auth.php @@ -4,102 +4,102 @@ return [ // Exceptions - 'unknownAuthenticator' => '{0} は有効なオーセンティケーターではありません。', // '{0} is not a valid authenticator.', - 'unknownUserProvider' => '使用するユーザープロバイダーを決定できません。', // 'Unable to determine the User Provider to use.', - 'invalidUser' => '指定されたユーザーを見つけることができません。', // 'Unable to locate the specified user.', - 'bannedUser' => '現在あなたはアクセスが禁止されているため、ログインできません。', - 'logOutBannedUser' => 'アクセスが禁止されたため、ログアウトされました。', - 'badAttempt' => 'ログインできません。認証情報を確認してください。', // 'Unable to log you in. Please check your credentials.', - 'noPassword' => 'パスワードのないユーザーは認証できません。', // 'Cannot validate a user without a password.', - 'invalidPassword' => 'ログインできません。パスワードを確認してください。', // 'Unable to log you in. Please check your password.', - 'noToken' => 'すべてのリクエストは、{0}ヘッダーにBearerトークンが必要です。', // 'Every request must have a bearer token in the Authorization header.', - 'badToken' => 'アクセストークンが無効です。', // 'The access token is invalid.', - 'oldToken' => 'アクセストークンの有効期限が切れています。', // 'The access token has expired.', - 'noUserEntity' => 'パスワード検証のため、Userエンティティを指定する必要があります。', // 'User Entity must be provided for password validation.', - 'invalidEmail' => 'メールアドレスが一致しません。', // 'Unable to verify the email address matches the email on record.', - 'unableSendEmailToUser' => '申し訳ありませんが、メールの送信に問題がありました。 "{0}"にメールを送信できませんでした。', // 'Sorry, there was a problem sending the email. We could not send an email to "{0}".', - 'throttled' => 'このIPアドレスからのリクエストが多すぎます。 {0}秒後に再試行できます。', // Too many requests made from this IP address. You may try again in {0} seconds. - 'notEnoughPrivilege' => '目的の操作を実行するために必要な権限がありません。', // You do not have the necessary permission to perform the desired operation. + 'unknownAuthenticator' => '{0} は有効なオーセンティケーターではありません。', // '{0} is not a valid authenticator.' + 'unknownUserProvider' => '使用するユーザープロバイダーを決定できません。', // 'Unable to determine the User Provider to use.' + 'invalidUser' => '指定されたユーザーを見つけることができません。', // 'Unable to locate the specified user.' + 'bannedUser' => '現在あなたはアクセスが禁止されているため、ログインできません。', // 'Can not log you in as you are currently banned.' + 'logOutBannedUser' => 'アクセスが禁止されたため、ログアウトされました。', // 'You have been logged out because you have been banned.' + 'badAttempt' => 'ログインできません。認証情報を確認してください。', // 'Unable to log you in. Please check your credentials.' + 'noPassword' => 'パスワードのないユーザーは認証できません。', // 'Cannot validate a user without a password.' + 'invalidPassword' => 'ログインできません。パスワードを確認してください。', // 'Unable to log you in. Please check your password.' + 'noToken' => 'すべてのリクエストは、{0}ヘッダーにBearerトークンが必要です。', // 'Every request must have a bearer token in the {0} header.' + 'badToken' => 'アクセストークンが無効です。', // 'The access token is invalid.' + 'oldToken' => 'アクセストークンの有効期限が切れています。', // 'The access token has expired.' + 'noUserEntity' => 'パスワード検証のため、Userエンティティを指定する必要があります。', // 'User Entity must be provided for password validation.' + 'invalidEmail' => 'メールアドレスが一致しません。', // 'Unable to verify the email address matches the email on record.' + 'unableSendEmailToUser' => '申し訳ありませんが、メールの送信に問題がありました。 "{0}"にメールを送信できませんでした。', // 'Sorry, there was a problem sending the email. We could not send an email to "{0}".' + 'throttled' => 'このIPアドレスからのリクエストが多すぎます。 {0}秒後に再試行できます。', // 'Too many requests made from this IP address. You may try again in {0} seconds.' + 'notEnoughPrivilege' => '目的の操作を実行するために必要な権限がありません。', // 'You do not have the necessary permission to perform the desired operation.' // JWT Exceptions - 'invalidJWT' => '(To be translated) The token is invalid.', - 'expiredJWT' => '(To be translated) The token has expired.', - 'beforeValidJWT' => '(To be translated) The token is not yet available.', + 'invalidJWT' => '(To be translated) The token is invalid.', // 'The token is invalid.' + 'expiredJWT' => '(To be translated) The token has expired.', // 'The token has expired.' + 'beforeValidJWT' => '(To be translated) The token is not yet available.', // 'The token is not yet available.' - 'email' => 'メールアドレス', // 'Email Address', - 'username' => 'ユーザー名', // 'Username', - 'password' => 'パスワード', // 'Password', - 'passwordConfirm' => 'パスワード(再)', // 'Password (again)', - 'haveAccount' => 'すでにアカウントをお持ちの方', // 'Already have an account?', + 'email' => 'メールアドレス', // 'Email Address' + 'username' => 'ユーザー名', // 'Username' + 'password' => 'パスワード', // 'Password' + 'passwordConfirm' => 'パスワード(再)', // 'Password (again)' + 'haveAccount' => 'すでにアカウントをお持ちの方', // 'Already have an account?' // Buttons - 'confirm' => '確認する', // 'Confirm', - 'send' => '送信する', // 'Send', + 'confirm' => '確認する', // 'Confirm' + 'send' => '送信する', // 'Send' // Registration - 'register' => '登録', // 'Register', - 'registerDisabled' => '現在、登録はできません。', // 'Registration is not currently allowed.', - 'registerSuccess' => 'ようこそ!', // 'Welcome aboard!', + 'register' => '登録', // 'Register' + 'registerDisabled' => '現在、登録はできません。', // 'Registration is not currently allowed.' + 'registerSuccess' => 'ようこそ!', // 'Welcome aboard!' // Login - 'login' => 'ログイン', // 'Login', - 'needAccount' => 'アカウントが必要な方', // 'Need an account?', - 'rememberMe' => 'ログイン状態を保持する', // 'Remember me?', - 'forgotPassword' => 'パスワードをお忘れの方', // 'Forgot your password?', - 'useMagicLink' => 'ログインリンクを使用する', // 'Use a Login Link', - 'magicLinkSubject' => 'あなたのログインリンク', // 'Your Login Link', - 'magicTokenNotFound' => 'リンクを確認できません。', // 'Unable to verify the link.', - 'magicLinkExpired' => '申し訳ございません、リンクは切れています。', // 'Sorry, link has expired.', - 'checkYourEmail' => 'メールをチェックしてください!', // 'Check your email!', - 'magicLinkDetails' => 'ログインリンクが含まれたメールを送信しました。これは {0} 分間だけ有効です。', // 'We just sent you an email with a Login link inside. It is only valid for {0} minutes.', - 'successLogout' => '正常にログアウトしました。', // 'You have successfully logged out.', + 'login' => 'ログイン', // 'Login' + 'needAccount' => 'アカウントが必要な方', // 'Need an account?' + 'rememberMe' => 'ログイン状態を保持する', // 'Remember me?' + 'forgotPassword' => 'パスワードをお忘れの方', // 'Forgot your password?' + 'useMagicLink' => 'ログインリンクを使用する', // 'Use a Login Link' + 'magicLinkSubject' => 'あなたのログインリンク', // 'Your Login Link' + 'magicTokenNotFound' => 'リンクを確認できません。', // 'Unable to verify the link.' + 'magicLinkExpired' => '申し訳ございません、リンクは切れています。', // 'Sorry, link has expired.' + 'checkYourEmail' => 'メールをチェックしてください!', // 'Check your email!' + 'magicLinkDetails' => 'ログインリンクが含まれたメールを送信しました。これは {0} 分間だけ有効です。', // 'We just sent you an email with a Login link inside. It is only valid for {0} minutes.' + 'successLogout' => '正常にログアウトしました。', // 'You have successfully logged out.' // Passwords - 'errorPasswordLength' => 'パスワードは最低でも {0, number} 文字でなければなりません。', // 'Passwords must be at least {0, number} characters long.', - 'suggestPasswordLength' => 'パスフレーズ(最大255文字)は、覚えやすく、より安全なパスワードになります。', // 'Pass phrases - up to 255 characters long - make more secure passwords that are easy to remember.', - 'errorPasswordCommon' => 'パスワードは一般的なものであってはなりません。', // 'Password must not be a common password.', - 'suggestPasswordCommon' => 'パスワードは、65,000を超える一般的に使用されるパスワード、またはハッキングによって漏洩したパスワードに対してチェックされました。', // 'The password was checked against over 65k commonly used passwords or passwords that have been leaked through hacks.', - 'errorPasswordPersonal' => 'パスワードは、個人情報を再ハッシュ化したものを含むことはできません。', // 'Passwords cannot contain re-hashed personal information.', - 'suggestPasswordPersonal' => 'メールアドレスやユーザー名のバリエーションは、パスワードに使用しないでください。', // 'Variations on your email address or username should not be used for passwords.', - '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' => '{param} バイトを超えるパスワードは設定できません。', // '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.', + 'errorPasswordLength' => 'パスワードは最低でも {0, number} 文字でなければなりません。', // 'Passwords must be at least {0, number} characters long.' + 'suggestPasswordLength' => 'パスフレーズ(最大255文字)は、覚えやすく、より安全なパスワードになります。', // 'Pass phrases - up to 255 characters long - make more secure passwords that are easy to remember.' + 'errorPasswordCommon' => 'パスワードは一般的なものであってはなりません。', // 'Password must not be a common password.' + 'suggestPasswordCommon' => 'パスワードは、65,000を超える一般的に使用されるパスワード、またはハッキングによって漏洩したパスワードに対してチェックされました。', // 'The password was checked against over 65k commonly used passwords or passwords that have been leaked through hacks.' + 'errorPasswordPersonal' => 'パスワードは、個人情報を再ハッシュ化したものを含むことはできません。', // 'Passwords cannot contain re-hashed personal information.' + 'suggestPasswordPersonal' => 'メールアドレスやユーザー名のバリエーションは、パスワードに使用しないでください。', // 'Variations on your email address or username should not be used for passwords.' + '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' => '{param} バイトを超えるパスワードは設定できません。', // '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 - 'emailInfo' => '本人に関する情報:', - 'emailIpAddress' => 'IPアドレス:', - 'emailDevice' => 'デバイス:', - 'emailDate' => '日時:', + 'emailInfo' => '本人に関する情報:', // 'Some information about the person:' + 'emailIpAddress' => 'IPアドレス:', // 'IP Address:' + 'emailDevice' => 'デバイス:', // 'Device:' + 'emailDate' => '日時:', // 'Date:' // 2FA - 'email2FATitle' => '二要素認証', // 'Two Factor Authentication', - 'confirmEmailAddress' => 'メールアドレスを確認してください。', // 'Confirm your email address.', - 'emailEnterCode' => 'メールを確認してください', // 'Confirm your Email', - 'emailConfirmCode' => '先ほどあなたのメールアドレスにお送りした 6桁のコードを入力してください。', // 'Enter the 6-digit code we just sent to your email address.', - 'email2FASubject' => '認証コード', // 'Your authentication code', - 'email2FAMailBody' => 'あなたの認証コード:', // 'Your authentication code is:', - 'invalid2FAToken' => 'コードが間違っています。', // 'The code was incorrect.', - 'need2FA' => '二要素認証を完了させる必要があります。', // 'You must complete a two-factor verification.', - 'needVerification' => 'アカウントの有効化を完了するために、メールを確認してください。', // 'Check your email to complete account activation.', + 'email2FATitle' => '二要素認証', // 'Two Factor Authentication' + 'confirmEmailAddress' => 'メールアドレスを確認してください。', // 'Confirm your email address.' + 'emailEnterCode' => 'メールを確認してください', // 'Confirm your Email' + 'emailConfirmCode' => '先ほどあなたのメールアドレスにお送りした 6桁のコードを入力してください。', // 'Enter the 6-digit code we just sent to your email address.' + 'email2FASubject' => '認証コード', // 'Your authentication code' + 'email2FAMailBody' => 'あなたの認証コード:', // 'Your authentication code is:' + 'invalid2FAToken' => 'コードが間違っています。', // 'The code was incorrect.' + 'need2FA' => '二要素認証を完了させる必要があります。', // 'You must complete a two-factor verification.' + 'needVerification' => 'アカウントの有効化を完了するために、メールを確認してください。', // 'Check your email to complete account activation.' // Activate - 'emailActivateTitle' => 'メールアクティベーション', // 'Email Activation', - 'emailActivateBody' => 'メールアドレスを確認するために、コードを送信しました。以下にコピーペーストしてください。', // 'We just sent an email to you with a code to confirm your email address. Copy that code and paste it below.', - 'emailActivateSubject' => 'アクティベーションコード', // 'Your activation code', - 'emailActivateMailBody' => '以下のコードを使用してアカウントを有効化し、サイトの利用を開始してください。', // 'Please use the code below to activate your account and start using the site.', - 'invalidActivateToken' => 'コードが間違っています。', // 'The code was incorrect.', - 'needActivate' => 'メールアドレスに送信されたコードを確認し、登録を完了する必要があります。', // 'You must complete your registration by confirming the code sent to your email address.', - 'activationBlocked' => 'ログインする前にアカウントを有効化する必要があります。', + 'emailActivateTitle' => 'メールアクティベーション', // 'Email Activation' + 'emailActivateBody' => 'メールアドレスを確認するために、コードを送信しました。以下にコピーペーストしてください。', // 'We just sent an email to you with a code to confirm your email address. Copy that code and paste it below.' + 'emailActivateSubject' => 'アクティベーションコード', // 'Your activation code' + 'emailActivateMailBody' => '以下のコードを使用してアカウントを有効化し、サイトの利用を開始してください。', // 'Please use the code below to activate your account and start using the site.' + 'invalidActivateToken' => 'コードが間違っています。', // 'The code was incorrect.' + 'needActivate' => 'メールアドレスに送信されたコードを確認し、登録を完了する必要があります。', // 'You must complete your registration by confirming the code sent to your email address.' + 'activationBlocked' => 'ログインする前にアカウントを有効化する必要があります。', // 'You must activate your account before logging in.' // Groups - 'unknownGroup' => '{0} は有効なグループではありません。', // '{0} is not a valid group.', - 'missingTitle' => 'グループにはタイトルが必要です。', // 'Groups must have a title.', + 'unknownGroup' => '{0} は有効なグループではありません。', // '{0} is not a valid group.' + 'missingTitle' => 'グループにはタイトルが必要です。', // 'Groups must have a title.' // Permissions - 'unknownPermission' => '{0} は有効なパーミッションではありません。', // '{0} is not a valid permission.', + 'unknownPermission' => '{0} は有効なパーミッションではありません。', // '{0} is not a valid permission.' ]; From 046c411722c5d3294d4b260d078cd21176cb531b Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 22 Apr 2023 18:31:55 +0900 Subject: [PATCH 134/142] lang: translate JWT Exceptions --- src/Language/ja/Auth.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Language/ja/Auth.php b/src/Language/ja/Auth.php index 5b4a27806..1ec2f346b 100644 --- a/src/Language/ja/Auth.php +++ b/src/Language/ja/Auth.php @@ -21,9 +21,9 @@ 'throttled' => 'このIPアドレスからのリクエストが多すぎます。 {0}秒後に再試行できます。', // 'Too many requests made from this IP address. You may try again in {0} seconds.' 'notEnoughPrivilege' => '目的の操作を実行するために必要な権限がありません。', // 'You do not have the necessary permission to perform the desired operation.' // JWT Exceptions - 'invalidJWT' => '(To be translated) The token is invalid.', // 'The token is invalid.' - 'expiredJWT' => '(To be translated) The token has expired.', // 'The token has expired.' - 'beforeValidJWT' => '(To be translated) The token is not yet available.', // 'The token is not yet available.' + 'invalidJWT' => 'トークンが無効です。', // 'The token is invalid.' + 'expiredJWT' => 'トークンの有効期限が切れています。', // 'The token has expired.' + 'beforeValidJWT' => 'このトークンはまだ使えません。', // 'The token is not yet available.' 'email' => 'メールアドレス', // 'Email Address' 'username' => 'ユーザー名', // 'Username' From b77784f9a8313de771a4ead38cb36823642bac7e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Wed, 26 Apr 2023 05:52:53 +0330 Subject: [PATCH 135/142] lang: add ukrainian translation --- src/Language/uk/Auth.php | 104 ++++++++++++++++++ .../Language/AbstractTranslationTestCase.php | 4 +- tests/Language/UkrainianTranslationTest.php | 21 ++++ 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/Language/uk/Auth.php create mode 100644 tests/Language/UkrainianTranslationTest.php diff --git a/src/Language/uk/Auth.php b/src/Language/uk/Auth.php new file mode 100644 index 000000000..f375335f2 --- /dev/null +++ b/src/Language/uk/Auth.php @@ -0,0 +1,104 @@ + '{0} не є дійсним автентифікатором.', + 'unknownUserProvider' => 'Неможливо визначити постачальника користувача для використання.', + 'invalidUser' => 'Неможливо знайти вказаного користувача.', + 'bannedUser' => 'Неможливо увійти, оскільки ви зараз забанені.', + 'logOutBannedUser' => 'Ви вийшли з системи, оскільки вас забанили.', + 'badAttempt' => 'Неможливо увійти. Перевірте свої облікові дані.', + 'noPassword' => 'Неможливо перевірити користувача без пароля.', + 'invalidPassword' => 'Неможливо увійти. Перевірте свій пароль.', + 'noToken' => 'Кожен запит повинен мати токен носія в заголовку {0}.', + 'badToken' => 'Токен доступу недійсний.', + 'oldToken' => 'Термін дії токена доступу минув.', + 'noUserEntity' => 'Потрібно вказати сутність користувача для підтвердження пароля.', + 'invalidEmail' => 'Неможливо перевірити, що адреса електронної пошти відповідає зареєстрованій.', + 'unableSendEmailToUser' => 'Вибачте, під час надсилання електронного листа виникла проблема. Не вдалося надіслати електронний лист на "{0}".', + 'throttled' => 'Забагато запитів зроблено з цієї IP-адреси. Ви можете спробувати ще раз через {0} секунд.', + 'notEnoughPrivilege' => 'У вас немає необхідного дозволу для виконання потрібної операції.', + // JWT Exceptions + 'invalidJWT' => '(To be translated) The token is invalid.', + 'expiredJWT' => '(To be translated) The token has expired.', + 'beforeValidJWT' => '(To be translated) The token is not yet available.', + + 'email' => 'Адреса електронної пошти', + 'username' => 'Ім’я користувача', + 'password' => 'Пароль', + 'passwordConfirm' => 'Пароль (ще раз)', + 'haveAccount' => 'Вже є обліковий запис?', + + // Buttons + 'confirm' => 'Підтвердити', + 'send' => 'Надіслати', + + // Registration + 'register' => 'Зареєструватися', + 'registerDisabled' => 'Реєстрація зараз не дозволена.', + 'registerSuccess' => 'Ласкаво просимо на борт!', + + // Login + 'login' => 'Вхід', + 'needAccount' => 'Потрібен обліковий запис?', + 'rememberMe' => 'Пам’ятай мене?', + 'forgotPassword' => 'Забули пароль?', + 'useMagicLink' => 'Використовуйте посилання для входу', + 'magicLinkSubject' => 'Ваше посилання для входу', + 'magicTokenNotFound' => 'Неможливо перевірити посилання.', + 'magicLinkExpired' => 'Вибачте, термін дії посилання закінчився.', + 'checkYourEmail' => 'Перевірте свою електронну пошту!', + 'magicLinkDetails' => 'Ми щойно надіслали вам електронний лист із посиланням для входу. Він дійсний лише протягом {0} хвилин.', + 'successLogout' => 'Ви успішно вийшли.', + + // Passwords + 'errorPasswordLength' => 'Паролі повинні містити принаймні {0, числових} символів.', + 'suggestPasswordLength' => 'Паролі до 255 символів створюють надійніші паролі, які легко запам’ятати.', + 'errorPasswordCommon' => 'Пароль не має бути звичайним.', + 'suggestPasswordCommon' => 'Пароль перевірено на більш ніж 65 тисяч часто використовуваних паролів або паролів, які були розкриті через хакерські атаки.', + 'errorPasswordPersonal' => 'Паролі не можуть містити повторно хешовану особисту інформацію.', + 'suggestPasswordPersonal' => 'Варіанти вашої адреси електронної пошти або імені користувача не повинні використовувати для паролів.', + 'errorPasswordTooSimilar' => 'Пароль занадто схожий на ім’я користувача.', + 'suggestPasswordTooSimilar' => 'Не використовуйте частини свого імені користувача в паролі.', + 'errorPasswordPwned' => 'Пароль {0} було розкрито внаслідок витоку даних і було виявлено {1} разів у {2} зламаних паролів.', + 'suggestPasswordPwned' => '{0} ніколи не слід використовувати як пароль. Якщо ви використовуєте його десь, негайно змініть його.', + 'errorPasswordEmpty' => 'Необхідно ввести пароль.', + 'passwordChangeSuccess' => 'Пароль успішно змінено', + 'userDoesNotExist' => 'Пароль не змінено. Користувач не існує', + 'resetTokenExpired' => 'Вибачте. Термін дії вашого токена скидання минув.', + + // Email Globals + 'emailInfo' => 'Деяка відомості про особу:', + 'emailIpAddress' => 'IP-адреса:', + 'emailDevice' => 'Пристрій:', + 'emailDate' => 'Дата:', + + // 2FA + 'email2FATitle' => 'Двофакторна автентифікація', + 'confirmEmailAddress' => 'Підтвердьте адресу електронної пошти.', + 'emailEnterCode' => 'Підтвердьте свій Email', + 'emailConfirmCode' => 'Введіть 6-значний код, який ми щойно надіслали на вашу адресу електронної пошти.', + 'email2FASubject' => 'Ваш код автентифікації', + 'email2FAMailBody' => 'Ваш код автентифікації:', + 'invalid2FAToken' => 'Код невірний.', + 'need2FA' => 'Ви повинні пройти двофакторну перевірку.', + 'needVerification' => 'Перевірте свою електронну пошту, щоб завершити активацію облікового запису.', + + // Activate + 'emailActivateTitle' => 'Активація електронної пошти', + 'emailActivateBody' => 'Ми щойно надіслали вам електронний лист із кодом для підтвердження вашої електронної адреси. Скопіюйте цей код і вставте його нижче.', + 'emailActivateSubject' => 'Ваш код активації', + 'emailActivateMailBody' => 'Будь ласка, використовуйте наведений нижче код, щоб активувати свій обліковий запис і почати користуватися сайтом.', + 'invalidActivateToken' => 'Код був невірний.', + 'needActivate' => 'Ви повинні завершити реєстрацію, підтвердивши код, надісланий на вашу електронну адресу.', + 'activationBlocked' => 'Ви повинні активувати свій обліковий запис перед входом.', + + // Groups + 'unknownGroup' => '{0} недійсна група.', + 'missingTitle' => 'Групи повинні мати назву.', + + // Permissions + 'unknownPermission' => '{0} не дійсний дозвіл.', +]; diff --git a/tests/Language/AbstractTranslationTestCase.php b/tests/Language/AbstractTranslationTestCase.php index 30252fc8a..05097897c 100644 --- a/tests/Language/AbstractTranslationTestCase.php +++ b/tests/Language/AbstractTranslationTestCase.php @@ -73,8 +73,8 @@ abstract class AbstractTranslationTestCase extends TestCase SerbianTranslationTest::class => 'sr', SwedishTranslationTest::class => 'sv-SE', // ThaiTranslationTest::class => 'th', - TurkishTranslationTest::class => 'tr', - // UkrainianTranslationTest::class => 'uk', + TurkishTranslationTest::class => 'tr', + UkrainianTranslationTest::class => 'uk', // VietnameseTranslationTest::class => 'vi', // SimplifiedChineseTranslationTest::class => 'zh-CN', // TraditionalChineseTranslationTest::class => 'zh-TW', diff --git a/tests/Language/UkrainianTranslationTest.php b/tests/Language/UkrainianTranslationTest.php new file mode 100644 index 000000000..79bdfad01 --- /dev/null +++ b/tests/Language/UkrainianTranslationTest.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Language; + +/** + * @internal + */ +final class UkrainianTranslationTest extends AbstractTranslationTestCase +{ +} From ccec638e48b9403524845ed4d8452399554e97e6 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Wed, 26 Apr 2023 06:01:03 +0330 Subject: [PATCH 136/142] fix: add `errorPasswordTooLongBytes` --- src/Language/uk/Auth.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Language/uk/Auth.php b/src/Language/uk/Auth.php index f375335f2..6337b493b 100644 --- a/src/Language/uk/Auth.php +++ b/src/Language/uk/Auth.php @@ -65,6 +65,7 @@ 'errorPasswordPwned' => 'Пароль {0} було розкрито внаслідок витоку даних і було виявлено {1} разів у {2} зламаних паролів.', 'suggestPasswordPwned' => '{0} ніколи не слід використовувати як пароль. Якщо ви використовуєте його десь, негайно змініть його.', 'errorPasswordEmpty' => 'Необхідно ввести пароль.', + 'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.', 'passwordChangeSuccess' => 'Пароль успішно змінено', 'userDoesNotExist' => 'Пароль не змінено. Користувач не існує', 'resetTokenExpired' => 'Вибачте. Термін дії вашого токена скидання минув.', From f237d10b5e639743599e512507505b3dda743d8b Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Wed, 26 Apr 2023 07:43:26 +0330 Subject: [PATCH 137/142] fix: translate all string --- src/Language/uk/Auth.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Language/uk/Auth.php b/src/Language/uk/Auth.php index 6337b493b..56828330c 100644 --- a/src/Language/uk/Auth.php +++ b/src/Language/uk/Auth.php @@ -21,9 +21,9 @@ 'throttled' => 'Забагато запитів зроблено з цієї IP-адреси. Ви можете спробувати ще раз через {0} секунд.', 'notEnoughPrivilege' => 'У вас немає необхідного дозволу для виконання потрібної операції.', // JWT Exceptions - 'invalidJWT' => '(To be translated) The token is invalid.', - 'expiredJWT' => '(To be translated) The token has expired.', - 'beforeValidJWT' => '(To be translated) The token is not yet available.', + 'invalidJWT' => 'Маркер недійсний.', + 'expiredJWT' => 'Термін дії маркера минув.', + 'beforeValidJWT' => 'Маркер ще не доступний.', 'email' => 'Адреса електронної пошти', 'username' => 'Ім’я користувача', @@ -65,7 +65,7 @@ 'errorPasswordPwned' => 'Пароль {0} було розкрито внаслідок витоку даних і було виявлено {1} разів у {2} зламаних паролів.', 'suggestPasswordPwned' => '{0} ніколи не слід використовувати як пароль. Якщо ви використовуєте його десь, негайно змініть його.', 'errorPasswordEmpty' => 'Необхідно ввести пароль.', - 'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.', + 'errorPasswordTooLongBytes' => 'Довжина пароля не може перевищувати {param} байт.', 'passwordChangeSuccess' => 'Пароль успішно змінено', 'userDoesNotExist' => 'Пароль не змінено. Користувач не існує', 'resetTokenExpired' => 'Вибачте. Термін дії вашого токена скидання минув.', From 6d5965d8b593896aed40e38e8fcedf6930865c41 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Apr 2023 15:58:34 +0900 Subject: [PATCH 138/142] docs: add description for user-level and group-level permissions --- docs/authorization.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/authorization.md b/docs/authorization.md index 9976a7211..16ac5a797 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -80,12 +80,20 @@ public $permissions = [ ## Assigning Permissions to Groups In order to grant any permissions to a group, they must have the permission assigned to the group, within the `AuthGroups` -config file, under the `$matrix` property. The matrix is an associative array with the group name as the key, +config file, under the `$matrix` property. + +> **Note** This defines **group-level permissons**. + +The matrix is an associative array with the group name as the key, and an array of permissions that should be applied to that group. ```php public $matrix = [ - 'admin' => ['admin.access', 'users.create', 'users.edit', 'users.delete', 'beta.access'], + 'admin' => [ + 'admin.access', + 'users.create', 'users.edit', 'users.delete', + 'beta.access' + ], ]; ``` @@ -104,8 +112,8 @@ The `Authorizable` trait on the `User` entity provides the following methods to #### can() Allows you to check if a user is permitted to do a specific action. The only argument is the permission string. Returns -boolean `true`/`false`. Will check the user's direct permissions first, and then check against all of the user's groups -permissions to determine if they are allowed. +boolean `true`/`false`. Will check the user's direct permissions (**user-level permissions**) first, and then check against all of the user's groups +permissions (**group-level permissions**) to determine if they are allowed. ```php if ($user->can('users.create')) { @@ -133,6 +141,10 @@ if (! $user->hasPermission('users.create')) { } ``` +> **Note** This method checks only **user-level permissions**, and does not check +> group-level permissions. If you want to check if the user can do something, +> use the `$user->can()` method instead. + #### Authorizing via Routes You can restrict access to a route or route group through a @@ -168,7 +180,7 @@ override the group, so it's possible that a user can perform an action that thei #### addPermission() -Adds one or more permissions to the user. If a permission doesn't exist, a `CodeIgniter\Shield\Authorization\AuthorizationException` +Adds one or more **user-level** permissions to the user. If a permission doesn't exist, a `CodeIgniter\Shield\Authorization\AuthorizationException` is thrown. ```php @@ -177,7 +189,7 @@ $user->addPermission('users.create', 'users.edit'); #### removePermission() -Removes one or more permissions from a user. If a permission doesn't exist, a `CodeIgniter\Shield\Authorization\AuthorizationException` +Removes one or more **user-level** permissions from a user. If a permission doesn't exist, a `CodeIgniter\Shield\Authorization\AuthorizationException` is thrown. ```php @@ -186,7 +198,7 @@ $user->removePermission('users.delete'); #### syncPermissions() -Updates the user's permissions to only include the permissions in the given list. Any existing permissions on that user +Updates the user's **user-level** permissions to only include the permissions in the given list. Any existing permissions on that user not in this list will be removed. ```php @@ -195,12 +207,14 @@ $user->syncPermissions('admin.access', 'beta.access'); #### getPermissions() -Returns all permissions this user has assigned directly to them. +Returns all **user-level** permissions this user has assigned directly to them. ```php $user->getPermissions(); ``` +> **Note** This method does not return **group-level permissions**. + ## Managing User Groups #### addGroup() From af2e9600412dbb66c217de8c5c8d5d07ea4ac005 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Apr 2023 16:18:19 +0900 Subject: [PATCH 139/142] Prep for 1.0.0-beta.6 release --- src/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth.php b/src/Auth.php index 4ec710a5a..41163479a 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -28,7 +28,7 @@ class Auth /** * The current version of CodeIgniter Shield */ - public const SHIELD_VERSION = '1.0.0-beta.5'; + public const SHIELD_VERSION = '1.0.0-beta.6'; protected Authentication $authenticate; From a3788d4effa37fa142a81a7890f9aa788d27ed1b Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Apr 2023 16:32:03 +0900 Subject: [PATCH 140/142] docs: break long comments --- src/Config/AuthGroups.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Config/AuthGroups.php b/src/Config/AuthGroups.php index 1fef9e312..d740e614d 100644 --- a/src/Config/AuthGroups.php +++ b/src/Config/AuthGroups.php @@ -20,10 +20,11 @@ class AuthGroups extends BaseConfig * -------------------------------------------------------------------- * Groups * -------------------------------------------------------------------- - * An associative array of the available groups in the system, where the keys are - * the group names and the values are arrays of the group info. + * An associative array of the available groups in the system, where the keys + * are the group names and the values are arrays of the group info. * - * Whatever value you assign as the key will be used to refer to the group when using functions such as: + * Whatever value you assign as the key will be used to refer to the group + * when using functions such as: * $user->addGroup('superadmin'); * * @var array> From 9071cf1627a059e5eab202750afb37413b627f0e Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Apr 2023 16:32:39 +0900 Subject: [PATCH 141/142] docs: remove incomplete sentence --- src/Config/AuthGroups.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Config/AuthGroups.php b/src/Config/AuthGroups.php index d740e614d..bb5d07bf1 100644 --- a/src/Config/AuthGroups.php +++ b/src/Config/AuthGroups.php @@ -58,8 +58,7 @@ class AuthGroups extends BaseConfig * -------------------------------------------------------------------- * Permissions * -------------------------------------------------------------------- - * The available permissions in the system. Each system is defined - * where the key is the + * The available permissions in the system. * * If a permission is not listed here it cannot be used. */ From 387f3b7a423f32fd8419db8aff7df9cbde5dbfd8 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 26 Apr 2023 16:33:58 +0900 Subject: [PATCH 142/142] docs: add comment --- src/Config/AuthGroups.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Config/AuthGroups.php b/src/Config/AuthGroups.php index bb5d07bf1..3b92aee82 100644 --- a/src/Config/AuthGroups.php +++ b/src/Config/AuthGroups.php @@ -77,6 +77,8 @@ class AuthGroups extends BaseConfig * Permissions Matrix * -------------------------------------------------------------------- * Maps permissions to groups. + * + * This defines group-level permissions. */ public array $matrix = [ 'superadmin' => [