* @author Robin Chalas */ class JWTTokenAuthenticator implements AuthenticatorInterface { /** * @var JWTTokenManagerInterface */ private $jwtManager; /** * @var EventDispatcherInterface */ private $dispatcher; /** * @var TokenExtractorInterface */ private $tokenExtractor; /** * @var TokenStorageInterface */ private $preAuthenticationTokenStorage; /** * @var TranslatorInterface */ private $translator; public function __construct( JWTTokenManagerInterface $jwtManager, EventDispatcherInterface $dispatcher, TokenExtractorInterface $tokenExtractor, TokenStorageInterface $preAuthenticationTokenStorage, TranslatorInterface $translator = null ) { $this->jwtManager = $jwtManager; $this->dispatcher = $dispatcher; $this->tokenExtractor = $tokenExtractor; $this->preAuthenticationTokenStorage = $preAuthenticationTokenStorage; $this->translator = $translator; } public function supports(Request $request) { return false !== $this->getTokenExtractor()->extract($request); } /** * Returns a decoded JWT token extracted from a request. * * {@inheritdoc} * * @return PreAuthenticationJWTUserTokenInterface * * @throws InvalidTokenException If an error occur while decoding the token * @throws ExpiredTokenException If the request token is expired */ public function getCredentials(Request $request) { $tokenExtractor = $this->getTokenExtractor(); if (!$tokenExtractor instanceof TokenExtractorInterface) { throw new \RuntimeException(sprintf('Method "%s::getTokenExtractor()" must return an instance of "%s".', self::class, TokenExtractorInterface::class)); } if (false === ($jsonWebToken = $tokenExtractor->extract($request))) { return; } $preAuthToken = new PreAuthenticationJWTUserToken($jsonWebToken); try { if (!$payload = $this->jwtManager->decode($preAuthToken)) { throw new InvalidTokenException('Invalid JWT Token'); } $preAuthToken->setPayload($payload); } catch (JWTDecodeFailureException $e) { if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) { $expiredTokenException = new ExpiredTokenException(); $expiredTokenException->setToken($preAuthToken); throw $expiredTokenException; } throw new InvalidTokenException('Invalid JWT Token', 0, $e); } return $preAuthToken; } /** * Returns an user object loaded from a JWT token. * * {@inheritdoc} * * @param PreAuthenticationJWTUserTokenInterface $preAuthToken Implementation of the (Security) TokenInterface * * @throws \InvalidArgumentException If preAuthToken is not of the good type * @throws InvalidPayloadException If the user identity field is not a key of the payload * @throws UserNotFoundException If no user can be loaded from the given token */ public function getUser($preAuthToken, UserProviderInterface $userProvider) { if (!$preAuthToken instanceof PreAuthenticationJWTUserTokenInterface) { throw new \InvalidArgumentException(sprintf('The first argument of the "%s()" method must be an instance of "%s".', __METHOD__, PreAuthenticationJWTUserTokenInterface::class)); } $payload = $preAuthToken->getPayload(); $idClaim = $this->jwtManager->getUserIdClaim(); if (!isset($payload[$idClaim])) { throw new InvalidPayloadException($idClaim); } $user = $this->loadUser($userProvider, $payload, $payload[$idClaim]); $this->preAuthenticationTokenStorage->setToken($preAuthToken); return $user; } /** * {@inheritdoc} */ public function onAuthenticationFailure(Request $request, AuthenticationException $authException) { $errorMessage = strtr($authException->getMessageKey(), $authException->getMessageData()); if (null !== $this->translator) { $errorMessage = $this->translator->trans($authException->getMessageKey(), $authException->getMessageData(), 'security'); } $response = new JWTAuthenticationFailureResponse($errorMessage); if ($authException instanceof ExpiredTokenException) { $event = new JWTExpiredEvent($authException, $response, $request); $eventName = Events::JWT_EXPIRED; } else { $event = new JWTInvalidEvent($authException, $response, $request); $eventName = Events::JWT_INVALID; } $this->dispatcher->dispatch($event, $eventName); return $event->getResponse(); } /** * {@inheritdoc} */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) { return; } /** * {@inheritdoc} * * @return JWTAuthenticationFailureResponse */ public function start(Request $request, AuthenticationException $authException = null) { $exception = new MissingTokenException('JWT Token not found', 0, $authException); $event = new JWTNotFoundEvent($exception, new JWTAuthenticationFailureResponse($exception->getMessageKey()), $request); $this->dispatcher->dispatch($event, Events::JWT_NOT_FOUND); return $event->getResponse(); } /** * {@inheritdoc} */ public function checkCredentials($credentials, UserInterface $user) { return true; } /** * {@inheritdoc} * * @throws \RuntimeException If there is no pre-authenticated token previously stored */ public function createAuthenticatedToken(UserInterface $user, $providerKey) { $preAuthToken = $this->preAuthenticationTokenStorage->getToken(); if (null === $preAuthToken) { throw new \RuntimeException('Unable to return an authenticated token since there is no pre authentication token.'); } $authToken = new JWTUserToken($user->getRoles(), $user, $preAuthToken->getCredentials(), $providerKey); $this->dispatcher->dispatch(new JWTAuthenticatedEvent($preAuthToken->getPayload(), $authToken), Events::JWT_AUTHENTICATED); $this->preAuthenticationTokenStorage->setToken(null); return $authToken; } /** * {@inheritdoc} */ public function supportsRememberMe() { return false; } /** * 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. * * @return TokenExtractorInterface */ protected function getTokenExtractor() { return $this->tokenExtractor; } /** * @return JWTTokenManagerInterface */ protected function getJwtManager() { return $this->jwtManager; } /** * @return EventDispatcherInterface */ protected function getDispatcher() { return $this->dispatcher; } /** * @return TokenStorageInterface */ protected function getPreAuthenticationTokenStorage() { return $this->preAuthenticationTokenStorage; } /** * Loads the user to authenticate. * * @param UserProviderInterface $userProvider An user provider * @param array $payload The token payload * @param string $identity The key from which to retrieve the user "username" * * @return UserInterface */ protected function loadUser(UserProviderInterface $userProvider, array $payload, $identity) { $providers = $userProvider instanceof ChainUserProvider ? $userProvider->getProviders() : [$userProvider]; foreach ($providers as $provider) { try { if ($provider instanceof PayloadAwareUserProviderInterface) { return $provider->loadUserByUsernameAndPayload($identity, $payload); } if (method_exists($provider, 'loadUserByIdentifier')) { return $provider->loadUserByIdentifier($identity); } return $provider->loadUserByUsername($identity); } catch (SecurityUserNotFoundException | UsernameNotFoundException $e) { // try next one } } if (class_exists(SecurityUserNotFoundException::class)) { $ex = new SecurityUserNotFoundException(sprintf('There is no user with name "%s".', $identity)); $ex->setUserIdentifier($identity); } else { $ex = new UsernameNotFoundException(sprintf('There is no user with name "%s".', $identity)); $ex->setUsername($identity); } throw $ex; } }