Nous allons voir comment mettre en place une authentification OAuth 2.0 entre une application Symfony/Api Platform et l’outil de gestion d’authentification Keycloak.

Prérequis

  • Docker
  • Droit d’édition du fichier hosts

Sources de départ

Pour commencer, on a besoin de télécharger les éléments suivants :

Mise en place des certificats pour Api Platform

Les certificats sont générés dans api-platform-2.5.6/docker/dev-tls/Dockerfile. On ajoute les noms de domaines host.docker.internal et keycloak dans la liste des noms de domaines certifiés :

mkcert --cert-file localhost.crt --key-file localhost.key localhost host.docker.internal keycloak 127.0.0.1 ::1 mercure; \

Lancement du serveur d’Api Platform

Il faut changer le port du container Vulcain car ce port est également utilisé par le container de Keycloak. Cela se passe dans api-platform-2.5.6/docker-compose.yml :

vulcain:
  [...]
  ports:
    - target: 443
      published: 8444
      protocol: tcp

On peut maintenant exécuter les commandes suivantes pour fabriquer, télécharger et instancier les images Docker nécessaires au bon fonctionnement d’Api Platform :

docker-compose build
docker-compose pull
docker-compose up -d

Si vous obtenez l’erreur temporary error (try again later) alors il faut ajouter network: host dans toutes les étapes build du docker-compose.

Mise en place des certificats pour Keycloak

On va utiliser les certificats créés par Api Platform. Pour cela, dans le même dossier que le docker-compose.yml de Keycloak, exécutez :

docker cp api-platform-256_dev-tls_1:/certs/ .

Il reste à relier ces certificats au container Docker. Dans le docker-compose.yml :

keycloak:
  [...]
  volumes:
    - ./certs/localhost.crt:/etc/x509/https/tls.crt
    - ./certs/localhost.key:/etc/x509/https/tls.key

Lancement du serveur Keycloak

Avant de lancer le serveur Keycloak, il faut mapper le port https. Dans le docker-compose.yml :

keycloak:
  [...]
  ports:
    - 8080:8080
    - 8443:8443

On peut à présent exécuter les containers :

docker-compose pull
docker-compose up -d

Configuration du réseau Docker

Les containers d’Api Platform et Keycloak ont besoin d’être sur le même réseau Docker afin qu’ils puissent communiquer. On va connecter le réseau de Keycloak dans Api Platform.

Dans api-platform-2.5.6/

docker-compose down

On ajoute dans le docker-compose.yml la configuration du réseau :

[...]
networks:
  default:
    external:
      name: keycloak_default

et on relance

docker-compose up -d

Configuration du hosts

Il faut éditer le fichier hosts afin d’ajouter certains domaines vers Docker. Pour cela, ouvrez le fichier hosts de votre système dans un éditeur de texte en administrateur et ajoutez 127.0.0.1 host.docker.internal et 127.0.0.1 keycloak.

Configuration de Keycloak

Allez sur https://keycloak:8443 avec votre navigateur.

Une erreur de sécurité apparaît sur votre navigateur car le certificat créé par Api Platform n’est pas reconnu. Pour remédier à ce problème, il faut ajouter /certs/localCA.crt dans les autorités de confiance du navigateur.

Sur Keycloak, se connecter avec les identifiants :

username: Admin
password: Pa55w0rd

Créer un realm :

name: dev

Créer un client :

Client ID: api-service
Client Protocol: openid-connect

Configurer le client :

Access type: confidential
Service Accounts Enabled: on
Authorization Enabled: on
Root URL: https://host.docker.internal:8444
Valid Redirect URLs: /*
Web Origins: /

Créer un utilisateur et son mot de passe :

Installation des dépendances pour Api Platform

On a besoin de deux dépendances :

  • knpuniversity/oauth2-client-bundle qui intègre la gestion de l’OAuth 2.0 dans Symfony. Il est compatible avec un grand nombre de service OAuth 2.0. La liste des clients possibles est disponible sur packagist.
  • stevenmaguire/oauth2-keycloak, le client Keycloak pour la dépendance précédente.
docker exec -it api-platform-256_php_1 composer require stevenmaguire/oauth2-keycloak
docker exec -it api-platform-256_php_1 composer require knpuniversity/oauth2-client-bundle

Configuration d’Api Platform

Configuration des Trusted Hosts et des CORS Allow Origin

Ces réglages sont disponibles dans le fichier d’environnement /api/.env. On va ajouter le domaine host.docker.internal dans Trusted Hosts et CORS Allow Origin :

[...]
TRUSTED_HOSTS='^localhost|host.docker.internal|api$'
[...]
CORS_ALLOW_ORIGIN=^https?://(localhost|host.docker.internal|127\.0\.0\.1)(:[0-9]+)?$
[...]

Authentificateur

Dans un nouveau dossier Security dans /api/src, créez le fichier KeycloakAuthenticator.php :

<?php

namespace App\Security;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class KeycloakAuthenticator extends SocialAuthenticator
{
    private $clientRegistry;
    private $router;
    private $session;

    public function __construct(ClientRegistry $clientRegistry, RouterInterface $router, SessionInterface $session)
    {
        $this->clientRegistry = $clientRegistry;
        $this->router = $router;
        $this->session = $session;
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
        $path = $request->getPathInfo();
        $this->session->set("nextPath", $path);

        if ($request->headers->get("Authorization") === null) {
            return new RedirectResponse(
                '/connect/',
                Response::HTTP_TEMPORARY_REDIRECT
            );
        }

        $targetUrl = $this->router->generate('connect_check', ["Authorization" => $request->headers->get("Authorization")]);
        return new RedirectResponse(
            $targetUrl,
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }

    public function supports(Request $request)
    {
        return $request->attributes->get('_route') === 'connect_check';
    }

    public function getCredentials(Request $request)
    {
        if ($request->headers->get("Authorization") === null) {
            return $this->fetchAccessToken($this->getClient());
        }

        $token = str_replace("Bearer ", "", $request->headers->get("Authorization"));
        return new AccessToken(["access_token" => $token, "token_type" => "bearer"]);
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        try {
            return $userProvider->loadUserByUsername($this->getClient()->fetchUserFromToken($credentials)->getId());
        } catch (IdentityProviderException $e) {
            throw new UnauthorizedHttpException($e->getMessage());
        }
    }

    private function getClient()
    {
        return $this->clientRegistry->getClient('keycloak');
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());
        return new Response($message, Response::HTTP_FORBIDDEN);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
    {
        if ($this->session->has("nextPath")) {
            $path = $this->session->get("nextPath");
            $this->session->remove("nextPath");
            return new RedirectResponse($path, 307);
        }

        return new RedirectResponse("/");
    }
}

Celui-ci contient la classe KeycloakAuthenticator responsable de la gestion de l’authentification. Cette classe implémente les méthodes abstraites de SocialAuthentificator :

  • start : appelé lors d’un accès à une ressource protégée avec un utilisateur non authentifié et permet de lancer la procédure d’authentification en redirigeant la requête. Ici, on commence par stocker l’URL demandé par l’utilisateur afin de pouvoir le rediriger, une fois l’authentification effectuée. Ensuite, on regarde si la requête contient un token, si oui, il y a une redirection vers la procédure de vérification du token, sinon, il y a une redirection vers la page de login.
  • support : renvoie vrai si la requête est destinée à l’authentification. Ici, support renvoie vrai si la route est l’URL de vérification du token.
  • getCredentials : renvoie le token à partir de la requête. Si la requête contient un token alors getCredentials renvoie ce token sinon, récupère le token auprès de Keycloak.
  • getUser : renvoie un utilisateur à partir du token.
  • onAuthenticationFailure : action effectuée quand il y a une erreur lors d’une authentification. Ici, renvoie une réponse 403 Forbidden.
  • onAuthenticationSuccess : action effectuée quand une authentification réussie. Ici, redirige vers la page stockée en session lors de l’étape start.

Prévisualiser(ouvre un nouvel onglet)

Contrôleur

On va implémenter le contrôleur d’authentification AuthController :

<?php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class AuthController extends AbstractController
{
    /**
     * @Route("/connect", name="connect_start")
     */
    public function connectAction(ClientRegistry $clientRegistry) {
        return $clientRegistry
            ->getClient('keycloak')
            ->redirect(['offline_access']);
    }

    /**
     * @Route("/connect/check", name="connect_check")
     */
    public function connectCheckAction() {}

    /**
     * @Route("/logout", name="logout")
     */
    public function logoutAction() {}
}

Ce contrôleur répond à 3 routes :

  • /connect : redirige l’utilisateur vers la page d’authentification de Keycloak
  • /connect/check : vérifie la validé du token. L’implémentation est vide car la requête est interceptée par l’authentificateur.
  • /logout : déconnecte un utilisateur. L’implémentation est vide car la requête est interceptée par le composant de sécurité de Symfony.

Fichier de configuration

Il y a 2 fichiers de configuration à modifier :

  • knpu_oauth2_client.yaml : contenant la configuration des clients oauth2.
  • security.yaml : contenant les réglages de sécurité.
knpu_oauth2_client:
    clients:
        # configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
        keycloak:
            # must be "keycloak" - it activates that type!
            type: keycloak
            # add and set these environment variables in your .env files
            client_id: 'api-service'
            client_secret: '9aa10044-48e3-4f3e-89ea-37c4e8a81b58'
            # a route name you'll create
            redirect_route: connect_check
            redirect_params: {}
            # Keycloak server URL
            auth_server_url: https://keycloak:8443/auth
            # Keycloak realm
            realm: dev
            # whether to check OAuth2 "state": defaults to true
            use_state: false

Le client_secret est disponible dans Keycloak :

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        oauth:
            id: knpu.oauth2.user_provider
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: lazy
            provider: oauth
            logout:
                path:   logout
            guard:
                authenticators:
                    - App\Security\KeycloakAuthenticator

Entité

Ils ne reste plus qu’à configurer l’entité afin que seuls les utilisateurs connectés puissent accéder aux données. Pour cela, il faut ajouter attributes={"security"="is_granted('ROLE_USER')"} dans l’annotation @ApiResource de l’entité.

[...]
/**
 *
 * @ApiResource(
 *     attributes={"security"="is_granted('ROLE_USER')"}
 * )
 * @ORM\Entity
 */
class Greeting
[...]

Tests

Sur navigateur

On accède à Api Plarform sur https://host.docker.internal:8444. La première fois qu’on se connecte sur Api Platform, on n’est pas authentifié :

On accède maintenant à https://host.docker.internal:8444/greetings (une page nécessitant d’être connectée pour pouvoir y accéder). On est alors redirigé sur Keycloak pour rentrer ses identifiants puis, une fois les éléments de connexion entrés, on est à nouveau redirigé sur greetings, mais cette fois de manière authentifié :

Sur Postman

On créé une requête Get sur https://host.docker.internal:8444/greetings. Il faut sélectionner une authentification de type OAuth 2.0 avec les données d’authentification dans les Request Headers. Si on met un faux token on obtient :

Pour obtenir, un token valide, cliquez sur Get New Access Token :

Avec un token valide, on obtient bien notre réponse :

Conclusion

Nous avons vu comment :

  • Ajouter un client dans Keycloak afin de pouvoir authentifier des utilisateurs sur ce dernier.
  • Mettre une place et configurer une authentification OAuth 2.0 sur Api Platform.

L’ensemble des sources est disponible sur ce dépôt GitHub : https://github.com/INGENIANCE/Api-Plateform-With-Keycloak

Références


1 commentaire

EK · 24 septembre 2020 à 12 h 11 min

Bonjour,

Merci pour ce super article! Je souhaiterais utiliser ce tutoriel comme base pour connecter une application php a Keycloak et gérer les tokens.

J’ai dans un premier temps suivi votre tutoriel pour commencer. Mais lorsque j’arrive à l’étape d’ouvrir le lien https://host.docker.internal:8444/greetings, j’obtiens l’erreur suivante:

cURL error 60: SSL: no alternative certificate subject name matches target host name ‘keycloak’ (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)

Pourriez-vous svp m’indiquer une solution pour résoudre ce problème?

Merci d’avance.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Mise en place de l’authentification OAuth 2.0 entre Symfony/Api Platform et Keycloak

par Emmanuel Leroux temps de lecture : 7 min
1