Skip to content

Commit

Permalink
CHANGE: Move to Sf 6 and PHP 8.1
Browse files Browse the repository at this point in the history
CHANGE: Add refresh-failed/refresh succeeded events in case authentication fails
CHANGE: Add domain specific exceptions
  • Loading branch information
yivi committed Dec 29, 2021
1 parent 1819207 commit 4978781
Show file tree
Hide file tree
Showing 27 changed files with 1,291 additions and 1,133 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
/var
*.cache
/vendor-bin/**/vendor
/vendor-bin/**/composer.lock binary
.idea/
1 change: 1 addition & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
],
'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true],
'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'],
'phpdoc_to_comment' => ['ignored_tags' => ['var']],
]
);

Expand Down
45 changes: 32 additions & 13 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
{
"name": "yivoff/jwt-refresh-bundle",
"description": "Token Refresh for JWT. Independent of persistence layer, splitting id/hash for verification",
"type": "symfony-bundle",
"description": "Token Refresh for JWT. Independent of persistence layer, splitting id/hash for verification",
"license": "MIT",
"require": {
"php": ">=8.1",
"lexik/jwt-authentication-bundle": "^v2.11.3",
"symfony/config": "^6.0",
"symfony/dependency-injection": "^6.0",
"symfony/framework-bundle": "^6.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4",
"nyholm/symfony-bundle-test": "dev-master",
"roave/security-advisories": "dev-master",
"symfony/console": "^6.0"
},
"suggest": {
"symfony/console": "^6.0"
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
},
"autoload": {
"psr-4": {
"Yivoff\\JwtRefreshBundle\\": "src"
Expand All @@ -13,17 +34,15 @@
"Yivoff\\JwtRefreshBundle\\Test\\": "tests"
}
},
"require": {
"php": ">=8.0",
"symfony/dependency-injection": "^5.3",
"symfony/framework-bundle": "^5.3",
"lexik/jwt-authentication-bundle": "^v2.11.3",
"symfony/config": "^5.3"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"bamarni/composer-bin-plugin": "^1.4",
"nyholm/symfony-bundle-test": "dev-master",
"symfony/console": "^5.3"
"scripts": {
"code-style": "vendor-bin/csfixer/vendor/friendsofphp/php-cs-fixer/php-cs-fixer fix",
"preflight": [
"@code-style",
"@psalm",
"@test"
],
"psalm": "vendor-bin/static/vendor/vimeo/psalm/psalm",
"test": "vendor-bin/testing/vendor/phpunit/phpunit/phpunit",
"test-pretty": "vendor-bin/testing/vendor/phpunit/phpunit/phpunit --testdox"
}
}
12 changes: 8 additions & 4 deletions psalm.xml
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
<?xml version="1.0"?>
<psalm
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor-bin/static/vendor/vimeo/psalm/config.xsd"
memoizeMethodCallResults="true"
totallyTyped="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor-bin/static/vendor/vimeo/psalm/config.xsd"
errorLevel="1"
>
<projectFiles>
<directory name="src"/>
<ignoreFiles>
<file name="src/DependencyInjection/BundleConfiguration.php"/>
<file name="vendor/bin/psalm"/>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
</psalm>
2 changes: 1 addition & 1 deletion src/Console/PurgeExpiredTokensCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class PurgeExpiredTokensCommand extends Command

public function __construct(private RefreshTokenProviderInterface $provider)
{
parent::__construct(self::$defaultName);
parent::__construct((string) self::$defaultName);
}

protected function configure(): void
Expand Down
5 changes: 4 additions & 1 deletion src/DependencyInjection/YivoffJwtRefreshExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Yivoff\JwtRefreshBundle\Console\PurgeExpiredTokensCommand;
use Yivoff\JwtRefreshBundle\Contracts\HasherInterface;
Expand All @@ -29,6 +30,7 @@ public function load(array $configs, ContainerBuilder $container): void

$config = $this->processConfiguration($configuration, $configs);

/** @var string $value */
foreach ($config as $key => $value) {
$container->setParameter(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.'.$key, $value);
}
Expand All @@ -52,7 +54,8 @@ public function load(array $configs, ContainerBuilder $container): void
->setArgument(0, new Reference(HasherInterface::class))
->setArgument(1, new Reference('lexik_jwt_authentication.handler.authentication_success'))
->setArgument(2, $providerReference)
->setArgument(3, $config['parameter_name'])
->setArgument(3, new Reference(EventDispatcherInterface::class))
->setArgument(4, $config['parameter_name'])
;

$container->setAlias(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.authenticator', Authenticator::class)
Expand Down
14 changes: 14 additions & 0 deletions src/Event/JwtRefreshTokenFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Yivoff\JwtRefreshBundle\Event;

use Yivoff\JwtRefreshBundle\Exception\FailType;

class JwtRefreshTokenFailed
{
public function __construct(public readonly FailType $failType, public readonly ?string $tokenId, public readonly ?string $userIdentifier)
{
}
}
12 changes: 12 additions & 0 deletions src/Event/JwtRefreshTokenSuccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Yivoff\JwtRefreshBundle\Event;

class JwtRefreshTokenSuccess
{
public function __construct(public readonly string $tokenId, public readonly string $userIdentifier)
{
}
}
8 changes: 1 addition & 7 deletions src/EventListener/AttachRefreshToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Yivoff\JwtRefreshBundle\Contracts\RefreshTokenProviderInterface;
use Yivoff\JwtRefreshBundle\Contracts\TokenIdGeneratorInterface;
use Yivoff\JwtRefreshBundle\Model\RefreshToken;
use function method_exists;

final class AttachRefreshToken
{
Expand All @@ -31,12 +30,7 @@ public function __invoke(AuthenticationSuccessEvent $event): void
$data = $event->getData();
$user = $event->getUser();

if (method_exists($user, 'getUserIdentifier')) {
$userId = $user->getUserIdentifier();
} else {
/** @psalm-suppress DeprecatedMethod */
$userId = $user->getUsername();
}
$userId = $user->getUserIdentifier();

$tokenId = $this->tokenIdGenerator->generateIdentifier(20);
$verifier = $this->tokenIdGenerator->generateVerifier(32);
Expand Down
16 changes: 16 additions & 0 deletions src/Exception/FailType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Yivoff\JwtRefreshBundle\Exception;

enum FailType: string
{
case PAYLOAD = 'Invalid Payload';

case INVALID = 'Invalid Token';

case EXPIRED = 'Expired Token';

case NOT_FOUND = 'Token not found';
}
18 changes: 18 additions & 0 deletions src/Exception/JwtRefreshException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Yivoff\JwtRefreshBundle\Exception;

use Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

abstract class JwtRefreshException extends AuthenticationException
{
public function __construct(public readonly ?string $tokenId, public readonly ?string $userIdentifier, public readonly FailType $failType, ?Exception $previous = null)
{
$message = $this->failType->value.(null !== $tokenId ? '. Token Id: '.$tokenId : '').(null !== $userIdentifier ? '. User Id: '.$userIdentifier : '');

parent::__construct($message, 0, $previous);
}
}
15 changes: 15 additions & 0 deletions src/Exception/PayloadInvalidException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Yivoff\JwtRefreshBundle\Exception;

use Exception;

class PayloadInvalidException extends JwtRefreshException
{
public function __construct(?string $tokenId, ?string $userIdentifier, ?Exception $previous = null)
{
parent::__construct($tokenId, $userIdentifier, FailType::PAYLOAD, $previous);
}
}
15 changes: 15 additions & 0 deletions src/Exception/TokenExpiredException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Yivoff\JwtRefreshBundle\Exception;

use Exception;

class TokenExpiredException extends JwtRefreshException
{
public function __construct(?string $tokenId, ?string $userIdentifier, ?Exception $previous = null)
{
parent::__construct($tokenId, $userIdentifier, FailType::EXPIRED, $previous);
}
}
15 changes: 15 additions & 0 deletions src/Exception/TokenInvalidException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Yivoff\JwtRefreshBundle\Exception;

use Exception;

class TokenInvalidException extends JwtRefreshException
{
public function __construct(?string $tokenId, ?string $userIdentifier, ?Exception $previous = null)
{
parent::__construct($tokenId, $userIdentifier, FailType::INVALID, $previous);
}
}
15 changes: 15 additions & 0 deletions src/Exception/TokenNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Yivoff\JwtRefreshBundle\Exception;

use Exception;

class TokenNotFoundException extends JwtRefreshException
{
public function __construct(?string $tokenId, ?string $userIdentifier, ?Exception $previous = null)
{
parent::__construct($tokenId, $userIdentifier, FailType::NOT_FOUND, $previous);
}
}
26 changes: 20 additions & 6 deletions src/Security/Authenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Yivoff\JwtRefreshBundle\Contracts\HasherInterface;
use Yivoff\JwtRefreshBundle\Contracts\RefreshTokenInterface;
use Yivoff\JwtRefreshBundle\Contracts\RefreshTokenProviderInterface;
use Yivoff\JwtRefreshBundle\Event\JwtRefreshTokenFailed;
use Yivoff\JwtRefreshBundle\Event\JwtRefreshTokenSuccess;
use Yivoff\JwtRefreshBundle\Exception\JwtRefreshException;
use Yivoff\JwtRefreshBundle\Exception\PayloadInvalidException;
use Yivoff\JwtRefreshBundle\Exception\TokenExpiredException;
use Yivoff\JwtRefreshBundle\Exception\TokenInvalidException;
use Yivoff\JwtRefreshBundle\Exception\TokenNotFoundException;
use function explode;
use function str_contains;
use function time;
Expand All @@ -25,34 +33,37 @@ public function __construct(
private HasherInterface $encoder,
private AuthenticationSuccessHandler $successHandler,
private RefreshTokenProviderInterface $tokenProvider,
private EventDispatcherInterface $eventDispatcher,
private string $parameterName
) {
}

public function authenticate(HttpFoundation\Request $request): PassportInterface
public function authenticate(HttpFoundation\Request $request): Passport
{
$credentials = (string) $request->request->get($this->parameterName);

if (!str_contains($credentials, ':')) {
throw new AuthenticationException('Invalid Token Format');
throw new PayloadInvalidException(null, null);
}

[$tokenId, $userProvidedVerification] = explode(':', $credentials);

$token = $this->tokenProvider->getTokenWithIdentifier($tokenId);

if (!$token instanceof RefreshTokenInterface) {
throw new AuthenticationException('Token Does Not Exist');
throw new TokenNotFoundException($tokenId, null);
}

if ($token->getValidUntil() <= time()) {
throw new AuthenticationException('Token expired');
throw new TokenExpiredException($tokenId, $token->getUsername());
}

if (!$this->encoder->verify($userProvidedVerification, $token->getVerifier())) {
throw new AuthenticationException('Token verification failed');
throw new TokenInvalidException($tokenId, $token->getUsername());
}

$this->eventDispatcher->dispatch(new JwtRefreshTokenSuccess($tokenId, $token->getUsername()));

return new SelfValidatingPassport(new UserBadge($token->getUsername()));
}

Expand All @@ -63,6 +74,9 @@ public function supports(HttpFoundation\Request $request): bool

public function onAuthenticationFailure(HttpFoundation\Request $request, AuthenticationException $exception): HttpFoundation\Response
{
/** @var JwtRefreshException $exception */
$this->eventDispatcher->dispatch(new JwtRefreshTokenFailed($exception->failType, $exception->tokenId, $exception->userIdentifier));

return new HttpFoundation\JsonResponse(['error' => $exception->getMessage()], HttpFoundation\Response::HTTP_UNAUTHORIZED);
}

Expand Down
4 changes: 4 additions & 0 deletions tests/DependencyInjection/BundleInitializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,19 @@ public function testServiceAndAliasesCreation(): void
// tests if your services exists
$this->assertTrue($container->has(AttachRefreshToken::class));
$this->assertTrue($container->has(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.attach_refresh_token_listener'));
$this->assertInstanceOf(AttachRefreshToken::class, $container->get(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.attach_refresh_token_listener'));

$this->assertTrue($container->has(Authenticator::class));
$this->assertTrue($container->has(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.authenticator'));
$this->assertInstanceOf(Authenticator::class, $container->get(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.authenticator'));

$this->assertTrue($container->has(HasherInterface::class));
$this->assertTrue($container->has(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.hasher'));
$this->assertInstanceOf(HasherInterface::class, $container->get(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.hasher'));

$this->assertTrue($container->has(TokenIdGeneratorInterface::class));
$this->assertTrue($container->has(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.token_id_generator'));
$this->assertInstanceOf(TokenIdGeneratorInterface::class, $container->get(YivoffJwtRefreshBundle::BUNDLE_PREFIX.'.token_id_generator'));
}

protected static function createKernel(array $options = []): TestKernel
Expand Down
Loading

0 comments on commit 4978781

Please sign in to comment.