diff --git a/app/composer.json b/app/composer.json index 6b4bc79b4cac8452cb60b948b471c785424ed214..f48b567e92d1a974f13af73498af02a621522314 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 91792987b8463abfe8e5032cb3dea934a844260d..83acbf4215984b00a79efda3dd9c6016fd08ec09 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 e5e468c5b16a67b458b0eda87cc65fb41120d164..38b44aec9f8cfe7294256c11a0e52011412235fd 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 0000000000000000000000000000000000000000..cd4cb5c5618e31fece93a3794798cc956f245f4a --- /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 b4562029548b6418a2c97423c4656053a5278f2d..1df7f12e0ce452f846da97684739894e781eb35c 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 dbc41f6e7c5ebdf66cc64ce14bfafd7c59889a27..c0ff9031ab4a667271bc4f42fd4f7f8bdb421fca 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 0000000000000000000000000000000000000000..3f58614f99fd3528647ad1edee4b5cb261fe7cc6 --- /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 39dc3d24820c87fd5339aad0f967809e5dc4456f..957845cd6b3097275bd530f0fef942e11b7ba6bd 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 0000000000000000000000000000000000000000..0f9704ca911962823e82ba44ea10db1d6aa36349 --- /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 0000000000000000000000000000000000000000..d607a63131fee41eaa87914f04d41cb8522638a6 --- /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 0000000000000000000000000000000000000000..aded87da5f6e59fb78989c96396e045d8bc759c1 --- /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 59d98cc41862b6eaa6256936eb496898443f9192..94842964aa7b72acd4ea958c06309d195ba853b7 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 937e1d62ba5b2dd097d17940e7116b7e13957bce..47e06d611f1f98d8563abe5d1111204afcc67b4e 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 a76ca5fdbea3cd3f97b3d84d2eb0a5ef20db12dd..7e27dadfea8538886095f6b0dff671b9019fe02b 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": {