Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new jwt authenticator for Symfony 5.3+ Security system #872

Merged
merged 2 commits into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ build
vendor
composer.lock
.phpunit.result.cache
.idea
60 changes: 60 additions & 0 deletions DependencyInjection/Security/Factory/JWTAuthenticatorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory;

use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* Wires the "jwt" authenticator from user configuration.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTAuthenticatorFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{
/**
* {@inheritdoc}
*/
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
return [];
}

/**
* {@inheritdoc}
*/
public function getPosition()
{
return 'pre_auth';
}

/**
* {@inheritdoc}
*/
public function getKey()
{
return 'jwt';
}

/**
* {@inheritdoc}
*/
public function addConfiguration(NodeDefinition $node)
{
// no-op - no config here for now
}

public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId)
{
$authenticatorId = 'security.authenticator.jwt.'.$firewallName;
$container
->setDefinition($authenticatorId, new ChildDefinition('lexik_jwt_authentication.security.jwt_authenticator'))
->replaceArgument(3, new Reference($userProviderId));

return $authenticatorId;
}
}
7 changes: 7 additions & 0 deletions DependencyInjection/Security/Factory/JWTFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
*/
class JWTFactory implements SecurityFactoryInterface
{
public function __construct($triggerDeprecation = true)
{
if ($triggerDeprecation) {
trigger_deprecation('lexik/jwt-authentication-bundle', '2.0', 'Class "%s" is deprecated, use "%s" instead.', self::class, JWTAuthenticatorFactory::class);
}
}

/**
* {@inheritdoc}
*/
Expand Down
7 changes: 6 additions & 1 deletion LexikJWTAuthenticationBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
namespace Lexik\Bundle\JWTAuthenticationBundle;

use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\WireGenerateTokenCommandPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTAuthenticatorFactory;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTFactory;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTUserFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -30,7 +32,10 @@ public function build(ContainerBuilder $container)
$extension = $container->getExtension('security');

$extension->addUserProviderFactory(new JWTUserFactory());
$extension->addSecurityListenerFactory(new JWTFactory()); // BC 1.x, to be removed in 3.0
$extension->addSecurityListenerFactory(new JWTFactory(false)); // BC 1.x, to be removed in 3.0
if (interface_exists(AuthenticatorFactoryInterface::class)) {
$extension->addSecurityListenerFactory(new JWTAuthenticatorFactory());
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions Resources/config/token_authenticator.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,13 @@
<service class="Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage" />
</argument>
</service>

<service id="lexik_jwt_authentication.security.jwt_authenticator" class="Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\JWTAuthenticator" abstract="true">
<argument type="service" id="lexik_jwt_authentication.jwt_manager"/>
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="lexik_jwt_authentication.extractor.chain_extractor"/>
<argument /> <!-- User Provider -->
</service>

</services>
</container>
222 changes: 222 additions & 0 deletions Security/Authenticator/JWTAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<?php

declare(strict_types=1);

namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator;

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidPayloadException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\Token\JWTPostAuthenticationToken;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\PayloadAwareUserProviderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\ChainUserProvider;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
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\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class JWTAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
/**
* @var TokenExtractorInterface
*/
private $tokenExtractor;

/**
* @var JWTTokenManagerInterface
*/
private $jwtManager;

/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;

/**
* @var UserProviderInterface
*/
private $userProvider;

public function __construct(
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $eventDispatcher,
TokenExtractorInterface $tokenExtractor,
UserProviderInterface $userProvider
) {
$this->tokenExtractor = $tokenExtractor;
$this->jwtManager = $jwtManager;
$this->eventDispatcher = $eventDispatcher;
$this->userProvider = $userProvider;
}

/**
* {@inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null): JWTAuthenticationFailureResponse
{
$exception = new MissingTokenException('JWT Token not found', 0, $authException);
$event = new JWTNotFoundEvent($exception, new JWTAuthenticationFailureResponse($exception->getMessageKey()));

$this->eventDispatcher->dispatch($event, Events::JWT_NOT_FOUND);

return $event->getResponse();
}

public function supports(Request $request): ?bool
{
return false !== $this->getTokenExtractor()->extract($request);
}

public function authenticate(Request $request): PassportInterface
{
$token = $this->getTokenExtractor()->extract($request);

try {
if (!$payload = $this->jwtManager->decodeFromJsonWebToken($token)) {
throw new InvalidTokenException('Invalid JWT Token');
}
} catch (JWTDecodeFailureException $e) {
if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) {
throw new ExpiredTokenException();
}

throw new InvalidTokenException('Invalid JWT Token', 0, $e);
}

$idClaim = $this->jwtManager->getUserIdClaim();
if (!isset($payload[$idClaim])) {
throw new InvalidPayloadException($idClaim);
}

$passport = new SelfValidatingPassport(
new UserBadge($payload[$idClaim],
function ($userIdentifier) use($payload) {
return $this->loadUser($payload, $userIdentifier);
})
);

$passport->setAttribute('payload', $payload);

return $passport;
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
$response = new JWTAuthenticationFailureResponse($errorMessage);

if ($exception instanceof ExpiredTokenException) {
$event = new JWTExpiredEvent($exception, $response);
$eventName = Events::JWT_EXPIRED;
} else {
$event = new JWTInvalidEvent($exception, $response);
$eventName = Events::JWT_INVALID;
}

$this->eventDispatcher->dispatch($event, $eventName);

return $event->getResponse();
}

/**
* Gets the token extractor to be used for retrieving a JWT token in the
* current request.
*
* Override this method for adding/removing extractors to the chain one or
* returning a different {@link TokenExtractorInterface} implementation.
*/
protected function getTokenExtractor(): TokenExtractorInterface
{
return $this->tokenExtractor;
}

/**
* Loads the user to authenticate.
*
* @param array $payload The token payload
* @param string $identity The key from which to retrieve the user "identifier"
*/
protected function loadUser(array $payload, string $identity): UserInterface
{
if ($this->userProvider instanceof PayloadAwareUserProviderInterface) {
if (method_exists($this->userProvider, 'loadUserByIdentifierAndPayload')) {
return $this->userProvider->loadUserByIdentifierAndPayload($identity, $payload);
} else {
return $this->userProvider->loadUserByUsernameAndPayload($identity, $payload);
}
}

if ($this->userProvider instanceof ChainUserProvider) {
foreach ($this->userProvider->getProviders() as $provider) {
try {
if ($provider instanceof PayloadAwareUserProviderInterface) {
if (method_exists(PayloadAwareUserProviderInterface::class, 'loadUserByIdentifierAndPayload')) {
return $this->userProvider->loadUserByIdentifierAndPayload($identity, $payload);
} else {
return $this->userProvider->loadUserByUsernameAndPayload($identity, $payload);
}
}

return $provider->loadUserByIdentifier($identity);
// More generic call to catch both UsernameNotFoundException for SF<5.3 and new UserNotFoundException
} catch (AuthenticationException $e) {
// try next one
}
}

if(!class_exists(UsernameNotFoundException::class)) {
$ex = new UsernameNotFoundException(sprintf('There is no user with username "%s".', $identity));
$ex->setUsername($identity);
} else {
$ex = new UserNotFoundException(sprintf('There is no user with identifier "%s".', $identity));
$ex->setUserIdentifier($identity);
}

throw $ex;
}

if (method_exists($this->userProvider, 'loadUserByIdentifier')) {
return $this->userProvider->loadUserByIdentifier($identity);
} else {
return $this->userProvider->loadUserByUsername($identity);
}
}

public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
$token = parent::createAuthenticatedToken($passport, $firewallName);

if (!$passport instanceof SelfValidatingPassport) {
throw new \LogicException(sprintf('Expected "%s" but got "%s".', SelfValidatingPassport::class, get_debug_type($passport)));
}

$this->eventDispatcher->dispatch(new JWTAuthenticatedEvent($passport->getAttribute('payload'), $token), Events::JWT_AUTHENTICATED);

return $token;
}
}
3 changes: 2 additions & 1 deletion Security/Guard/JWTTokenAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
Expand All @@ -40,7 +41,7 @@
* @author Nicolas Cabot <n.cabot@lexik.fr>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTTokenAuthenticator extends AbstractGuardAuthenticator
class JWTTokenAuthenticator implements AuthenticatorInterface
{
/**
* @var JWTTokenManagerInterface
Expand Down
Loading