src/Security/OpenIdAuthenticator.php line 111

Open in your IDE?
  1. <?php
  2. namespace App\Security;
  3. use App\Security\Client\OpenIdClient;
  4. use App\Security\Dto\TokensBag;
  5. use App\Security\Exception\InvalidStateException;
  6. use App\Security\Exception\OpenIdServerException;
  7. use LogicException;
  8. use RuntimeException;
  9. use Symfony\Component\HttpFoundation\RedirectResponse;
  10. use Symfony\Component\HttpFoundation\Request;
  11. use Symfony\Component\HttpFoundation\RequestStack;
  12. use Symfony\Component\HttpFoundation\Response;
  13. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  14. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  15. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  16. use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
  17. use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
  18. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
  19. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  20. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  21. use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
  22. use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
  23. use Symfony\Component\Uid\Uuid;
  24. use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
  25. class OpenIdAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterfaceInteractiveAuthenticatorInterface
  26. {
  27.     private const STATE_QUERY_KEY 'state';
  28.     private const STATE_SESSION_KEY 'openid_state';
  29.     public function __construct(
  30.         private UrlGeneratorInterface $urlGenerator,
  31.         private OpenIdClient $openIdClient,
  32.         private RequestStack $requestStack,
  33.         private string $authorizationEndpoint,
  34.         private string $clientId,
  35.     ) {}
  36.     public function supports(Request $request): ?bool
  37.     {
  38.         return 'openid_redirecturi' === $request->attributes->get('_route');
  39.     }
  40.     public function authenticate(Request $request): Passport
  41.     {
  42.         $sessionState $request->getSession()->get(self::STATE_SESSION_KEY);
  43.         $queryState $request->get(self::STATE_QUERY_KEY);
  44.         if ($queryState === null || $queryState !== $sessionState) {
  45.             throw new InvalidStateException(sprintf(
  46.                 'query state (%s) is not the same as session state (%s)',
  47.                 $queryState ?? 'NULL',
  48.                 $sessionState ?? 'NULL',
  49.             ));
  50.         }
  51.         $request->getSession()->remove(self::STATE_SESSION_KEY);
  52.         try {
  53.             $response $this->openIdClient->getTokenFromAuthorizationCode($request->query->get('code'''));
  54.         } catch (HttpExceptionInterface $e) {
  55.             throw new OpenIdServerException(sprintf(
  56.                 'Bad status code returned by openID server (%s)',
  57.                 $e->getResponse()->getStatusCode(),
  58.             ), previous$e);
  59.         }
  60.         $responseData json_decode($responsetrue);
  61.         if (false === $responseData) {
  62.             throw new OpenIdServerException(sprintf('Can\'t parse json in response: %s'$response->getContent()));
  63.         }
  64.         $jwtToken $responseData['id_token'] ?? null;
  65.         if (null === $jwtToken) {
  66.             throw new OpenIdServerException(sprintf('No access token found in response %s'$response->getContent()));
  67.         }
  68.         $refreshToken $responseData['refresh_token'] ?? null;
  69.         if (null === $refreshToken) {
  70.             throw new RuntimeException(sprintf('No refresh token found in response %s'$response->getContent()));
  71.         }
  72.         $userBadge = new UserBadge($jwtToken);
  73.         $passport = new SelfValidatingPassport($userBadge, [new PreAuthenticatedUserBadge()]);
  74.         $passport->setAttribute(TokensBag::class, new TokensBag($responseData['access_token'] ?? null$refreshToken));
  75.         return $passport;
  76.     }
  77.     public function onAuthenticationSuccess(Request $requestTokenInterface $tokenstring $firewallName): ?Response
  78.     {
  79.         return new RedirectResponse($this->urlGenerator->generate('profile'));
  80.     }
  81.     public function onAuthenticationFailure(Request $requestAuthenticationException $exception): ?Response
  82.     {
  83.         $request->getSession()->getFlashBag()->add(
  84.             'error',
  85.             'An authentication error occured',
  86.         );
  87.         return new RedirectResponse($this->urlGenerator->generate('home'));
  88.     }
  89.     public function start(Request $requestAuthenticationException $authException null): Response
  90.     {
  91.         $state = (string)Uuid::v4();
  92.         $request->getSession()->set(self::STATE_SESSION_KEY$state);
  93.         $qs http_build_query([
  94.             'client_id' => $this->clientId,
  95.             'response_type' => 'code',
  96.             'state' => $state,
  97.             'scope' => 'openid roles profile email',
  98.             // Force http since working on localhost
  99.             'redirect_uri' => 'http:'.$this->urlGenerator->generate('openid_redirecturi', [], UrlGeneratorInterface::NETWORK_PATH),
  100.         ]);
  101.         return new RedirectResponse(sprintf('%s?%s'$this->authorizationEndpoint$qs));
  102.     }
  103.     public function createToken(Passport $passportstring $firewallName): TokenInterface
  104.     {
  105.         $token parent::createToken($passport$firewallName);
  106.         if (!$passport instanceof Passport) {
  107.             throw new \LogicException(sprintf('Passport must be a subclass of %s, %s given'Passport::class, get_class($passport)));
  108.         }
  109.         $currentRequest $this->requestStack->getCurrentRequest();
  110.         if (null === $currentRequest) {
  111.             throw new LogicException(sprintf('%s can only be used in an http context'__CLASS__));
  112.         }
  113.         $jwtExpires $currentRequest->attributes->get('_app_jwt_expires');
  114.         if (null === $jwtExpires) {
  115.             throw new \LogicException('Missing _app_jwt_expires in the session');
  116.         }
  117.         $currentRequest->attributes->remove('_app_jwt_expires');
  118.         $tokens $passport->getAttribute(TokensBag::class);
  119.         if (null === $tokens) {
  120.             throw new \LogicException(sprintf('Can\'t find %s in passport attributes'TokensBag::class));
  121.         }
  122.         $token->setAttribute(TokensBag::class, $tokens->withExpiration($jwtExpires));
  123.         return $token;
  124.     }
  125.     public function isInteractive(): bool
  126.     {
  127.         return true;
  128.     }
  129. }