From 7716ee08d91117966fd545d3626e75bc5ea745e5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 31 May 2022 17:25:34 +0900 Subject: [PATCH 001/101] 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 002/101] 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 003/101] 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 004/101] 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 005/101] 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 006/101] 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 007/101] 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 008/101] 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 009/101] 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 010/101] 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 011/101] 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 012/101] 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 013/101] 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 014/101] 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 015/101] 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 016/101] 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 017/101] 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 018/101] 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 019/101] 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 020/101] 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 021/101] 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 022/101] 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 023/101] 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 024/101] 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 025/101] 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 026/101] 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 027/101] 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 028/101] 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 029/101] 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 030/101] 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 031/101] 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 032/101] 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 033/101] 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 034/101] 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 035/101] 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 036/101] 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 037/101] 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 038/101] 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 039/101] 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 040/101] 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 041/101] 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 042/101] 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 043/101] 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 044/101] 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 045/101] 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 046/101] 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 047/101] 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 048/101] 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 049/101] 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 050/101] 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 051/101] 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 052/101] 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 053/101] 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 054/101] 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 055/101] 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 056/101] 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 057/101] 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 058/101] 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 059/101] 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 060/101] 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 061/101] 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 062/101] 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 063/101] 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 064/101] 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 065/101] 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 066/101] 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 067/101] 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 068/101] 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 069/101] 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 070/101] 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 071/101] 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 072/101] 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 073/101] 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 074/101] 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 075/101] 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 076/101] 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 077/101] 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 078/101] 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 079/101] 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 080/101] 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 081/101] 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 082/101] 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 083/101] 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 084/101] 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 085/101] 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 086/101] 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 087/101] 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 088/101] 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 089/101] 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 090/101] 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 091/101] 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 092/101] 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 093/101] 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 094/101] 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 095/101] 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 096/101] 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 097/101] 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 098/101] 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 099/101] 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 100/101] 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 101/101] 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)