Introduction

Nous devions faire évoluer une application du service public réalisée avec des technologies obsolètes (framework et dépendances non mis à jour, nombreuses CVE) pour y ajouter de nouvelles fonctionnalités. Pour des raisons de priorité et de moyens, il a été décidé de ne pas tout refaire en premier lieu. Cette application est server-side : la génération des pages est réalisée côté serveur.

Architecture initiale Architecture initiale

Nous avons choisi de réaliser les nouvelles fonctionnalités dans un socle technique plus pérenne. Il y aura donc 2 applications et il faudra passer de l’une à l’autre de manière transparente afin de maintenir la simplicité de navigation pour un utilisateur. Il nous fallait donc mettre en place un service d’authentification SSO pour n’avoir à se connecter qu’une seule fois et faire en sorte d’avoir la même charte graphique sur les nouveaux services (nouvelle application). Cette nouvelle application sera client-side : les pages seront créées côté client et les requêtes seront envoyées sur le serveur quand cela sera nécessaire.

Architecture cible avec serveur SSO Architecture cible avec serveur SSO

Cette stratégie permettra de basculer les fonctionnalités déjà réalisées dans la nouvelle application à terme.

Mise en place du service d’authentification SSO

Nous avons choisi d’utiliser la solution Keycloak Server, un serveur de gestion des identités et des accès open-source qui permet de gérer l’authentification unique avec plusieurs protocoles (OpenID Connect, OAuth 2.0 et SAML2.0). Il prend en charge la connexion à des annuaires externes type LDAP ou Active Directory. Nous allons créer un domaine (ou royaume) pour définir l’ensemble des utilisateurs, leurs rôles et les applications auxquelles ils auront accès.

Ajouter un domaine Ajouter un domaine

Nous choisirons le protocole OpenID Connect pour l’ensemble des clients définis par la suite. L’application existante fournit son propre système d’authentification :

  • un formulaire sur la page d’accueil permet à l’utilisateur de saisir son adresse mail ainsi que son mot de passe
  • un mécanisme de session HTTP maintient les données de l’utilisateur connecté côté serveur
  • une gestion des utilisateurs permet aux administrateurs de créer et modifier des utilisateurs et de changer leurs permissions

1ère étape : intégration du service d’authentification à l’application existante

Pour migrer le système d’authentification dans Keycloak, nous allons commencer par créer le client dans le domaine :

Création du client legacy Création du client legacy

Nous le laissons Actif avec le flow Standard, désactivons “Direct access grants enabled” qui ne sera pas utile dans notre cas, saisissons les URLs de redirections valides et choisissons l’Access Type confidential : c’est le type à utiliser pour les applications server-side. Il permet d’autoriser seulement l’application à initier la demande de login pour le client ID donné. Cela nécessite un secret partagé entre le serveur d’authentification et l’application non visible par les utilisateurs (OAuth2).

Côté application, si l’utilisateur souhaite se connecter (bouton Se Connecter) il sera redirigé vers l’interface d’authentification du domaine (Keycloak). Pour mettre en œuvre OIDC, nous aurons besoin de nouveaux endpoints sur l’ancienne application :

  • /callback : il devra aller récupérer l’identité de l’utilisateur connecté et nous permettra d’initier la session utilisateur par défaut ou bien de la supprimer si le paramètre logoutendpoint est renseigné
  • /logout : il déconnectera l’utilisateur de Keycloak

Un framework de sécurité était déjà en place : Shiro qui permet de contrôler l’authentification et les autorisations. Pour compléter ce framework et proposer ces endpoints, nous utilisons la bibliothèque pac4j.

1
2
3
4
5
6
7
8
9
10
11
12
<!-- License Apache 2.0 -->
<dependency>
    <groupId>io.buji</groupId>
    <artifactId>buji-pac4j</artifactId>
    <version>7.0.0</version>
</dependency>
<!-- License Apache 2.0 -->
<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>pac4j-oidc</artifactId>
    <version>5.4.3</version>
</dependency>

À présent, il faut activer “Backchannel Logout Session Required” dans la configuration du client pour indiquer au service Keycloak d’appeler une URL pour invalider la session de l’utilisateur. Cette URL est à renseigner dans “Backchannel Logout URL” :

1
http://<client-host>/callback?logoutendpoint=true

2ème étape : provisionnement des utilisateurs dans le service d’authentification

À présent, le service d’authentification vérifie l’accès aux utilisateurs (email et mot de passe) depuis sa base de données. Il faut donc adapter la gestion utilisateurs existante pour que la base de données soit provisionnée sur l’ajout d’un utilisateur et supprimer l’authentification et la modification du mot de passe. Pour cela, nous utilisons l’API Admin de Keycloak. Le client Keycloak “service-public.legacy” doit avoir accès aux API d’Admin et avoir les rôles de gestion des utilisateurs. Nous devons donc activer “Service Accounts Enabled” dans la configuration Keycloak du client.

Note : Il faut éviter d’exposer l’API admin de Keycloak sur internet pour des raisons de sécurité. Seul le service doit pouvoir y accéder.

Activation du Service Accounts Activation du Service Accounts

Puis, il faut assigner les rôles “manage-users”, “view-authorization”, “view-users” du client “realm-management” dans la partie “Service Account Roles” :

Rôles à assigner Rôles à assigner

Keycloak fournit une bibliothèque Java pour utiliser son API : keycloak-admin-client dans la même version que celle du service pour garantir la compatibilité.

1
2
3
4
5
6
<!-- License Apache 2.0 -->
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-admin-client</artifactId>
    <version>18.0.0</version>
</dependency>

Voici la configuration nécessaire pour utiliser l’API :

1
2
3
4
5
6
7
Keycloack keycloak = KeycloakBuilder.builder()
   .serverUrl(<serveur Keycloak>)
   .realm("service-public.usagers")
   .grantType(CLIENT_CREDENTIALS)
   .clientId("service-public.legacy")
   .clientSecret(<secret du projet legacy>)
   .build();

Nous pourrons alors créer les utilisateurs depuis l’ancienne application avec le code suivant :

1
2
3
4
5
6
7
8
try (Response response = keycloak
        .realm("service-public.usagers")
        .users()
        .create(buildUserRepresentation(user))) {
    if ( !response.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL)) {
        throw new Exception(response.getStatusInfo().getReasonPhrase());
    }
}

La documentation est accessible ici : https://www.keycloak.org/docs-api/18.0/javadocs/org/keycloak/admin/client/KeycloakBuilder.html

3ème étape : configuration du nouveau service

Le point d’entrée de l’application se fera sur le nouveau service. Il est composé d’une partie frontend : l’application web et d’une partie backend, l’API. Pour la partie frontend, nous devrons déclarer un nouveau client dans le même domaine Keycloak :

Création du nouveau client Création du nouveau client

Nous le laissons Actif avec le flow Standard, désactivons « Direct access grants enabled » qui ne sera pas utile dans notre cas, saisissons les URLs de redirections valides et choisissons l’Access Type public : c’est le type à utiliser pour les applications client-side. Il n’est pas utile de partager un secret car il serait vu par l’utilisateur (embarqué dans l’application téléchargée). N’importe quelle application qui a une URL validée (Valid redirect URI) pourra initier la demande de login.

Cette nouvelle application est réalisée avec le framework Angular. Nous utilisons donc la dépendance keycloak-angular (https://github.com/mauriciovigolo/keycloak-angular) qui nous permet de s’interfacer simplement avec le service. Nous utilisons la configuration suivante :

1
2
3
4
5
6
7
8
9
10
{
    "config": {
        "url": "<serveur Keycloak>/auth",
        "realm": "service-public.usagers",
        "clientId": "service-public.nouveau"
    },
    "initOptions": {
        "onLoad": "check-sso"
    }
}

Côté backend, il suffira de vérifier que le Bearer token fourni est bien issu du domaine du serveur Keycloak. Ce backend est réalisé avec Spring Boot, nous devons donc ajouter la dépendance suivante :

1
2
3
4
5
6
<!-- License Apache 2.0 -->
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
    <version>18.0.0</version>
</dependency>

Spécifier l’adapter :

1
2
3
4
5
6
7
8
9
10
11
12
<dependencyManagement>
    <dependencies>
        <!-- License Apache 2.0 -->
        <dependency>
            <groupId>org.keycloak.bom</groupId>
            <artifactId>keycloak-adapter-bom</artifactId>
            <version>18.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Et intégrer la configuration suivante :

1
2
3
4
5
6
keycloak:
  enabled: true
  auth-server-url: <URL Serveur Keycloak>
  realm: service-public.usagers
  resource: service-public.nouveau.backend
  bearer-only: true

Nous précisons ici bearer-only: true pour indiquer que l’accès à l’API se fait seulement après une vérification du Bearer Token. Cela permet d’éviter les redirections pour que l’utilisateur s’authentifie.

Note: Depuis la réalisation de la migration, l’adapter Keycloak est passé deprecated https://www.keycloak.org/2022/02/adapter-deprecation. Il faut donc préférer l’utilisation de Spring Security avec OpenID Connect.

Conclusion

Depuis que cette solution est mise en œuvre, l’ajout de nouvelles fonctionnalités s’est fait de manière beaucoup plus sereine et nous avons pu migrer certaines anciennes fonctionnalités au fil de l’eau dès qu’il y avait besoin de les faire évoluer.

La gestion des utilisateurs a pu être également complètement revue dans un module séparé sans impacter le système existant.

Le service Keycloak a eu un rôle crucial dans cette mise en œuvre, nous nous sommes rendus compte à quel point le service était complet et adaptable selon les besoins avec un haut niveau de customisation.

Enfin, cette réalisation confirme l’importance de décomposer son architecture en plusieurs services avec une utilité spécifique afin de limiter les impacts lors d’évolutions ou de migrations.