src/Security/KeycloakAuthenticator.php line 197

Open in your IDE?
  1. <?php
  2. namespace App\Security;
  3. use App\Security\Dto\TokensBag;
  4. use App\Security\Exception\OpenIdServerException;
  5. use App\Service\KeycloakApiService;
  6. use App\Specification\KeycloakModeUsed;
  7. use App\Specification\SimmtLoginPageModeUsed;
  8. use Doctrine\ORM\EntityManagerInterface;
  9. use App\Security\Exception\InvalidStateException;
  10. use Psr\Log\LoggerInterface;
  11. use Ramsey\Uuid\Uuid;
  12. use LogicException;
  13. use RuntimeException;
  14. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  15. use Symfony\Component\HttpFoundation\RedirectResponse;
  16. use Symfony\Component\HttpFoundation\Request;
  17. use Symfony\Component\HttpFoundation\RequestStack;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  20. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  21. use Symfony\Component\Routing\RouterInterface;
  22. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  23. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  24. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  25. use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
  26. use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
  27. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  28. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  29. use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
  30. use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
  31. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
  32. class KeycloakAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterfaceInteractiveAuthenticatorInterface
  33. {
  34.     private const STATE_QUERY_KEY 'state';
  35.     private const STATE_SESSION_KEY 'openid_state';
  36.     public const LOGIN_ROUTE 'login';
  37.     public const OIDC_LOGIN_ROUTE 'oidc_redirect_uri';
  38.     private EntityManagerInterface $em;
  39.     private RouterInterface $router;
  40.     private UrlGeneratorInterface $urlGenerator;
  41.     private KeycloakApiService $keycloakApiService;
  42.     private ParameterBagInterface $param;
  43.     private RequestStack $requestStack;
  44.     private LoggerInterface $logger;
  45.     private string $clientId;
  46.     private string $keycloakRealmName;
  47.     private string $authorizationEndpoint;
  48.     public function __construct(
  49.         RouterInterface $router,
  50.         KeycloakApiService $keycloakApiService,
  51.         UrlGeneratorInterface $urlGenerator,
  52.         ParameterBagInterface $param,
  53.         RequestStack $requestStack,
  54.         LoggerInterface $logger,
  55.     ) {
  56.         $this->router $router;
  57.         $this->keycloakApiService $keycloakApiService;
  58.         $this->urlGenerator $urlGenerator;
  59.         $this->param $param;
  60.         $this->requestStack $requestStack;
  61.         $this->logger $logger;
  62.         $this->clientId $this->param->get('simmt_keycloak_client_id');
  63.         $this->keycloakRealmName $this->param->get('simmt_keycloak_realms_name');
  64.         $this->authorizationEndpoint $this->param->get('simmt_keycloak_app_url');
  65.     }
  66.     public function supports(Request $request): ?bool
  67.     {
  68.         if (SimmtLoginPageModeUsed::isSatisfiedBy($this->param)) {
  69.             return self::LOGIN_ROUTE === $request->attributes->get('_route')
  70.                 && $request->isMethod('POST');
  71.         }
  72.         return ($request->attributes->get('_route') === self::OIDC_LOGIN_ROUTE
  73.             || $request->attributes->get('_route') === self::LOGIN_ROUTE);
  74.     }
  75.     public function authenticate(Request $request): Passport
  76.     {
  77.         $sessionState $request->getSession()->get(self::STATE_SESSION_KEY);
  78.         $queryState $request->get(self::STATE_QUERY_KEY);
  79.         if ($queryState === null || $queryState !== $sessionState) {
  80.             throw new InvalidStateException(sprintf(
  81.                 'query state (%s) is not the same as session state (%s)',
  82.                 $queryState ?? 'NULL',
  83.                 $sessionState ?? 'NULL',
  84.             ));
  85.         }
  86.         $request->getSession()->remove(self::STATE_SESSION_KEY);
  87.         try {
  88.             $response $this->keycloakApiService->getTokenFromAuthorizationCode($request->query->get('code'''));
  89.         } catch (HttpExceptionInterface $e) {
  90.             throw new OpenIdServerException(sprintf(
  91.                 'Bad status code returned by openID server (%s)',
  92.                 $e->getStatusCode(),
  93.             ), previous$e);
  94.         }
  95.         $responseData json_decode($responsetrue);
  96.         if (false === $responseData) {
  97.             throw new OpenIdServerException(sprintf('Can\'t parse json in response: %s'$response));
  98.         }
  99.         $jwtToken $responseData['id_token'] ?? null;
  100.         if (null === $jwtToken) {
  101.             throw new OpenIdServerException(sprintf('No access token found in response %s'$response));
  102.         }
  103.         $refreshToken $responseData['refresh_token'] ?? null;
  104.         if (null === $refreshToken) {
  105.             throw new RuntimeException(sprintf('No refresh token found in response %s'$response));
  106.         }
  107.         $decoded $this->keycloakApiService->getPublicKey($responseData['access_token']);
  108.         if (array_key_exists('email', (array)$decoded)) {
  109.             $email $decoded->email;
  110.             $session $request->getSession();
  111.             $session->remove('oidc_access_token');
  112.             $session->remove('oidc_refresh_token');
  113.             $session->remove('oidc_expires_token');
  114.             $session->remove('oidc_expires_refresh_token');
  115.             $session->set('oidc_access_token'$responseData['access_token']);
  116.             $session->set('oidc_refresh_token'$responseData['refresh_token']);
  117.             $session->set('oidc_expires_token'$responseData['expires_in']);
  118.             $session->set('oidc_expires_refresh_token'$responseData['refresh_expires_in']);
  119.             $this->logger->info(">>>>>>>>>>>>>>>>>> 1er ACCESS TOKEN LORS DE LA CONNEXION >>>>>>>>>>>>>>>>>>");
  120.             $this->logger->debug($responseData['access_token']);
  121.             $this->logger->info("<<<<<<<<<<<<<<<<<< 1er ACCESS TOKEN LORS DE LA CONNEXION <<<<<<<<<<<<<<<<<<");
  122.             $this->logger->info(">>>>>>>>>>>>>>>>>> 1er REFRESH TOKEN LORS DE LA CONNEXION >>>>>>>>>>>>>>>>>>");
  123.             $this->logger->debug($responseData['refresh_token']);
  124.             $this->logger->info("<<<<<<<<<<<<<<<<<< 1er REFRESH TOKEN LORS DE LA CONNEXION <<<<<<<<<<<<<<<<<<");
  125.             $this->requestStack->getCurrentRequest()->attributes->set('_app_jwt_expires'$responseData['expires_in']);
  126.             $userBadge = new UserBadge($email);
  127.             $passport = new SelfValidatingPassport($userBadge, [new PreAuthenticatedUserBadge()]);
  128.             $passport->setAttribute(TokensBag::class, new TokensBag($session->get('oidc_access_token')
  129.                 ?? null$session->get('oidc_refresh_token')));
  130.             return $passport;
  131.         } else {
  132.             throw new AccessDeniedException('Unauthorized access');
  133.         }
  134.     }
  135.     public function onAuthenticationSuccess(Request $requestTokenInterface $tokenstring $providerKey): RedirectResponse
  136.     {
  137.         $this->logger->info("User connexion success");
  138.         return new RedirectResponse($this->urlGenerator->generate('home'));
  139.     }
  140.     public function onAuthenticationFailure(Request $requestAuthenticationException $exception): Response
  141.     {
  142.         $this->logger->info("User connexion failed");
  143.         $request->getSession()->getFlashBag()->add(
  144.             'error',
  145.             'An authentication error occured',
  146.         );
  147.         $vistoryRedirect $this->param->get('vistory_redirect');
  148.         if (KeycloakModeUsed::isSatisfiedBy($this->param) &&
  149.             (($request->attributes->get('_route') != 'login' ||
  150.                 $request->attributes->get('_route') == 'oidc_redirect_uri') && $vistoryRedirect != 'true' )) {
  151.             return new RedirectResponse($this->urlGenerator->generate('user_not_found'));
  152.         }
  153.         return new RedirectResponse($this->urlGenerator->generate('logout'));
  154.     }
  155.     public function start(Request $requestAuthenticationException $authException null): Response
  156.     {
  157.         if (SimmtLoginPageModeUsed::isSatisfiedBy($this->param)) {
  158.             return new RedirectResponse($this->router->generate('login'));
  159.         }
  160.         $scheme $request->getScheme();
  161.         $uri $scheme ':' $this->urlGenerator->generate('oidc_redirect_uri', [], UrlGeneratorInterface::NETWORK_PATH);
  162.         $state =  Uuid::uuid4()->toString();
  163.         $request->getSession()->set(self::STATE_SESSION_KEY$state);
  164.         $qs http_build_query([
  165.             'client_id' => $this->clientId,
  166.             'response_type' => 'code',
  167.             'state' => $state,
  168.             'scope' => "openid roles profile email",
  169.             'redirect_uri' => $uri,
  170.         ]);
  171.         return new RedirectResponse(sprintf("%s/realms/%s/protocol/openid-connect/auth?%s",
  172.             $this->authorizationEndpoint$this->keycloakRealmName$qs));
  173.     }
  174.     public function createToken(Passport $passportstring $firewallName): TokenInterface
  175.     {
  176.         $token parent::createToken($passport$firewallName);
  177.         $currentRequest $this->requestStack->getCurrentRequest();
  178.         if (null === $currentRequest) {
  179.             throw new LogicException(sprintf('%s can only be used in an http context'__CLASS__));
  180.         }
  181.         $jwtExpires $currentRequest->attributes->get('_app_jwt_expires');
  182.         if (null === $jwtExpires) {
  183.             throw new \LogicException('Missing _app_jwt_expires in the session');
  184.         }
  185.         $currentRequest->attributes->remove('_app_jwt_expires');
  186.         $tokens $passport->getAttribute(TokensBag::class);
  187.         if (null === $tokens) {
  188.             throw new \LogicException(sprintf('Can\'t find %s in passport attributes'TokensBag::class));
  189.         }
  190.         $token->setAttribute(TokensBag::class, $tokens->withExpiration($jwtExpires));
  191.         return $token;
  192.     }
  193.     public function isInteractive(): bool
  194.     {
  195.         return true;
  196.     }
  197. }