Skip to content

Commit

Permalink
Add Symfony 5.x new Security Authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
TristanPouliquen authored and chalasr committed Jun 19, 2021
1 parent 0d36113 commit a4d8015
Show file tree
Hide file tree
Showing 9 changed files with 572 additions and 28 deletions.
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

This comment has been minimized.

Copy link
@garak

garak Jul 4, 2021

Contributor

why?

This comment has been minimized.

Copy link
@chalasr

chalasr Jul 4, 2021

Collaborator

nice catch, reverted in 16275d9

205 changes: 205 additions & 0 deletions Security/Authenticator/JWTTokenAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

declare(strict_types=1);

namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator;

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\Authentication\Token\PreAuthenticationJWTUserToken;
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 JWTTokenAuthenticator 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
{
$tokenExtractor = $this->getTokenExtractor();

try {
if (!$payload = $this->jwtManager->decodeFromJsonWebToken($tokenExtractor->extract($request))) {
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);
}

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

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(PayloadAwareUserProviderInterface::class, '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(UserProviderInterface::class, 'loadUserByIdentifier')) {
return $this->userProvider->loadUserByIdentifierAndPayload($identity);
} else {
return $this->userProvider->loadUserByUsernameAndPayload($identity);
}
}
}
21 changes: 13 additions & 8 deletions Security/User/JWTUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
*/
class JWTUser implements JWTUserInterface
{
private $username;
private $userIdentifier;
private $roles;

/**
* @final
*/
public function __construct(string $username, array $roles = [])
public function __construct(string $userIdentifier, array $roles = [])
{
$this->username = $username;
$this->userIdentifier = $userIdentifier;
$this->roles = $roles;
}

Expand All @@ -38,31 +38,36 @@ public static function createFromPayload($username, array $payload)
/**
* {@inheritdoc}
*/
public function getUsername()
public function getUsername(): string
{
return $this->username;
return $this->getUserIdentifier();
}

public function getUserIdentifier(): string
{
return $this->userIdentifier;
}

/**
* {@inheritdoc}
*/
public function getRoles()
public function getRoles(): array
{
return $this->roles;
}

/**
* {@inheritdoc}
*/
public function getPassword()
public function getPassword(): ?string
{
return null;
}

/**
* {@inheritdoc}
*/
public function getSalt()
public function getSalt(): ?string
{
return null;
}
Expand Down
27 changes: 23 additions & 4 deletions Security/User/JWTUserProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,26 @@ public function __construct($class)
* {@inheritdoc}
*
* @param array $payload The JWT payload from which to create an instance
*
* @return JWTUserInterface
*/
public function loadUserByUsername($username, array $payload = [])
{
return $this->loadUserByUsernameAndPayload($username, $payload);
}

/**
* {@inheritdoc}
*
* @param array $payload The JWT payload from which to create an instance
*/
public function loadUserByIdentifier(string $identifier, array $payload = []): UserInterface
{
return $this->loadUserByIdentifierAndPayload($identifier, $payload);
}

/**
* {@inheritdoc}
*/
public function loadUserByUsernameAndPayload($username, array $payload)
public function loadUserByUsernameAndPayload(string $username, array $payload): UserInterface
{
$class = $this->class;

Expand All @@ -49,6 +57,17 @@ public function loadUserByUsernameAndPayload($username, array $payload)
return $this->cache[$username] = $class::createFromPayload($username, $payload);
}

public function loadUserByIdentifierAndPayload(string $userIdentifier, array $payload): UserInterface
{
$class = $this->class;

if (isset($this->cache[$userIdentifier])) {
return $this->cache[$userIdentifier];
}

return $this->cache[$userIdentifier] = $class::createFromPayload($userIdentifier, $payload);
}

/**
* {@inheritdoc}
*/
Expand All @@ -57,7 +76,7 @@ public function supportsClass($class)
return $class === $this->class || (new \ReflectionClass($class))->implementsInterface(JWTUserInterface::class);
}

public function refreshUser(UserInterface $user)
public function refreshUser(UserInterface $user): UserInterface
{
return $user; // noop
}
Expand Down
14 changes: 10 additions & 4 deletions Security/User/PayloadAwareUserProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;

use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

Expand All @@ -11,11 +12,16 @@ interface PayloadAwareUserProviderInterface extends UserProviderInterface
/**
* Load a user by its username, including the JWT token payload.
*
* @param string $username
*
* @throws UsernameNotFoundException if the user is not found
*
* @return UserInterface
* @deprecated since 2.12, implement loadByIdentifier() instead.
*/
public function loadUserByUsernameAndPayload(string $username, array $payload)/*: UserInterface*/;

/**
* Load a user by its username, including the JWT token payload.
*
* @throws UserNotFoundException if the user is not found
*/
public function loadUserByUsernameAndPayload($username, array $payload);
public function loadUserByIdentifierAndPayload(string $userIdentifier, array $payload): UserInterface;
}
Loading

0 comments on commit a4d8015

Please sign in to comment.