DEV Community

Cover image for Authentification OpenID Connect avec Symfony (3/3)
Guillaume
Guillaume

Posted on

Authentification OpenID Connect avec Symfony (3/3)

Cet article fait partie d'une série d'articles :

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Démarrez la base avec un simple :

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Démarrez le serveur interne à Symfony:

symfony serve
Enter fullscreen mode Exit fullscreen mode

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:
Creation utilisateur

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:
Alt Text
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

make security 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
Enter fullscreen mode Exit fullscreen mode

ajout keycloakId

Comme conseillé à la fin de l’exécution de la commande, on va créer un fichier de migration :

bin/console make:migration
Enter fullscreen mode Exit fullscreen mode

make migration

Puis exécuter cette migration:

bin/console doctrine:migrations:migrate
Enter fullscreen mode Exit fullscreen mode

Vérifiez dans votre base de données que la table "user" s'est bien créée :

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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 :

  1. L'utilisateur essaie de se connecter sur l'application Symfony
  2. 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).
  3. 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)
  4. 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)
  5. 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 }

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
 .......
Enter fullscreen mode Exit fullscreen mode

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
        );
    }
Enter fullscreen mode Exit fullscreen mode
  • 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';
    }
Enter fullscreen mode Exit fullscreen mode
  • 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());
    }
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
Enter fullscreen mode Exit fullscreen mode
  • 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);
    }
Enter fullscreen mode Exit fullscreen mode
  • 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);
    }
Enter fullscreen mode Exit fullscreen mode

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');
    }
Enter fullscreen mode Exit fullscreen mode

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

Et voilà ! Testons maintenant !

Démarrons notre serveur Symfony:

symfony serve
Enter fullscreen mode Exit fullscreen mode

Puis ouvrez votre navigateur à l'adresse http://localhost:8000/dashboard.

Vous êtes automatique redirigé sur le serveur Keycloak:
login keycloak

Puis Keycloak vous demande votre consentement sur les données partagées avec l'application Symfony:
Consentement

Après consentement, on revient bien sur notre page dashboard ....AUTHENTIFIE !

YEEEESSS

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 (7)

Collapse
 
manonworld profile image
Mostafa Atwa

That's what I am talking about. Really Comprehensive, up to date and honest.

Collapse
 
samswunk profile image
samswunk

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 ?

Collapse
 
gbtux profile image
Guillaume

Oups, désolé pas vu ton message ... à mon avis ça ressemble à un probleme de firewall. Donc vérifie ton security.yml.

Collapse
 
ginopane profile image
Siarhei Karavai
  1. 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"

  2. The KeycloalAuthenticator guard must be listed in guards section of security.yaml.

  3. 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

Collapse
 
ginopane profile image
Siarhei Karavai

Oh, you have #2 at the end, I missed that

Collapse
 
charlybrown4487 profile image
charlybrown4487

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 ?

Collapse
 
drlouiza profile image
droai louiza

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?