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

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