Cet article fait partie d'une série d'articles :
- Introduction
- Mettre en place un serveur OpenID Connect avec Keycloak
- Symfony et Keycloak (cet article)
D'abord "rendons à César ce qui appartient à César" : merci à Grafikart pour son excellent article/vidéo sur le sujet d'Oauth/OpenID : "Authentification sociale sur Symfony".
Evidemment, si je fais cet article, c'est qu'il y a une différence! Nous n'utiliserons pas ni Github, ni Google ou autre, mais notre Keycloak, et il y a quelques subtilités qui valent bien un article :D
Création du projet
Note : un projet Symfony 5 est utilisé ici, mais Symfony 4.4 est tout à fait utilisable.
Installez la commande symfony comme expliqué ici.
Puis, créez un projet (ici en version full/application web):
symfony new TestKeycloak --full
Pour avoir une route de test dans notre application, on va créer une simple route /dashboard.
Dans le répertoire de notre projet, créons le controller:
bin/console make:controller DashboardController
On va laisser le controller par défaut, ça n'a aucune importance pour notre exemple.
On va aussi laisser le template dans son état initial.
Configuration de la base de données
Il faut configurer la base de données.
Personnellement, s'agissant d'un projet de test, je fais ça avec docker et docker-compose pour démarrer une base PostgreSQL.
Si vous voulez faire comme moi, créez un fichier docker-compose.yaml à la racine de votre projet avec le contenu suivant:
version: '3'
services:
database:
image: postgres:13-alpine
environment:
POSTGRES_USER: main
POSTGRES_PASSWORD: main
POSTGRES_DB: keycloak
ports:
- 5432:5432
Démarrez la base avec un simple :
docker-compose up -d
Configurez ensuite la base dans le projet en créant le fichier .env.local à la racine du projet avec le contenu suivant:
DATABASE_URL="postgresql://main:main@127.0.0.1:5432/keycloak?serverVersion=13&charset=utf8"
Démarrez le serveur interne à Symfony:
symfony serve
Puis rendez-vous dans votre navigateur à l'adresse http://localhost:8000/dashboard
Notre projet initial est en place, voyons pour la partie authentification.
Création d'un utilisateur dans Keycloak
Dans notre cas, nous n'avons pas de fournisseur externe d'identité, comme dans une entreprise, avec un annuaire LDAP ou Active Directory, ou une base de données quelconque.
Mais pas de soucis, on va pouvoir créer dans notre Keycloak des utilisateurs de test.
Pour cela, rendez-vous dans l'administration de votre Keycloak : https://votre_domaine/auth/admin.
Dans le menu Manage/Users cliquez sur le bouton "Add user" et créer un utilisateur comme suit:
Dans les détails de l'utilisateur créé, allez dans l'onglet "Credentials" et saisissez un mot de passe (avec confirmation) puis cliquez sur "Set password"
Note: toutes les manipulations seront faites sur le Realm (Domaine) "Master". Keycloak est complexe, et comporte de multiples fonctionnalités. Lisez la doc ;)
Création d'un client Keycloak
Chaque application "cliente" de Keycloak doit être configurée.
Pour créer un client pour notre application Symfony, allez dans le menu "Clients" dans l'interface d'admin de Keycloak puis sur le bouton "Create".
Ajoutez les informations comme suit:
Rien de difficile ici, on donne un nom à notre client, on utilise le protocole OpenID et l'URL de notre projet est bien http://localhost:8000
Cliquez sur "save" pour passer au panneau de contrôle de notre client.
Modifiez les valeurs suivantes :
- Consent Required doit être à ON
- Toggle Display client on consent screen doit être à ON
- Toggle Implicit Flow Enabled doit être à ON
- Set Access Type doit être à confidential
Laissez la valeur "Valid redirect URIs" à http://localhost:8000/* pour le moment, même si à terme il faudra sécuriser les URLs de redirection vers notre application Symfony.
Enregistrez la configuration et basculez sur l'onglet "Credentials".
Copiez le "secret", et insérez 3 variables d'environnement dans le fichier .env.local de votre projet Symfony tel que:
DATABASE_URL="postgresql://main:main@127.0.0.1:5432/keycloak?serverVersion=13&charset=utf8"
KEYCLOAK_SECRET=6b008eb2-e4c8-4afe-8016-cd59f3843d93
KEYCLOAK_CLIENTID=symfony
KEYCLOAK_APP_URL=https://votre_domaine/auth
Attention : mettez bien "/auth" à la fin de l'URL de votre Keycloak.
Passons maintenant à Symfony.
Attention: je reprécise mon besoin : disposer d'une authentification 100% via Keycloak. Si vous voulez AUSSI disposer d'un formulaire de login en plus de Keycloak, c'est possible ! Allez voir la doc officielle de Symfony sur la composant Security
Mais nous on va faire simple ;)
Symfony Security
Le composant Security est un des plus complexes à appréhender. Le composant est complexe, mais il nous facilite grandement la vie, n'oubliez jamais ça ! Combien de fois voyons-nous des applications "faites à la main" et mal/pas sécurisées !
Pas de panique, on va y aller pas à pas.
Création d'une classe User
Nous avons besoin d'une classe "User" pour 2 raisons :
- pouvoir enregistrer cet objet en base de données
- manipuler un objet tout au long de notre processus d'authentification
Encore une fois, Symfony nous facilite la vie avec une simple commande:
bin/console make:user
Notre classe utilisateur est maintenant créée, avec son repository, mais nous avons besoin d'un champs supplémentaire : keycloakId.
Ce champs nous servira à associer l'utilisateur Keycloak à notre utilisateur local.
Rien ne vous empêche d'ajouter d'autre champs, puisque on le verra plus tard, Keycloak nous envoie des champs comme le nom que je ne stocke pas dans mon exemple.
Pour ajouter ce champs faites un simple :
bin/console make:entity User
Comme conseillé à la fin de l’exécution de la commande, on va créer un fichier de migration :
bin/console make:migration
Puis exécuter cette migration:
bin/console doctrine:migrations:migrate
Vérifiez dans votre base de données que la table "user" s'est bien créée :
Ajout des bundles "clients" pour OAuth et Keycloak
Heureusement pour nous, on ne va pas coder toute la partie cliente entre Symfony et keycloak.
Comme d'habitude avec Symfony, il existe des bundles et aujourd'hui 2 vous nous intéresser particulièrement :
Le premier est une sorte de coquille vide : si vous regardez la doc, vous verrez que comme Grafikart, vous pouvez choisir un client Github, mais aussi Discord, Facebook, et plein d'autres !
Le deuxième est donc notre "Provider", l'implémentation spécifique à Keycloak.
Installons-les :
composer require knpuniversity/oauth2-client-bundle
composer require stevenmaguire/oauth2-keycloak
Oui je sais je fais ça en 2 commandes alors qu'on pourrait faire ça en une ! Pourquoi ? Parce que je n'aime pas installer plusieurs bundles dans la même commande : si l'installation d'un bundle plante c'est plus facile à debugger :-D
L'installation du premier bundle va exécuter une "recipe" (recette) pour aller créer un fichier de configuration spécifique : le fichier "config/packages/knpu_oauth2_client.yaml"
C'est lui qui va nous servir à configurer la connexion à notre Keycloak. Comme nous avons créé des variables d'environnement (dans notre .env.local) ça va être simple :
knpu_oauth2_client:
clients:
keycloak:
type: keycloak
auth_server_url: '%env(KEYCLOAK_APP_URL)%'
realm: 'master'
client_id: '%env(KEYCLOAK_CLIENTID)%'
client_secret: '%env(KEYCLOAK_SECRET)%'
redirect_route: 'oauth_check'
Détaillons un peu:
- le nom de notre "client" (ici keycloak) est arbitraire;
- le type en revanche est imposé ;) et c'est pour préciser l'implémentation à utiliser au 1er bundle;
- auth_server_url : l'URL de notre Keycloak
- realm : comme dit précédemment, j'utilise le Realm/Domaine de base : "master"
- client_id : le nom du client qu'on a créé dans Keycloak
- client_secret: le secret généré par Keycloak
- redirect_route: le nom de la route à appeler sur laquelle Keycloak redirigera après l'authentification. C'est l'URL de "callback" dans la littérature Keycloak (cf explications de la cinématique Keycloak/Oauth dans le chapitre précédent)
Pas de panique, cette URL/route n'existe pas encore, mais on va la créer après.
Cinématique
Il faut maintenant expliquer comment le processus d'authentification va marcher :
- L'utilisateur essaie de se connecter sur l'application Symfony
- Le firewall de Symfony détecte qu'il n'est pas loggué et le revoit vers l'URL de login de l'application (/oauth/login).
- Le contrôleur Symfony derrière cette URL va "démarrer" le client Keycloak et renvoyer une réponse de redirection à l'utilisateur vers le serveur Keycloak (à partir des informations de la configuration)
- L'utilisateur s'authentifie dans Keycloak et il est alors redirigé vers l'URL de callback (transmise en paramètre de la réponse du .3)
- Le firewall voit la redirection de l'utilisateur après authentification dans Keycloak, vérifie les informations transmises et si elles sont bonnes, authentifie l'utilisateur.
Maintenant qu'on a compris le fonctionnement, il faut l'implémenter:
- configurer la sécurité de Symfony pour aller sur "/oauth/login" si on est pas authentifié (en bref, un firewall)
- créer un contrôleur pour implémenter la route "/oauth/login" en redirigeant l'utilisateur
- implémenter un firewall pour l'URL de callback
Configurer le firewall pour l'URL de login
Symfony et son composant sécurité a déjà beaucoup de mécanismes fournis, dont des "Provider".
On va donc se servir du form_login Authentication Provider pour rediriger automatiquement l'utilisateur s'il n'est pas authentifié vers notre URL personnalisée.
La configuration se fait dans config/packages/security.yaml
Configurez-le comme suit:
security:
encoders:
App\Entity\User:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
form_login:
login_path: oauth_login
access_control:
- { path: ^/dashboard, roles: ROLE_USER }
Quelques explications:
- la rubrique "encoders" a été normalement déjà configurée, de même que "providers" par l'utilisation de la commande make:user. On sait donc quelle est la classe utilisée pour "provider" nos utilisateurs, et qu'on utilise, si on stocke des mots de passe dans la base (donc hors spectre Keycloak), qu'ils seront sécurisés.
- pour les firewalls, dans le main, ajoutez les lignes "form_login" et "login_path: oauth_login". On déclare donc bien une route avec le nom "oauth_login" comme URL de rediection par défaut si on est pas authentifié.
- dans la rubrique "access_control", on a déclaré que toutes les URLs commençant par /dashboard ont nécessité à être protégées par une authentification et que l'utilisateur authentifié doit à minima posséder le rôle "ROLE_USER".
Une fois cette configuration sauvegardée, continuons en implémentons notre route de nom "oauth_login" et d'URL "/oauth/login".
Créez un contrôleur:
bin/console make:controller OAuthController --no-template
Puis implémentez la route dite comme suit:
<?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\RedirectResponse;
use Symfony\Component\Routing\Annotation\Route;
class OAuthController extends AbstractController
{
/**
* @Route("/oauth/login", name="oauth_login")
*/
public function index(ClientRegistry $clientRegistry): RedirectResponse
{
/** @var KeycloakClient $client */
$client = $clientRegistry->getClient('keycloak');
return $client->redirect();
}
/**
* @Route("/oauth/callback", name="oauth_check")
*/
public function check()
{
}
}
Dans la première fonction, comme expliqué dans la cinématique, on récupère le client Keycloak et on redirige l'utilisateur.
La 2ème fonction est là uniquement pour implémenter l'URL de callback.
Elle est en revanche vide, puisque c'est un autre composant qui va implémenter la logique et "prendre la main".
Ce composant, c'est Symfony Guard, et sa classe abstraite AbstractGuardAuthenticator.
Notre bundle KnpUOAuth2ClientBundle implémente justement une surcouche à ce composant : SocialAuthenticator.
On va donc pouvoir se servir de cette classe abstraite pour implémenter notre firewall de "callback".
Dans le projet, créez un répertoire src/Security et une classe KeycloakAuthenticator telle que :
<?php
namespace App\Security;
use App\Entity\User;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Class KeycloakAuthenticator
*/
class KeycloakAuthenticator extends SocialAuthenticator
{
private $clientRegistry;
private $em;
private $router;
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
{
$this->clientRegistry = $clientRegistry;
$this->em = $em;
$this->router = $router;
}
.......
Notre classe étends bien le SocialAuthenticator, et on va avoir besoin de:
- ClientRegistry: le gestionnaire de clients OAuth
- EntityManagerInterface: pour lire/écrire dans la base de données
- RouterInterface: lire une route/URL
On les injecte dans le constructeur via l'injection de dépendances. Donc pas de configuration particulière à faire dans services.yaml.
Etendre le SocialAuthenticator nous oblige à implémenter un certain nombre de méthodes que voici:
- start: méthode appelée en cas d'erreur si l'authentification n'est pas envoyée dans la requête Pour nous : ```
public function start(Request $request, \Symfony\Component\Security\Core\Exception\AuthenticationException $authException = null)
{
return new RedirectResponse(
'/oauth/login', // might be the site, where users choose their oauth provider
Response::HTTP_TEMPORARY_REDIRECT
);
}
* supports: méthode appelée sur toutes les requêtes pour savoir si on déclenche cet Authenticator ou pas.
Pour nous:
public function supports(Request $request)
{
return $request->attributes->get('_route') === 'oauth_check';
}
* getCredentials: détermine comment on récupère les informations d'authentification dans la requête pour les passer en paramètre de la fonction *getUser*
Pour nous:
public function getCredentials(Request $request)
{
return $this->fetchAccessToken($this->getKeycloakClient());
}
* getUser : c'est LA fonction de l'authenticator : comment récupérer l'utilisateur. Ici, on a 3 possibilités :
- soit l'utilisateur existe et s'est déjà connecté avec Keycloak
- soit l'utilisateur existe dans la base mais ne s'est jamais connecté avec Keycloak
- soit l'utilisateur n'existe pas du tout et on le crée
Ainsi:
public function getUser($credentials, \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider)
{
$keycloakUser = $this->getKeycloakClient()->fetchUserFromToken($credentials);
//existing user ?
$existingUser = $this
->em
->getRepository(User::class)
->findOneBy(['keycloakId' => $keycloakUser->getId()]);
if ($existingUser) {
return $existingUser;
}
// if user exist but never connected with keycloak
$email = $keycloakUser->getEmail();
/** @var User $userInDatabase */
$userInDatabase = $this->em->getRepository(User::class)
->findOneBy(['email' => $email]);
if($userInDatabase) {
$userInDatabase->setKeycloakId($keycloakUser->getId());
$this->em->persist($userInDatabase);
$this->em->flush();
return $userInDatabase;
}
//user not exist in database
$user = new User();
$user->setKeycloakId($keycloakUser->getId());
$user->setEmail($keycloakUser->getEmail());
$user->setRoles(['ROLE_USER']);
$this->em->persist($user);
$this->em->flush();
return $user;
}
* onAuthenticationFailure: message renvoyé quand l'authentification échoue
Pour nous:
public function onAuthenticationFailure(Request $request, \Symfony\Component\Security\Core\Exception\AuthenticationException $exception)
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}
* onAuthenticationSuccess: que se passe-t-il après l'authentification ? Ici on redirige vers la page /dashboard
Pour nous:
public function onAuthenticationSuccess(Request $request, \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token, string $providerKey)
{
// change "app_homepage" to some route in your app
$targetUrl = $this->router->generate('dashboard');
return new RedirectResponse($targetUrl);
}
Et une fonction de récupération du client à partir du clientRegistry injecté dans le constructeur:
/**
* @return \KnpU\OAuth2ClientBundle\Client\Provider\KeycloakClient
*/
private function getKeycloakClient()
{
return $this->clientRegistry->getClient('keycloak');
}
Voilà ! Notre Authenticator est maintenant créé.
Pour l'utiliser, modifions encore notre fichier de configuration de la sécurité (config/packages/security.yaml):
![utilisation du Guard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3wkfkkbek34vic5crsx8.png)
Et voilà ! Testons maintenant !
Démarrons notre serveur Symfony:
symfony serve
Puis ouvrez votre navigateur à l'adresse [http://localhost:8000/dashboard](http://localhost:8000/dashboard).
Vous êtes automatique redirigé sur le serveur Keycloak:
![login keycloak](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6w8o5s4fyubdp9thghvz.png)
Puis Keycloak vous demande votre consentement sur les données partagées avec l'application Symfony:
![Consentement](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg7gku24vf2b6dki9ivv.png)
Après consentement, on revient bien sur notre page dashboard ....AUTHENTIFIE !
![YEEEESSS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eqfy80qlxavpx52by49z.png)
Voilà un petit aperçu des possibilités de Keycloak et de son intégration avec Symfony.
Des questions ? Des remarques ? N'hésitez pas !
Top comments (8)
That's what I am talking about. Really Comprehensive, up to date and honest.
Bonjour et merci beaucoup pour le tuto.
J'ai l'erreur suivante au retour de l'authentification :
Invalid response received from Authorization Server. Expected JSON.
J'ai vérifié, la réponse de l'API Userinfo de keycloack est vide, une idée svp ?
Bonjour,
vous avez trouvez une solution a votre erreur?
car je rencontre la même erreur et je trouves toujours pas de solution.
une idée svp?
Bonjour,
Avez vous résolue l'erreur? Je rencontre la même erreur.
Bonjour et merci pour ce tuto. Pas de chance pour moi, je rencontre le pb de bouclage des pages. Lorsque je me logg, tout fonctionne bien, onAuthenticationSuccess est déclenchée et un dd me permet de constater que j'ai bien les infos du user. Par contre, je ne sais pas pourquoi, le chargement de la page dashboard tourne en boucle, le Tokeninterface n'existe plus lors du chargement du Dashboard. J'ai du manquer quelque chose, mais rien n'y fait, je ne trouve pas.
Pourrais tu m'aider ?
Oups, désolé pas vu ton message ... à mon avis ça ressemble à un probleme de firewall. Donc vérifie ton security.yml.
The route "oauth/login" must be allowed anonymously in your security.yaml, or app won't be able to serve the page. The same for the "oauth/callback"
The KeycloalAuthenticator guard must be listed in guards section of security.yaml.
Correct scopes might need to be set for the client on Keycloak. Such as "profile" and "email" are default. If you don't set the login attempt may fail with "invalid_scopes" error
Oh, you have #2 at the end, I missed that