src/Security/KeycloakAuthenticator.php line 133

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\Security\Exception\InvalidStateException;
  7. use App\Utils\JwtUtils;
  8. use Doctrine\ORM\EntityManagerInterface;
  9. use Psr\Log\LoggerInterface;
  10. use Ramsey\Uuid\Uuid;
  11. use LogicException;
  12. use RuntimeException;
  13. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  14. use Symfony\Component\HttpFoundation\RedirectResponse;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\RequestStack;
  17. use Symfony\Component\HttpFoundation\Response;
  18. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  19. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  20. use Symfony\Component\Routing\RouterInterface;
  21. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  22. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  23. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  24. use Symfony\Component\Security\Core\Exception\UserNotFoundException;
  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.     use JwtUtils;
  35.     private const STATE_QUERY_KEY 'state';
  36.     private const STATE_SESSION_KEY 'openid_state';
  37.     public const LOGIN_ROUTE 'login';
  38.     public const OIDC_LOGIN_ROUTE 'oidc_redirect_uri';
  39.     private EntityManagerInterface $em;
  40.     private RouterInterface $router;
  41.     private UrlGeneratorInterface $urlGenerator;
  42.     private KeycloakApiService $keycloakApiService;
  43.     private ParameterBagInterface $param;
  44.     private RequestStack $requestStack;
  45.     private LoggerInterface $logger;
  46.     private string $clientId;
  47.     private string $keycloakRealmName;
  48.     private string $authorizationEndpoint;
  49.     public function __construct(
  50.         RouterInterface $router,
  51.         KeycloakApiService $keycloakApiService,
  52.         UrlGeneratorInterface $urlGenerator,
  53.         ParameterBagInterface $param,
  54.         RequestStack $requestStack,
  55.         LoggerInterface $logger,
  56.     ) {
  57.         $this->router $router;
  58.         $this->keycloakApiService $keycloakApiService;
  59.         $this->urlGenerator $urlGenerator;
  60.         $this->param $param;
  61.         $this->requestStack $requestStack;
  62.         $this->logger $logger;
  63.         $this->clientId $this->param->get('keycloak_client_id');
  64.         $this->keycloakRealmName $this->param->get('keycloak_realms_name');
  65.         $this->authorizationEndpoint $this->param->get('keycloak_app_url');
  66.     }
  67.     public function supports(Request $request): ?bool
  68.     {
  69.         return $request->request->get('Refresh-Token') ??
  70.             ($request->attributes->get('_route') === self::OIDC_LOGIN_ROUTE
  71.                 || $request->attributes->get('_route') === self::LOGIN_ROUTE);
  72.     }
  73.     public function authenticate(Request $request): Passport
  74.     {
  75.         // Vérifiez s'il y a des tokens dans les en-têtes HTTP
  76.         $accessToken $request->request->get('Authorization');
  77.         $refreshToken $request->request->get('Refresh-Token');
  78.         $session $request->getSession();
  79.         if ($request->request->has('Refresh-Token')) {
  80.             $this->removeOidcSession($session);
  81.             $session->set('oidc_access_token'$accessToken);
  82.             $session->set('oidc_refresh_token'$refreshToken);
  83.             $expirationTime $this->getExpirationTime($accessToken);
  84.             if ($expirationTime time()) {
  85.                 throw new UserNotFoundException('Session utilisateur expirée.');
  86.             }
  87.             setcookie("celaneo_access_token"""time() - 3600);
  88.             setcookie("celaneo_refresh_token"""time() - 3600);
  89.             setcookie("celaneo_access_token"$accessTokentime() + $expirationTime);
  90.             $session->set('oidc_expires_token'$expirationTime);
  91.             $decoded $this->keycloakApiService->getPublicKey($accessToken);
  92.             if (array_key_exists('email', (array)$decoded) && $expirationTime time()) {
  93.                 $email $decoded->email;
  94.                 $userBadge = new UserBadge($email);
  95.                 // Retournez un Passport avec l'utilisateur validé
  96.                 $passport = new SelfValidatingPassport($userBadge, [new PreAuthenticatedUserBadge()]);
  97.                 $passport->setAttribute(TokensBag::class, new TokensBag($session->get('oidc_access_token')
  98.                     ?? null$session->get('oidc_refresh_token')));
  99.                 return $passport;
  100.             } else {
  101.                 throw new UserNotFoundException('Unauthorized access');
  102.             }
  103.         }
  104.         // Si aucun token valide n'est trouvé dans les en-têtes HTTP, suivre le flux OIDC habituel
  105.         $sessionState $session->get(self::STATE_SESSION_KEY);
  106.         $queryState $request->get(self::STATE_QUERY_KEY);
  107.         if ($queryState === null || $queryState !== $sessionState) {
  108.             throw new InvalidStateException(sprintf(
  109.                 'query state (%s) is not the same as session state (%s)',
  110.                 $queryState ?? 'NULL',
  111.                 $sessionState ?? 'NULL',
  112.             ));
  113.         }
  114.         $session->remove(self::STATE_SESSION_KEY);
  115.         try {
  116.             $response $this->keycloakApiService->getTokenFromAuthorizationCode($request->query->get('code'''));
  117.         } catch (HttpExceptionInterface $e) {
  118.             throw new OpenIdServerException(sprintf(
  119.                 'Bad status code returned by openID server (%s)',
  120.                 $e->getStatusCode(),
  121.             ), previous$e);
  122.         }
  123.         $responseData json_decode($responsetrue);
  124.         if (false === $responseData) {
  125.             throw new OpenIdServerException(sprintf('Can\'t parse json in response: %s'$response));
  126.         }
  127.         $jwtToken $responseData['id_token'] ?? null;
  128.         if (null === $jwtToken) {
  129.             throw new OpenIdServerException(sprintf('No access token found in response %s'$response));
  130.         }
  131.         $refreshToken $responseData['refresh_token'] ?? null;
  132.         if (null === $refreshToken) {
  133.             throw new RuntimeException(sprintf('No refresh token found in response %s'$response));
  134.         }
  135.         $decoded $this->keycloakApiService->getPublicKey($responseData['access_token']);
  136.         if (array_key_exists('email', (array)$decoded)) {
  137.             $email $decoded->email;
  138.             $this->removeOidcSession($session);
  139.             $session->set('oidc_access_token'$responseData['access_token']);
  140.             $session->set('oidc_refresh_token'$responseData['refresh_token']);
  141.             $session->set('oidc_expires_token'$responseData['expires_in']);
  142.             $session->set('oidc_expires_refresh_token'$responseData['refresh_expires_in']);
  143.             setcookie("celaneo_access_token"""time() - 3600);
  144.             setcookie("celaneo_refresh_token"""time() - 3600);
  145.             setcookie("celaneo_access_token"$responseData['access_token'], time() + $responseData['expires_in']);
  146.             setcookie("celaneo_refresh_token"$responseData['refresh_token'], time() + $responseData['refresh_expires_in']);
  147.             $this->logger->info(">>>>>>>>>>>>>>>>>> 1er ACCESS TOKEN LORS DE LA CONNEXION >>>>>>>>>>>>>>>>>>");
  148.             $this->logger->debug($responseData['access_token']);
  149.             $this->logger->info("<<<<<<<<<<<<<<<<<< 1er ACCESS TOKEN LORS DE LA CONNEXION <<<<<<<<<<<<<<<<<<");
  150.             $this->logger->info(">>>>>>>>>>>>>>>>>> 1er REFRESH TOKEN LORS DE LA CONNEXION >>>>>>>>>>>>>>>>>>");
  151.             $this->logger->debug($responseData['refresh_token']);
  152.             $this->logger->info("<<<<<<<<<<<<<<<<<< 1er REFRESH TOKEN LORS DE LA CONNEXION <<<<<<<<<<<<<<<<<<");
  153.             $this->requestStack->getCurrentRequest()->attributes->set('_app_jwt_expires'$responseData['expires_in']);
  154.             $userBadge = new UserBadge($email);
  155.             $passport = new SelfValidatingPassport($userBadge, [new PreAuthenticatedUserBadge()]);
  156.             $passport->setAttribute(TokensBag::class, new TokensBag($session->get('oidc_access_token')
  157.                 ?? null$session->get('oidc_refresh_token')));
  158.             return $passport;
  159.         } else {
  160.             throw new UserNotFoundException('Unauthorized access');
  161.         }
  162.     }
  163.     public function onAuthenticationSuccess(Request $requestTokenInterface $tokenstring $providerKey): RedirectResponse
  164.     {
  165.         $this->logger->info("User connexion success");
  166.         return new RedirectResponse($this->urlGenerator->generate('home'));
  167.     }
  168.     public function onAuthenticationFailure(Request $requestAuthenticationException $exception): Response
  169.     {
  170.         $this->logger->info("User connexion failed");
  171.         $request->getSession()->getFlashBag()->add(
  172.             'error',
  173.             'An authentication error occured',
  174.         );
  175.         if ($request->attributes->get('_route') != 'login' ||
  176.             $request->attributes->get('_route') == 'oidc_redirect_uri') {
  177.             return new RedirectResponse($this->urlGenerator->generate('user_not_found'));
  178.         }
  179.         return new RedirectResponse($this->urlGenerator->generate('logout'));
  180.     }
  181.     public function start(Request $requestAuthenticationException $authException null): Response
  182.     {
  183.         if ($request->request->has('Refresh-Token')) {
  184.             $refreshToken $request->request->get('Refresh-Token');
  185.             $response $this->keycloakApiService->getTokenFromRefreshToken($refreshToken);
  186.             $responseData json_decode($responsetrue);
  187.             $session $request->getSession();
  188.             $session->remove('refresh_con');
  189.             $session->set('refresh_con'$responseData['refresh_token']);
  190.             return new RedirectResponse($this->urlGenerator->generate('home'));
  191.         } else {
  192.             $scheme $request->getScheme();
  193.             $uri $scheme ':' $this->urlGenerator->generate('oidc_redirect_uri', [], UrlGeneratorInterface::NETWORK_PATH);
  194.             $state Uuid::uuid4()->toString();
  195.             $request->getSession()->set(self::STATE_SESSION_KEY$state);
  196.             $qs http_build_query([
  197.                 'client_id' => $this->clientId,
  198.                 'response_type' => 'code',
  199.                 'state' => $state,
  200.                 'scope' => "openid roles profile email",
  201.                 'redirect_uri' => $uri,
  202.             ]);
  203.             return new RedirectResponse(sprintf("%s/realms/%s/protocol/openid-connect/auth?%s",
  204.                 $this->authorizationEndpoint$this->keycloakRealmName$qs));
  205.         }
  206.     }
  207.     public function createToken(Passport $passportstring $firewallName): TokenInterface
  208.     {
  209.         $token parent::createToken($passport$firewallName);
  210.         $currentRequest $this->requestStack->getCurrentRequest();
  211.         if (null === $currentRequest) {
  212.             throw new LogicException(sprintf('%s can only be used in an http context'__CLASS__));
  213.         }
  214.         $jwtExpires $currentRequest->attributes->get('_app_jwt_expires');
  215.         if (null === $jwtExpires) {
  216.             throw new \LogicException('Missing _app_jwt_expires in the session');
  217.         }
  218.         $currentRequest->attributes->remove('_app_jwt_expires');
  219.         $tokens $passport->getAttribute(TokensBag::class);
  220.         if (null === $tokens) {
  221.             throw new \LogicException(sprintf('Can\'t find %s in passport attributes'TokensBag::class));
  222.         }
  223.         $token->setAttribute(TokensBag::class, $tokens->withExpiration($jwtExpires));
  224.         return $token;
  225.     }
  226.     public function isInteractive(): bool
  227.     {
  228.         return true;
  229.     }
  230.     public function removeOidcSession($session)
  231.     {
  232.         $session->remove('oidc_access_token');
  233.         $session->remove('oidc_refresh_token');
  234.         $session->remove('oidc_expires_token');
  235.         $session->remove('oidc_expires_refresh_token');
  236.     }
  237. }