diff --git a/Entity/JWT/RefreshToken.php b/Entity/JWT/RefreshToken.php index 68fd289..143d66f 100644 --- a/Entity/JWT/RefreshToken.php +++ b/Entity/JWT/RefreshToken.php @@ -15,6 +15,7 @@ use Doctrine\ORM\Mapping as ORM; use Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken; +use StfalconStudio\ApiBundle\Model\JWT\CreatedAtAwareRefreshTokenInterface; use StfalconStudio\ApiBundle\Repository\JWT\RefreshTokenRepository; use Symfony\Component\Validator\Constraints as Assert; @@ -33,7 +34,7 @@ ] )] #[ORM\Index(columns: ['valid'], name: 'idx_refresh_token_valid')] -class RefreshToken extends AbstractRefreshToken +class RefreshToken extends AbstractRefreshToken implements CreatedAtAwareRefreshTokenInterface { #[ORM\Id] #[ORM\Column(name: 'id', type: 'integer')] diff --git a/EventListener/JWT/JwtRefreshSubscriber.php b/EventListener/JWT/JwtRefreshSubscriber.php index 0188947..64da99d 100644 --- a/EventListener/JWT/JwtRefreshSubscriber.php +++ b/EventListener/JWT/JwtRefreshSubscriber.php @@ -14,9 +14,9 @@ namespace StfalconStudio\ApiBundle\EventListener\JWT; use Gesdinet\JWTRefreshTokenBundle\Event\RefreshEvent; -use StfalconStudio\ApiBundle\Entity\JWT\RefreshToken; use StfalconStudio\ApiBundle\Exception\JWT\InvalidRefreshTokenException; use StfalconStudio\ApiBundle\Model\Credentials\CredentialsInterface; +use StfalconStudio\ApiBundle\Model\JWT\CreatedAtAwareRefreshTokenInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -41,18 +41,20 @@ public static function getSubscribedEvents(): iterable public function processRefreshToken(RefreshEvent $event): void { $user = $event->getToken()->getUser(); + $refreshToken = $event->getRefreshToken(); - if ($user instanceof CredentialsInterface) { - $refreshToken = $event->getRefreshToken(); + if (!$user instanceof CredentialsInterface || !$refreshToken instanceof CreatedAtAwareRefreshTokenInterface) { + return; + } + + $userCredentialsLastChangedAt = $user->getCredentialsLastChangedAt(); - if ($refreshToken instanceof RefreshToken) { - $userCredentialsLastChangedAt = $user->getCredentialsLastChangedAt(); - $refreshTokenCreatedAt = $refreshToken->getCreatedAt()->getTimestamp(); + if (!$userCredentialsLastChangedAt instanceof \DateTimeInterface) { + return; + } - if ($userCredentialsLastChangedAt instanceof \DateTimeInterface && $refreshTokenCreatedAt < $userCredentialsLastChangedAt->getTimestamp()) { - throw new InvalidRefreshTokenException(); - } - } + if ($refreshToken->getCreatedAt()->getTimestamp() < $userCredentialsLastChangedAt->getTimestamp()) { + throw new InvalidRefreshTokenException(); } } } diff --git a/Model/JWT/CreatedAtAwareRefreshTokenInterface.php b/Model/JWT/CreatedAtAwareRefreshTokenInterface.php new file mode 100644 index 0000000..a1fd0de --- /dev/null +++ b/Model/JWT/CreatedAtAwareRefreshTokenInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\JWT; + +use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface; + +/** + * CreatedAtAwareRefreshTokenInterface. + * + * A refresh token that knows when it was issued. + * + * The creation date should not be derived from RefreshTokenInterface::getValid(), + * because the expiration date is rewritten on every use when the `ttl_update` option is enabled + */ +interface CreatedAtAwareRefreshTokenInterface extends RefreshTokenInterface +{ + /** + * @return \DateTimeInterface + */ + public function getCreatedAt(): \DateTimeInterface; +} diff --git a/Tests/EventListener/JWT/JwtRefreshSubscriberTest.php b/Tests/EventListener/JWT/JwtRefreshSubscriberTest.php index 306c5d9..d0a5bd9 100644 --- a/Tests/EventListener/JWT/JwtRefreshSubscriberTest.php +++ b/Tests/EventListener/JWT/JwtRefreshSubscriberTest.php @@ -14,6 +14,7 @@ namespace StfalconStudio\ApiBundle\Tests\EventListener\JWT; use Gesdinet\JWTRefreshTokenBundle\Event\RefreshEvent; +use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use StfalconStudio\ApiBundle\Entity\JWT\RefreshToken; @@ -133,4 +134,33 @@ public function testProcessRefreshTokenWithoutException(): void $this->subscriber->processRefreshToken($this->refreshEvent); } + + public function testProcessRefreshTokenWithRefreshTokenWithoutCreatedAt(): void + { + $refreshTokenWithoutCreatedAt = $this->createMock(RefreshTokenInterface::class); + + $this->refreshEvent + ->expects(self::once()) + ->method('getToken') + ->willReturn($this->token) + ; + $this->refreshEvent + ->expects(self::once()) + ->method('getRefreshToken') + ->willReturn($refreshTokenWithoutCreatedAt) + ; + + $this->token + ->expects(self::once()) + ->method('getUser') + ->willReturn($this->user) + ; + + $this->user + ->expects(self::never()) + ->method('getCredentialsLastChangedAt') + ; + + $this->subscriber->processRefreshToken($this->refreshEvent); + } }