Ajout de l'authentification Keycloak dans un projet Symfony 7
Installation de KnpUOAuth2ClientBundle
composer require knpuniversity/oauth2-client-bundle
composer require stevenmaguire/oauth2-keycloak
Configuration du provider Keycloak
knpu_oauth2_client:
clients:
keycloak:
type: keycloak
auth_server_url: "%env(KEYCLOAK_APP_URL)%"
realm: "%env(KEYCLOAK_REALM)%"
client_id: "%env(KEYCLOAK_CLIENTID)%"
client_secret: "%env(KEYCLOAK_SECRET)%"
redirect_route: "oauth_callback"
version: "22.0.4"
Création de la classe User
php bin/console make:user
php bin/console make:entity User
php bin/console make:migration
php bin/console d:m:m
Génération des routes nécessaires
php bin/console make:controller OauthController
<?php
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\Provider\KeycloakClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/oauth', name: 'oauth_')]
class OauthController extends AbstractController
{
public function __construct(
private readonly ClientRegistry $clientRegistry
) {
}
#[Route('/login', name: 'login')]
public function login(): Response
{
return $this->getKeycloakClient()->redirect(['roles', 'profile', 'email', 'openid']);
}
#[Route('/callback', name: 'callback')]
public function callback(Request $request): void
{
}
#[Route('/logout', name: 'logout')]
public function logout(): Response
{
}
private function getKeycloakClient(): KeycloakClient
{
/** @var KeycloakClient **/
return $this->clientRegistry->getClient("keycloak");
}
}
php bin/console make:auth
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
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;
class GPTKeycloakAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
public function __construct(
private readonly ClientRegistry $clientRegistry,
private readonly EntityManagerInterface $entityManager,
private readonly RouterInterface $router
)
{
}
public function supports(Request $request): ?bool
{
return $request->attributes->get("_route") === "oauth_callback";
}
public function authenticate(Request $request): Passport
{
$client = $this->clientRegistry->getClient('keycloak');
$accessToken = $this->fetchAccessToken($client);
return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
/** @var KeycloakResourceOwner $keycloakUser */
$keycloakUser = $client->fetchUserFromToken($accessToken);
$email = $keycloakUser->getEmail();
$existingUser = $this->entityManager->getRepository(User::class)->findOneBy(['keycloakId' => $keycloakUser->getId()]);
if ($existingUser) {
$existingUser->setFullname($keycloakUser->getName());
$existingUser->setEmail($email);
$this->entityManager->persist($existingUser);
$this->entityManager->flush();
return $existingUser;
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $email]);
if (!$user) {
$user = new User();
$user->setEmail($email);
}
$user->setFullname($keycloakUser->getName());
$user->setKeycloakId($keycloakUser->getId());
$this->entityManager->persist($user);
$this->entityManager->flush();
return $user;
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new RedirectResponse($this->router->generate("app_home"));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}
public function start(Request $request, AuthenticationException $authException = null): Response
{
return new RedirectResponse($this->router->generate("oauth_login"), Response::HTTP_TEMPORARY_REDIRECT);
}
}
parameters:
keycloak.base_url: '%env(KEYCLOAK_APP_URL)%'
keycloak.realm: '%env(KEYCLOAK_REALM)%'
keycloak.client_id: '%env(KEYCLOAK_CLIENTID)%'
security:
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
main:
logout:
path: /oauth/logout
php bin/console make:listener LogoutSubscriber Symfony\Component\Security\Http\Event\LogoutEvent
<?php
namespace App\EventSubscriber;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly ParameterBagInterface $parameterBag,
private readonly RouterInterface $router
) {
}
public function onLogoutEvent(LogoutEvent $event): void
{
$response = new RedirectResponse(
$this->generateKeycloakLogoutUrl(),
Response::HTTP_SEE_OTHER
);
$event->setResponse($response);
}
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => 'onLogoutEvent',
];
}
private function generateKeycloakLogoutUrl(): string
{
return sprintf("%s/realms/%s/protocol/openid-connect/logout?post_logout_redirect_uri=%s&client_id=%s", $this->parameterBag->get("keycloak.base_url"), $this->parameterBag->get("keycloak.realm"), urlencode($this->router->generate("app_home", [], UrlGeneratorInterface::ABSOLUTE_URL)), $this->parameterBag->get("keycloak.client_id"));
}
}
Ajouter les variables d'environnement
KEYCLOAK_SECRET=mon_secret
KEYCLOAK_CLIENTID=keycloak_client_id
KEYCLOAK_REALM=keycloak_realm
KEYCLOAK_APP_URL=https://keycloak.example.com
Last modified: 26 January 2024