<?php
namespace App\Security;
use App\Security\Dto\TokensBag;
use App\Security\Exception\OpenIdServerException;
use App\Service\KeycloakApiService;
use App\Security\Exception\InvalidStateException;
use App\Utils\JwtUtils;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use LogicException;
use RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
class KeycloakAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
{
use JwtUtils;
private const STATE_QUERY_KEY = 'state';
private const STATE_SESSION_KEY = 'openid_state';
public const LOGIN_ROUTE = 'login';
public const OIDC_LOGIN_ROUTE = 'oidc_redirect_uri';
private EntityManagerInterface $em;
private RouterInterface $router;
private UrlGeneratorInterface $urlGenerator;
private KeycloakApiService $keycloakApiService;
private ParameterBagInterface $param;
private RequestStack $requestStack;
private LoggerInterface $logger;
private string $clientId;
private string $keycloakRealmName;
private string $authorizationEndpoint;
public function __construct(
RouterInterface $router,
KeycloakApiService $keycloakApiService,
UrlGeneratorInterface $urlGenerator,
ParameterBagInterface $param,
RequestStack $requestStack,
LoggerInterface $logger,
) {
$this->router = $router;
$this->keycloakApiService = $keycloakApiService;
$this->urlGenerator = $urlGenerator;
$this->param = $param;
$this->requestStack = $requestStack;
$this->logger = $logger;
$this->clientId = $this->param->get('keycloak_client_id');
$this->keycloakRealmName = $this->param->get('keycloak_realms_name');
$this->authorizationEndpoint = $this->param->get('keycloak_app_url');
}
public function supports(Request $request): ?bool
{
return $request->request->get('Refresh-Token') ??
($request->attributes->get('_route') === self::OIDC_LOGIN_ROUTE
|| $request->attributes->get('_route') === self::LOGIN_ROUTE);
}
public function authenticate(Request $request): Passport
{
// Vérifiez s'il y a des tokens dans les en-têtes HTTP
$accessToken = $request->request->get('Authorization');
$refreshToken = $request->request->get('Refresh-Token');
$session = $request->getSession();
if ($request->request->has('Refresh-Token')) {
$this->removeOidcSession($session);
$session->set('oidc_access_token', $accessToken);
$session->set('oidc_refresh_token', $refreshToken);
$expirationTime = $this->getExpirationTime($accessToken);
if ($expirationTime < time()) {
throw new UserNotFoundException('Session utilisateur expirée.');
}
setcookie("celaneo_access_token", "", time() - 3600);
setcookie("celaneo_refresh_token", "", time() - 3600);
setcookie("celaneo_access_token", $accessToken, time() + $expirationTime);
$session->set('oidc_expires_token', $expirationTime);
$decoded = $this->keycloakApiService->getPublicKey($accessToken);
if (array_key_exists('email', (array)$decoded) && $expirationTime > time()) {
$email = $decoded->email;
$userBadge = new UserBadge($email);
// Retournez un Passport avec l'utilisateur validé
$passport = new SelfValidatingPassport($userBadge, [new PreAuthenticatedUserBadge()]);
$passport->setAttribute(TokensBag::class, new TokensBag($session->get('oidc_access_token')
?? null, $session->get('oidc_refresh_token')));
return $passport;
} else {
throw new UserNotFoundException('Unauthorized access');
}
}
// Si aucun token valide n'est trouvé dans les en-têtes HTTP, suivre le flux OIDC habituel
$sessionState = $session->get(self::STATE_SESSION_KEY);
$queryState = $request->get(self::STATE_QUERY_KEY);
if ($queryState === null || $queryState !== $sessionState) {
throw new InvalidStateException(sprintf(
'query state (%s) is not the same as session state (%s)',
$queryState ?? 'NULL',
$sessionState ?? 'NULL',
));
}
$session->remove(self::STATE_SESSION_KEY);
try {
$response = $this->keycloakApiService->getTokenFromAuthorizationCode($request->query->get('code', ''));
} catch (HttpExceptionInterface $e) {
throw new OpenIdServerException(sprintf(
'Bad status code returned by openID server (%s)',
$e->getStatusCode(),
), previous: $e);
}
$responseData = json_decode($response, true);
if (false === $responseData) {
throw new OpenIdServerException(sprintf('Can\'t parse json in response: %s', $response));
}
$jwtToken = $responseData['id_token'] ?? null;
if (null === $jwtToken) {
throw new OpenIdServerException(sprintf('No access token found in response %s', $response));
}
$refreshToken = $responseData['refresh_token'] ?? null;
if (null === $refreshToken) {
throw new RuntimeException(sprintf('No refresh token found in response %s', $response));
}
$decoded = $this->keycloakApiService->getPublicKey($responseData['access_token']);
if (array_key_exists('email', (array)$decoded)) {
$email = $decoded->email;
$this->removeOidcSession($session);
$session->set('oidc_access_token', $responseData['access_token']);
$session->set('oidc_refresh_token', $responseData['refresh_token']);
$session->set('oidc_expires_token', $responseData['expires_in']);
$session->set('oidc_expires_refresh_token', $responseData['refresh_expires_in']);
setcookie("celaneo_access_token", "", time() - 3600);
setcookie("celaneo_refresh_token", "", time() - 3600);
setcookie("celaneo_access_token", $responseData['access_token'], time() + $responseData['expires_in']);
setcookie("celaneo_refresh_token", $responseData['refresh_token'], time() + $responseData['refresh_expires_in']);
$this->logger->info(">>>>>>>>>>>>>>>>>> 1er ACCESS TOKEN LORS DE LA CONNEXION >>>>>>>>>>>>>>>>>>");
$this->logger->debug($responseData['access_token']);
$this->logger->info("<<<<<<<<<<<<<<<<<< 1er ACCESS TOKEN LORS DE LA CONNEXION <<<<<<<<<<<<<<<<<<");
$this->logger->info(">>>>>>>>>>>>>>>>>> 1er REFRESH TOKEN LORS DE LA CONNEXION >>>>>>>>>>>>>>>>>>");
$this->logger->debug($responseData['refresh_token']);
$this->logger->info("<<<<<<<<<<<<<<<<<< 1er REFRESH TOKEN LORS DE LA CONNEXION <<<<<<<<<<<<<<<<<<");
$this->requestStack->getCurrentRequest()->attributes->set('_app_jwt_expires', $responseData['expires_in']);
$userBadge = new UserBadge($email);
$passport = new SelfValidatingPassport($userBadge, [new PreAuthenticatedUserBadge()]);
$passport->setAttribute(TokensBag::class, new TokensBag($session->get('oidc_access_token')
?? null, $session->get('oidc_refresh_token')));
return $passport;
} else {
throw new UserNotFoundException('Unauthorized access');
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): RedirectResponse
{
$this->logger->info("User connexion success");
return new RedirectResponse($this->urlGenerator->generate('home'));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
$this->logger->info("User connexion failed");
$request->getSession()->getFlashBag()->add(
'error',
'An authentication error occured',
);
if ($request->attributes->get('_route') != 'login' ||
$request->attributes->get('_route') == 'oidc_redirect_uri') {
return new RedirectResponse($this->urlGenerator->generate('user_not_found'));
}
return new RedirectResponse($this->urlGenerator->generate('logout'));
}
public function start(Request $request, AuthenticationException $authException = null): Response
{
if ($request->request->has('Refresh-Token')) {
$refreshToken = $request->request->get('Refresh-Token');
$response = $this->keycloakApiService->getTokenFromRefreshToken($refreshToken);
$responseData = json_decode($response, true);
$session = $request->getSession();
$session->remove('refresh_con');
$session->set('refresh_con', $responseData['refresh_token']);
return new RedirectResponse($this->urlGenerator->generate('home'));
} else {
$scheme = $request->getScheme();
$uri = $scheme . ':' . $this->urlGenerator->generate('oidc_redirect_uri', [], UrlGeneratorInterface::NETWORK_PATH);
$state = Uuid::uuid4()->toString();
$request->getSession()->set(self::STATE_SESSION_KEY, $state);
$qs = http_build_query([
'client_id' => $this->clientId,
'response_type' => 'code',
'state' => $state,
'scope' => "openid roles profile email",
'redirect_uri' => $uri,
]);
return new RedirectResponse(sprintf("%s/realms/%s/protocol/openid-connect/auth?%s",
$this->authorizationEndpoint, $this->keycloakRealmName, $qs));
}
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
$token = parent::createToken($passport, $firewallName);
$currentRequest = $this->requestStack->getCurrentRequest();
if (null === $currentRequest) {
throw new LogicException(sprintf('%s can only be used in an http context', __CLASS__));
}
$jwtExpires = $currentRequest->attributes->get('_app_jwt_expires');
if (null === $jwtExpires) {
throw new \LogicException('Missing _app_jwt_expires in the session');
}
$currentRequest->attributes->remove('_app_jwt_expires');
$tokens = $passport->getAttribute(TokensBag::class);
if (null === $tokens) {
throw new \LogicException(sprintf('Can\'t find %s in passport attributes', TokensBag::class));
}
$token->setAttribute(TokensBag::class, $tokens->withExpiration($jwtExpires));
return $token;
}
public function isInteractive(): bool
{
return true;
}
public function removeOidcSession($session)
{
$session->remove('oidc_access_token');
$session->remove('oidc_refresh_token');
$session->remove('oidc_expires_token');
$session->remove('oidc_expires_refresh_token');
}
}