vendor/hwi/oauth-bundle/src/Security/Http/Authenticator/OAuthAuthenticator.php line 42

  1. <?php
  2. /*
  3.  * This file is part of the HWIOAuthBundle package.
  4.  *
  5.  * (c) Hardware Info <opensource@hardware.info>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace HWI\Bundle\OAuthBundle\Security\Http\Authenticator;
  11. use HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface;
  12. use HWI\Bundle\OAuthBundle\OAuth\State\State;
  13. use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
  14. use HWI\Bundle\OAuthBundle\Security\Core\Exception\OAuthAwareExceptionInterface;
  15. use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface;
  16. use HWI\Bundle\OAuthBundle\Security\Http\Authenticator\Passport\SelfValidatedOAuthPassport;
  17. use HWI\Bundle\OAuthBundle\Security\Http\ResourceOwnerMapInterface;
  18. use Symfony\Component\HttpFoundation\RedirectResponse;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\HttpFoundation\Response;
  21. use Symfony\Component\HttpKernel\HttpKernelInterface;
  22. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  23. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  24. use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
  25. use Symfony\Component\Security\Core\Exception\LazyResponseException;
  26. use Symfony\Component\Security\Core\User\UserInterface;
  27. use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
  28. use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
  29. use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
  30. use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
  31. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
  32. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  33. use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
  34. use Symfony\Component\Security\Http\HttpUtils;
  35. /**
  36.  * @author Vadim Borodavko <vadim.borodavko@gmail.com>
  37.  */
  38. final class OAuthAuthenticator implements AuthenticatorInterfaceAuthenticationEntryPointInterfaceInteractiveAuthenticatorInterface
  39. {
  40.     private HttpUtils $httpUtils;
  41.     private OAuthAwareUserProviderInterface $userProvider;
  42.     private ResourceOwnerMapInterface $resourceOwnerMap;
  43.     private AuthenticationSuccessHandlerInterface $successHandler;
  44.     private AuthenticationFailureHandlerInterface $failureHandler;
  45.     private HttpKernelInterface $httpKernel;
  46.     /**
  47.      * @var string[]
  48.      */
  49.     private array $checkPaths;
  50.     private array $options;
  51.     public function __construct(
  52.         HttpUtils $httpUtils,
  53.         OAuthAwareUserProviderInterface $userProvider,
  54.         ResourceOwnerMapInterface $resourceOwnerMap,
  55.         array $checkPaths,
  56.         AuthenticationSuccessHandlerInterface $successHandler,
  57.         AuthenticationFailureHandlerInterface $failureHandler,
  58.         HttpKernelInterface $kernel,
  59.         array $options
  60.     ) {
  61.         $this->failureHandler $failureHandler;
  62.         $this->successHandler $successHandler;
  63.         $this->checkPaths $checkPaths;
  64.         $this->resourceOwnerMap $resourceOwnerMap;
  65.         $this->userProvider $userProvider;
  66.         $this->httpUtils $httpUtils;
  67.         $this->httpKernel $kernel;
  68.         $this->options $options;
  69.     }
  70.     public function supports(Request $request): bool
  71.     {
  72.         foreach ($this->checkPaths as $checkPath) {
  73.             if ($this->httpUtils->checkRequestPath($request$checkPath)) {
  74.                 return true;
  75.             }
  76.         }
  77.         return false;
  78.     }
  79.     public function start(Request $requestAuthenticationException $authException null): Response
  80.     {
  81.         if ($this->options['use_forward'] ?? false) {
  82.             $subRequest $this->httpUtils->createRequest($request$this->options['login_path']);
  83.             $iterator $request->query->getIterator();
  84.             $subRequest->query->add(iterator_to_array($iterator));
  85.             $response $this->httpKernel->handle($subRequestHttpKernelInterface::SUB_REQUEST);
  86.             if (200 === $response->getStatusCode()) {
  87.                 $response->headers->set('X-Status-Code''401');
  88.             }
  89.             return $response;
  90.         }
  91.         return new RedirectResponse($this->httpUtils->generateUri($request$this->options['login_path']));
  92.     }
  93.     /**
  94.      * @throws AuthenticationException
  95.      * @throws LazyResponseException
  96.      */
  97.     public function authenticate(Request $request): Passport
  98.     {
  99.         [$resourceOwner$checkPath] = $this->resourceOwnerMap->getResourceOwnerByRequest($request);
  100.         if (!$resourceOwner instanceof ResourceOwnerInterface) {
  101.             throw new AuthenticationException('No resource owner match the request.');
  102.         }
  103.         if (!$resourceOwner->handles($request)) {
  104.             throw new AuthenticationException('No oauth code in the request.');
  105.         }
  106.         // If resource owner supports only one url authentication, call redirect
  107.         if ($request->query->has('authenticated') && $resourceOwner->getOption('auth_with_one_url')) {
  108.             $request->attributes->set('service'$resourceOwner->getName());
  109.             throw new LazyResponseException(new RedirectResponse(sprintf('%s?code=%s&authenticated=true'$this->httpUtils->generateUri($request'hwi_oauth_connect_service'), $request->query->get('code'))));
  110.         }
  111.         $resourceOwner->isCsrfTokenValid(
  112.             $this->extractCsrfTokenFromState($request->get('state'))
  113.         );
  114.         $accessToken $resourceOwner->getAccessToken(
  115.             $request,
  116.             $this->httpUtils->createRequest($request$checkPath)->getUri()
  117.         );
  118.         $token = new OAuthToken($accessToken);
  119.         $token->setResourceOwnerName($resourceOwner->getName());
  120.         return new SelfValidatedOAuthPassport($this->refreshToken($token), [new RememberMeBadge()]);
  121.     }
  122.     /**
  123.      * This function can be used for refreshing an expired token
  124.      * or for custom "password grant" authenticator, if site owner also owns oauth instance.
  125.      *
  126.      * @template T of OAuthToken
  127.      *
  128.      * @param T $token
  129.      *
  130.      * @return T
  131.      */
  132.     public function refreshToken(OAuthToken $token): OAuthToken
  133.     {
  134.         $resourceOwner $this->resourceOwnerMap->getResourceOwnerByName($token->getResourceOwnerName());
  135.         if (!$resourceOwner) {
  136.             throw new AuthenticationServiceException('Unknown resource owner set on token: '.$token->getResourceOwnerName());
  137.         }
  138.         if ($token->isExpired()) {
  139.             $expiredToken $token;
  140.             if ($refreshToken $expiredToken->getRefreshToken()) {
  141.                 $tokenClass \get_class($expiredToken);
  142.                 $token = new $tokenClass($resourceOwner->refreshAccessToken($refreshToken));
  143.                 $token->setResourceOwnerName($expiredToken->getResourceOwnerName());
  144.                 if (!$token->getRefreshToken()) {
  145.                     $token->setRefreshToken($expiredToken->getRefreshToken());
  146.                 }
  147.                 $token->copyPersistentDataFrom($expiredToken);
  148.             } else {
  149.                 // if you cannot refresh token, you do not need to make user_info request to oauth-resource
  150.                 if (null !== $expiredToken->getUser()) {
  151.                     return $expiredToken;
  152.                 }
  153.             }
  154.             unset($expiredToken);
  155.         }
  156.         $userResponse $resourceOwner->getUserInformation($token->getRawToken());
  157.         try {
  158.             $user $this->userProvider->loadUserByOAuthUserResponse($userResponse);
  159.         } catch (OAuthAwareExceptionInterface $e) {
  160.             $e->setToken($token);
  161.             $e->setResourceOwnerName($token->getResourceOwnerName());
  162.             throw $e;
  163.         }
  164.         if (!$user instanceof UserInterface) {
  165.             throw new AuthenticationServiceException('loadUserByOAuthUserResponse() must return a UserInterface.');
  166.         }
  167.         return $this->recreateToken($token$user);
  168.     }
  169.     /**
  170.      * @template T of OAuthToken
  171.      *
  172.      * @param T              $token
  173.      * @param ?UserInterface $user
  174.      *
  175.      * @return T
  176.      */
  177.     public function recreateToken(OAuthToken $tokenUserInterface $user null): OAuthToken
  178.     {
  179.         $user $user instanceof UserInterface $user $token->getUser();
  180.         $tokenClass \get_class($token);
  181.         if ($user) {
  182.             $newToken = new $tokenClass(
  183.                 $token->getRawToken(),
  184.                 method_exists($user'getRoles') ? $user->getRoles() : []
  185.             );
  186.             $newToken->setUser($user);
  187.         } else {
  188.             $newToken = new $tokenClass($token->getRawToken());
  189.         }
  190.         $newToken->setResourceOwnerName($token->getResourceOwnerName());
  191.         $newToken->setRefreshToken($token->getRefreshToken());
  192.         $newToken->setCreatedAt($token->getCreatedAt());
  193.         $newToken->setTokenSecret($token->getTokenSecret());
  194.         $newToken->setAttributes($token->getAttributes());
  195.         // required for compatibility with Symfony 5.4
  196.         if (method_exists($newToken'setAuthenticated')) {
  197.             $newToken->setAuthenticated((bool) $userfalse);
  198.         }
  199.         $newToken->copyPersistentDataFrom($token);
  200.         return $newToken;
  201.     }
  202.     public function createToken(Passport $passportstring $firewallName): TokenInterface
  203.     {
  204.         return $this->createAuthenticatedToken($passport$firewallName);
  205.     }
  206.     /**
  207.      * @param Passport|SelfValidatedOAuthPassport $passport
  208.      */
  209.     public function createAuthenticatedToken($passportstring $firewallName): TokenInterface
  210.     {
  211.         if ($passport instanceof SelfValidatedOAuthPassport) {
  212.             return $passport->getToken();
  213.         }
  214.         throw new \LogicException(sprintf('The first argument of "%s" must be instance of "%s", "%s" provided.'__METHOD__SelfValidatedOAuthPassport::class, \get_class($passport)));
  215.     }
  216.     public function onAuthenticationSuccess(Request $requestTokenInterface $tokenstring $firewallName): ?Response
  217.     {
  218.         return $this->successHandler->onAuthenticationSuccess($request$token);
  219.     }
  220.     public function onAuthenticationFailure(Request $requestAuthenticationException $exception): Response
  221.     {
  222.         return $this->failureHandler->onAuthenticationFailure($request$exception);
  223.     }
  224.     public function isInteractive(): bool
  225.     {
  226.         return true;
  227.     }
  228.     private function extractCsrfTokenFromState(?string $stateParameter): ?string
  229.     {
  230.         $state = new State($stateParameter);
  231.         return $state->getCsrfToken() ?: $stateParameter;
  232.     }
  233. }