/ 8 min read
SSO avec Keycloak et Symfony
Pourquoi Keycloak ?
Quand plusieurs applications d’un même écosystème doivent partager une authentification, gérer les utilisateurs manuellement dans chaque app devient vite un problème : chaque service a sa propre table users, sa propre logique de session, et si un utilisateur change son mot de passe sur l’une, les autres ne le savent pas.
Keycloak règle ça. C’est un serveur d’identité open source : un seul endroit pour gérer les utilisateurs, les rôles et les sessions. Toutes vos applications s’authentifient contre lui via OAuth2/OIDC, sans se parler entre elles.
Dans cet article, on met en place un SSO Keycloak complet avec Symfony — de la configuration du realm à la gestion des refresh tokens, avec Docker pour l’environnement local.
Architecture
Navigateur │ │ 1. L'utilisateur clique sur "Se connecter" ▼Symfony App ──── 2. Redirige vers Keycloak ────▶ Keycloak │ 3. Login/password │ ◀──── 4. Redirige avec un code d'autorisation ─────┘ │ │ 5. Symfony échange le code contre un JWT (access + refresh token) ▼Ressources protégéesKeycloak joue le rôle d’Authorization Server : il authentifie l’utilisateur et délivre les tokens. Symfony est le Client : il ne gère plus les mots de passe lui-même, il fait confiance aux tokens signés par Keycloak.
1. Lancer Keycloak avec Docker
On utilise start-dev uniquement pour le développement local — ce mode désactive HTTPS et certaines vérifications de sécurité, ce qui simplifie la mise en place. En production, on utilise start avec un certificat TLS.
services: keycloak: image: quay.io/keycloak/keycloak:24.0 command: start-dev environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin ports: - '8080:8080'docker compose up -dL’interface d’administration est accessible sur http://localhost:8080. Connectez-vous avec admin / admin.
2. Configurer le Realm
Un realm est un espace totalement isolé dans Keycloak : ses propres utilisateurs, ses propres clients, ses propres rôles. Keycloak en crée un par défaut nommé master — c’est le realm d’administration de Keycloak lui-même. On ne l’utilise jamais pour nos applications, pour éviter de mélanger les droits d’admin Keycloak avec les droits applicatifs.
- Dans l’interface, cliquez sur le menu déroulant en haut à gauche, puis Create Realm
- Nommez-le
mon-projet - Activez-le et sauvegardez
3. Créer le Client Symfony
Dans la terminologie Keycloak, un client représente une application qui va consommer le SSO. Chaque application (Symfony, une API, une app mobile) a son propre client, avec ses propres URLs et son propre secret.
On choisit le mode confidential (Client authentication activé) parce que Symfony tourne côté serveur et peut garder un secret — contrairement à une SPA JavaScript qui ne peut rien cacher.
- Clients > Create client
- Remplissez :
- Client ID :
symfony-app - Client authentication : activé (mode confidential)
- Client ID :
- Dans Settings, configurez les URLs autorisées. Keycloak les vérifie pour éviter les attaques de redirection :
- Valid redirect URIs :
https://mon-projet.fr/auth/keycloak/callback - Valid post logout redirect URIs :
https://mon-projet.fr/ - Web origins :
https://mon-projet.fr
- Valid redirect URIs :
- Dans Credentials, notez le Client Secret généré — c’est lui qu’on mettra dans le
.env.local
4. Créer les Rôles
Les rôles définissent ce qu’un utilisateur a le droit de faire dans votre application. On les définit dans Keycloak (et non dans Symfony) pour qu’ils soient centralisés : si un utilisateur change de rôle, il suffit de le modifier dans Keycloak, sans toucher au code.
- Clients > symfony-app > Roles > Create role
- Créez
ROLE_USERetROLE_ADMIN
Ces rôles doivent être inclus dans le JWT envoyé à Symfony. Pour ça, allez dans Clients > symfony-app > Client scopes, ouvrez le scope dédié, et vérifiez que le mapper de rôles a Add to ID token et Add to access token activés.
5. Créer un Utilisateur de test
- Users > Create new user, renseignez username et email, activez Email verified
- Onglet Credentials : définissez un mot de passe et désactivez Temporary (sinon l’utilisateur sera forcé de changer son mot de passe à la première connexion)
- Onglet Role mapping : assignez
ROLE_USER
6. Intégration côté Symfony
Deux packages, deux rôles distincts
composer require knpuniversity/oauth2-client-bundlecomposer require stevenmaguire/oauth2-keycloakCe ne sont pas des alternatives — les deux sont nécessaires et jouent des rôles différents :
-
knpuniversity/oauth2-client-bundle: c’est le bundle Symfony qui s’intègre au système de sécurité. Il fournit l’OAuth2Authenticator, gère le flow de redirection vers le serveur d’auth, et expose leClientRegistry. Il est générique et fonctionne avec n’importe quel provider OAuth2. -
stevenmaguire/oauth2-keycloak: c’est le provider spécifique à Keycloak. Il connaît les endpoints de Keycloak (/auth/realms/{realm}/protocol/openid-connect/...), sait parser leKeycloakResourceOwner, et gère la notion de realm. Sans lui, le bundle ne saurait pas comment parler à Keycloak.
Configuration du bundle
knpu_oauth2_client: clients: keycloak: type: keycloak # indique au bundle d'utiliser le provider Keycloak auth_server_url: '%env(KEYCLOAK_URL)%' realm: '%env(KEYCLOAK_REALM)%' client_id: '%env(KEYCLOAK_CLIENT_ID)%' client_secret: '%env(KEYCLOAK_CLIENT_SECRET)%' redirect_route: auth_keycloak_callback version: '24.0'Variables d’environnement
# .env.local — ne jamais commiter ce fichierKEYCLOAK_URL=http://localhost:8080KEYCLOAK_REALM=mon-projetKEYCLOAK_CLIENT_ID=symfony-appKEYCLOAK_CLIENT_SECRET=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxL’Authenticator
C’est le cœur de l’intégration côté Symfony. Il s’exécute uniquement quand Keycloak redirige l’utilisateur vers le callback, récupère le token d’accès, puis charge (ou crée) l’utilisateur correspondant en base.
<?phpnamespace App\Security;
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;
class KeycloakAuthenticator extends OAuth2Authenticator{ public function __construct( private ClientRegistry $clientRegistry, private RouterInterface $router, ) {}
// Ne s'active que sur la route de callback — les autres requêtes ne passent pas ici public function supports(Request $request): ?bool { return $request->attributes->get('_route') === 'auth_keycloak_callback'; }
public function authenticate(Request $request): Passport { $client = $this->clientRegistry->getClient('keycloak');
// Échange le code d'autorisation contre un access token + refresh token $accessToken = $this->fetchAccessToken($client);
return new SelfValidatingPassport( new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) { /** @var KeycloakResourceOwner $keycloakUser */ // Appel à Keycloak pour récupérer les infos de l'utilisateur (email, uuid, rôles...) $keycloakUser = $client->fetchUserFromToken($accessToken);
return $this->loadOrCreateUser($keycloakUser); }) ); }
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 { return new RedirectResponse($this->router->generate('app_login')); }
private function loadOrCreateUser(KeycloakResourceOwner $keycloakUser): mixed { // À adapter selon votre UserRepository // $keycloakUser->getId() → UUID Keycloak (stable, à utiliser comme identifiant) // $keycloakUser->getEmail() → email return null; // Remplacer par votre logique }}Le contrôleur OAuth
Deux routes suffisent : une pour déclencher la redirection vers Keycloak, une pour recevoir le callback. Le corps de callback() est vide car c’est l’authenticator qui prend la main.
<?phpnamespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Routing\Attribute\Route;
class SecurityController extends AbstractController{ #[Route('/auth/keycloak', name: 'auth_keycloak_start')] public function connect(ClientRegistry $clientRegistry): Response { // Les scopes demandés : openid (obligatoire), email et profile pour les infos user return $clientRegistry->getClient('keycloak')->redirect(['openid', 'email', 'profile']); }
#[Route('/auth/keycloak/callback', name: 'auth_keycloak_callback')] public function callback(): void { // Géré automatiquement par KeycloakAuthenticator }
#[Route('/logout', name: 'app_logout')] public function logout(): void { // Géré par le pare-feu Symfony Security }}Configuration du pare-feu
security: firewalls: main: lazy: true custom_authenticators: - App\Security\KeycloakAuthenticator logout: path: app_logout target: app_home
access_control: - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/app, roles: ROLE_USER }7. Gestion des Refresh Tokens
L’access token Keycloak est intentionnellement court (5 minutes par défaut). L’idée : s’il est volé, la fenêtre d’exploitation est réduite. Pour ne pas forcer l’utilisateur à se reconnecter toutes les 5 minutes, on utilise le refresh token — plus long, et échangeable contre un nouvel access token sans interaction de l’utilisateur.
Configurer les durées dans Keycloak
Dans Realm Settings > Tokens :
- Access Token Lifespan : 5 minutes — court par design
- SSO Session Max : 8 heures — durée de vie du refresh token, au-delà l’utilisateur doit se reconnecter
Rafraîchir le token côté Symfony
<?phpnamespace App\Service;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;use League\OAuth2\Client\Token\AccessToken;
class TokenRefreshService{ public function __construct(private ClientRegistry $clientRegistry) {}
public function refresh(string $refreshToken): AccessToken { $provider = $this->clientRegistry->getClient('keycloak')->getOAuth2Provider();
// Keycloak retourne un nouvel access token ET un nouveau refresh token return $provider->getAccessToken('refresh_token', [ 'refresh_token' => $refreshToken, ]); }}Stockez le refresh token en session lors de l’authentification et appelez ce service avant chaque appel à une ressource protégée.
8. Récupérer les Rôles depuis le Token JWT
Keycloak encode les rôles directement dans le JWT, dans le claim resource_access. Pour les extraire :
$tokenData = $accessToken->getValues();
// Les rôles du client "symfony-app" sont dans resource_access.symfony-app.roles$keycloakRoles = $tokenData['resource_access']['symfony-app']['roles'] ?? [];
// On les mappe ensuite sur les rôles Symfony dans le UserProviderC’est important de vérifier que les rôles viennent bien du bon client (symfony-app), et pas des rôles globaux du realm qui peuvent contenir d’autres permissions.
9. Docker Compose complet (dev + prod)
Pour que Symfony puisse appeler Keycloak via son nom de service Docker (keycloak), les deux doivent être dans le même réseau. On ajoute aussi PostgreSQL car Keycloak ne doit pas stocker ses données en mémoire en dehors du mode start-dev.
services: app: build: . environment: KEYCLOAK_URL: http://keycloak:8080 # nom du service Docker, pas localhost KEYCLOAK_REALM: mon-projet KEYCLOAK_CLIENT_ID: symfony-app KEYCLOAK_CLIENT_SECRET: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx depends_on: - keycloak
keycloak: image: quay.io/keycloak/keycloak:24.0 command: start-dev environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin KC_DB: postgres KC_DB_URL: jdbc:postgresql://db:5432/keycloak KC_DB_USERNAME: keycloak KC_DB_PASSWORD: keycloak depends_on: - db
db: image: postgres:16 environment: POSTGRES_DB: keycloak POSTGRES_USER: keycloak POSTGRES_PASSWORD: keycloakEn production : remplacez
start-devparstart, ajoutezKC_HOSTNAMEavec votre domaine, et faites passer Keycloak derrière un reverse proxy Nginx ou Traefik avec HTTPS.
Points à retenir
- Ne jamais utiliser le realm
masterpour vos applications — c’est réservé à l’administration de Keycloak - Access Tokens courts (5 min) + Refresh Tokens longs (8h) : c’est le bon équilibre sécurité/UX
- Le
client_secretne va que dans les variables d’environnement, jamais dans le code - En Docker, Symfony appelle Keycloak via son nom de service, pas
localhost - Les rôles dans le JWT viennent de
resource_access.{client-id}.roles, pas du niveau realm
Conclusion
Keycloak peut sembler imposant au premier abord parce qu’il y a beaucoup de concepts à poser (realm, client, scopes, mappers…). Mais une fois la configuration faite, l’intégration Symfony est relativement légère : un bundle, un authenticator, quelques routes. Et le vrai gain se mesure quand on ajoute une deuxième application : elle bénéficie du même SSO sans retoucher la gestion des utilisateurs.