From c372358f90e6ecbd2bc959936f6119c2a798ef6a Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko Date: Mon, 2 Sep 2024 17:50:20 +0500 Subject: [PATCH] added refresh token --- app/composer.json | 1 + app/composer.lock | 82 +++++++++++++++- app/config/bundles.php | 1 + .../packages/gesdinet_jwt_refresh_token.yaml | 2 + app/config/packages/security.yaml | 3 + app/config/routes.yaml | 5 +- .../routes/gesdinet_jwt_refresh_token.yaml | 2 + app/config/services.yaml | 18 +++- app/migrations/Version20240827103922.php | 35 +++++++ app/src/Entity/RefreshToken.php | 12 +++ app/src/Listeners/AuthenticationFailure.php | 36 +++++++ app/src/Listeners/JwtRefreshListener.php | 95 +++++++++++++++++++ app/src/Service/Dto/Classes/TokenDto.php | 3 + .../Response/Classes/TokenResponse.php | 15 ++- app/symfony.lock | 14 +++ 15 files changed, 318 insertions(+), 6 deletions(-) create mode 100644 app/config/packages/gesdinet_jwt_refresh_token.yaml create mode 100644 app/config/routes/gesdinet_jwt_refresh_token.yaml create mode 100644 app/migrations/Version20240827103922.php create mode 100644 app/src/Entity/RefreshToken.php create mode 100644 app/src/Listeners/AuthenticationFailure.php create mode 100644 app/src/Listeners/JwtRefreshListener.php diff --git a/app/composer.json b/app/composer.json index 6b4bc79..f48b567 100644 --- a/app/composer.json +++ b/app/composer.json @@ -13,6 +13,7 @@ "doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.2", + "gesdinet/jwt-refresh-token-bundle": "^1.3", "lexik/jwt-authentication-bundle": "^3.0", "nelmio/api-doc-bundle": "^4.27", "nelmio/cors-bundle": "^2.5", diff --git a/app/composer.lock b/app/composer.lock index 9179298..83acbf4 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8b6f0b619e1fcafec2ca3e3ae288f1c0", + "content-hash": "5df50b18b564c984ccc4431f60ca2151", "packages": [ { "name": "api-platform/core", @@ -1484,6 +1484,86 @@ ], "time": "2023-10-06T06:47:41+00:00" }, + { + "name": "gesdinet/jwt-refresh-token-bundle", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/markitosgv/JWTRefreshTokenBundle.git", + "reference": "83d687cc461b4bdae9ffc6efda97464093cae739" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/markitosgv/JWTRefreshTokenBundle/zipball/83d687cc461b4bdae9ffc6efda97464093cae739", + "reference": "83d687cc461b4bdae9ffc6efda97464093cae739", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^1.3.3|^2.0|^3.0", + "lexik/jwt-authentication-bundle": "^2.0|^3.0", + "php": ">=7.4", + "symfony/config": "^4.4|^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.1|^3.0", + "symfony/event-dispatcher": "^4.4|^5.4|^6.0|^7.0", + "symfony/http-foundation": "^4.4|^5.4|^6.0|^7.0", + "symfony/http-kernel": "^4.4|^5.4|^6.0|^7.0", + "symfony/polyfill-php80": "^1.15", + "symfony/property-access": "^4.4|^5.4|^6.0|^7.0", + "symfony/security-bundle": "^4.4|^5.4|^6.0|^7.0", + "symfony/security-core": "^4.4|^5.4|^6.0|^7.0", + "symfony/security-http": "^4.4|^5.4|^6.0|^7.0" + }, + "conflict": { + "doctrine/mongodb-odm": "<2.2", + "doctrine/orm": "<2.7" + }, + "require-dev": { + "doctrine/annotations": "^1.13|^2.0", + "doctrine/cache": "^1.11|^2.0", + "doctrine/mongodb-odm": "^2.2", + "doctrine/orm": "^2.7", + "matthiasnoback/symfony-config-test": "^4.2|^5.0", + "matthiasnoback/symfony-dependency-injection-test": "^4.2|^5.0", + "phpunit/phpunit": "^9.5", + "symfony/cache": "^4.4|^5.4|^6.0|^7.0", + "symfony/security-guard": "^4.4|^5.4" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Gesdinet\\JWTRefreshTokenBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcos Gómez Vilches", + "email": "marcos@gesdinet.com" + } + ], + "description": "Implements a refresh token system over Json Web Tokens in Symfony", + "keywords": [ + "jwt refresh token bundle symfony json web" + ], + "support": { + "issues": "https://github.com/markitosgv/JWTRefreshTokenBundle/issues", + "source": "https://github.com/markitosgv/JWTRefreshTokenBundle/tree/v1.3.0" + }, + "time": "2024-01-10T19:40:34+00:00" + }, { "name": "lcobucci/clock", "version": "3.2.0", diff --git a/app/config/bundles.php b/app/config/bundles.php index e5e468c..38b44ae 100644 --- a/app/config/bundles.php +++ b/app/config/bundles.php @@ -16,4 +16,5 @@ return [ DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], Liip\TestFixturesBundle\LiipTestFixturesBundle::class => ['dev' => true, 'test' => true], + Gesdinet\JWTRefreshTokenBundle\GesdinetJWTRefreshTokenBundle::class => ['all' => true], ]; diff --git a/app/config/packages/gesdinet_jwt_refresh_token.yaml b/app/config/packages/gesdinet_jwt_refresh_token.yaml new file mode 100644 index 0000000..cd4cb5c --- /dev/null +++ b/app/config/packages/gesdinet_jwt_refresh_token.yaml @@ -0,0 +1,2 @@ +gesdinet_jwt_refresh_token: + refresh_token_class: App\Entity\RefreshToken diff --git a/app/config/packages/security.yaml b/app/config/packages/security.yaml index b456202..1df7f12 100644 --- a/app/config/packages/security.yaml +++ b/app/config/packages/security.yaml @@ -24,6 +24,9 @@ security: pattern: ^/api stateless: true jwt: ~ + entry_point: jwt + refresh_jwt: + check_path: api_refresh dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ diff --git a/app/config/routes.yaml b/app/config/routes.yaml index dbc41f6..c0ff903 100644 --- a/app/config/routes.yaml +++ b/app/config/routes.yaml @@ -5,4 +5,7 @@ controllers: type: attribute api_login: - path: /api/login \ No newline at end of file + path: /api/login + +api_refresh: + path: /api/refresh \ No newline at end of file diff --git a/app/config/routes/gesdinet_jwt_refresh_token.yaml b/app/config/routes/gesdinet_jwt_refresh_token.yaml new file mode 100644 index 0000000..3f58614 --- /dev/null +++ b/app/config/routes/gesdinet_jwt_refresh_token.yaml @@ -0,0 +1,2 @@ +gesdinet_jwt_refresh_token: + path: /api/token/refresh diff --git a/app/config/services.yaml b/app/config/services.yaml index 39dc3d2..957845c 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -72,4 +72,20 @@ services: acme_api.event.jwt_expired_listener: class: App\Listeners\JwtListener tags: - - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_expired, method: onJWTExpired } \ No newline at end of file + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_expired, method: onJWTExpired } + + gesdinet.jwtrefreshtoken.send_token: + class: App\Listeners\JwtRefreshListener + arguments: + - '@gesdinet.jwtrefreshtoken.refresh_token_manager' + - '%gesdinet_jwt_refresh_token.ttl%' + - '@request_stack' + - '%gesdinet_jwt_refresh_token.token_parameter_name%' + - '%gesdinet_jwt_refresh_token.single_use%' + - '@gesdinet.jwtrefreshtoken.refresh_token_generator' + - '@gesdinet.jwtrefreshtoken.request.extractor.chain' + - '%gesdinet_jwt_refresh_token.cookie%' + - '%gesdinet_jwt_refresh_token.return_expiration%' + - '%gesdinet_jwt_refresh_token.return_expiration_parameter_name%' + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_success, method: attachRefreshToken } \ No newline at end of file diff --git a/app/migrations/Version20240827103922.php b/app/migrations/Version20240827103922.php new file mode 100644 index 0000000..0f9704c --- /dev/null +++ b/app/migrations/Version20240827103922.php @@ -0,0 +1,35 @@ +addSql('CREATE SEQUENCE refresh_tokens_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE refresh_tokens (id INT NOT NULL, refresh_token VARCHAR(128) NOT NULL, username VARCHAR(255) NOT NULL, valid TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9BACE7E1C74F2195 ON refresh_tokens (refresh_token)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE refresh_tokens_id_seq CASCADE'); + $this->addSql('DROP TABLE refresh_tokens'); + } +} diff --git a/app/src/Entity/RefreshToken.php b/app/src/Entity/RefreshToken.php new file mode 100644 index 0000000..d607a63 --- /dev/null +++ b/app/src/Entity/RefreshToken.php @@ -0,0 +1,12 @@ + 'failureRefresh' + ]; + } + + /** + * Ошибка refresh токена + * + * @param RefreshAuthenticationFailureEvent $event + * + * @return void + * @throws \JsonException + */ + public function failureRefresh(RefreshAuthenticationFailureEvent $event): void + { + $responseDto = new Response(); + $responseDto->addError('Refresh токен не валиден'); + + $response = $event->getResponse(); + + $response->setContent($responseDto->getResponse()->getContent()); + } +} \ No newline at end of file diff --git a/app/src/Listeners/JwtRefreshListener.php b/app/src/Listeners/JwtRefreshListener.php new file mode 100644 index 0000000..aded87d --- /dev/null +++ b/app/src/Listeners/JwtRefreshListener.php @@ -0,0 +1,95 @@ +getUser(); + + if (!$user instanceof UserInterface) { + return; + } + + $response = new TokenResponse(); + + $data = $event->getData(); + $response->setToken($data['data']['token']); + $request = $this->requestStack->getCurrentRequest(); + + if (null === $request) { + return; + } + + $refreshTokenString = $this->extractor->getRefreshToken($request, $this->tokenParameterName); + + if ($refreshTokenString && true === $this->singleUse) { + $refreshToken = $this->refreshTokenManager->get($refreshTokenString); + $refreshTokenString = null; + + if ($refreshToken instanceof RefreshTokenInterface) { + $this->refreshTokenManager->delete($refreshToken); + } + } + + if ($refreshTokenString) { + $response->setRefreshToken($refreshTokenString); + } else { + $refreshToken = $this->refreshTokenGenerator->createForUserWithTtl($user, $this->ttl); + + $this->refreshTokenManager->save($refreshToken); + $refreshTokenString = $refreshToken->getRefreshToken(); + $response->setRefreshToken($refreshTokenString); + } + + // Set response data + $data = json_decode($response->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR); + $event->getResponse()->setContent($response->getResponse()->getContent()); + $event->setData($data); + } +} \ No newline at end of file diff --git a/app/src/Service/Dto/Classes/TokenDto.php b/app/src/Service/Dto/Classes/TokenDto.php index 59d98cc..9484296 100644 --- a/app/src/Service/Dto/Classes/TokenDto.php +++ b/app/src/Service/Dto/Classes/TokenDto.php @@ -9,4 +9,7 @@ class TokenDto extends BaseDto { #[Groups(['data'])] public string $token; + + #[Groups(['data'])] + public string $refreshToken; } \ No newline at end of file diff --git a/app/src/Service/Response/Classes/TokenResponse.php b/app/src/Service/Response/Classes/TokenResponse.php index 937e1d6..47e06d6 100644 --- a/app/src/Service/Response/Classes/TokenResponse.php +++ b/app/src/Service/Response/Classes/TokenResponse.php @@ -17,8 +17,17 @@ class TokenResponse extends Response public function setToken(string $token): void { - $dto = new TokenDto(); - $dto->token = $token; - $this->data = $dto; + if (!isset($this->data)) { + $this->data = new TokenDto(); + } + $this->data->token = $token; + } + + public function setRefreshToken(string $token): void + { + if (!isset($this->data)) { + $this->data = new TokenDto(); + } + $this->data->refreshToken = $token; } } \ No newline at end of file diff --git a/app/symfony.lock b/app/symfony.lock index a76ca5f..7e27dad 100644 --- a/app/symfony.lock +++ b/app/symfony.lock @@ -64,6 +64,20 @@ "migrations/.gitignore" ] }, + "gesdinet/jwt-refresh-token-bundle": { + "version": "1.3", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "2390b4ed5c195e0b3f6dea45221f3b7c0af523a0" + }, + "files": [ + "config/packages/gesdinet_jwt_refresh_token.yaml", + "config/routes/gesdinet_jwt_refresh_token.yaml", + "src/Entity/RefreshToken.php" + ] + }, "lexik/jwt-authentication-bundle": { "version": "3.0", "recipe": { -- GitLab