Article rédigé par la Direction technique d’Atol CD :
Thomas Broyer, Xavier Calland et Laurent Meunier.

# Introduction

Le but de cet article est de voir comment il est possible de centraliser la gestion des utilisateurs et des rôles pour les rendre communs à plusieurs applications du monde SIG. Ces applications sont un GeoServer (application serveur de diffusion de flux cartographiques normalisés) et une application classique de visualisation de couches cartographiques (également appelée appli Viewer dans le reste de l’article) basée sur le composants Javascript OpenLayers.

Il s’agit d’un exemple simple, mais qui permet de démontrer la faisabilité dans un SIG plus complexe faisant intervenir plusieurs applications. Mais tout d’abord, essayons d’expliquer pourquoi ce besoin est apparu…

# Options possibles

Lors de différents projets, nous avons pu mettre en œuvre GeoServer dans plusieurs contextes applicatifs différents, chacun ayant sa propre manière de gérer les droits et les habilitations.
Une solution est de gérer les permissions directement dans GeoServer, il faut alors créer tous nos utilisateurs et rôles, peupler les rôles, puis affecter les rôles et permissions aux différentes couches. Et comme souvent GeoServer est couplé à une application métier dans laquelle il faut également gérer les permissions, on est donc reparti, dans l’application métier cette fois-ci, pour recréer utilisateurs/groupes/rôles, etc. Cela duplique donc la gestion des droits et habilitations dans autant d’applications présentes. Pas évident à mettre en place et encore moins à maintenir sur le long terme. Un oubli est vite arrivé et c’est la sécurité de l’accès aux données qui est impactée.

Une autre option consiste à placer GeoServer derrière notre application métier, celle-ci jouant alors le rôle de proxy filtrant pour les données cartographiques. L’application métier porte alors la gestion des permissions et autorise, ou non, l’accès à une donnée cartographique en fonction de l’utilisateur et de ses permissions. Cela évite d’avoir à maintenir les permissions dans GeoServer et peut sembler être une bonne option, mais ce n’est pas le plus efficient :

  • on paie le coût du passage obligatoire par l’application métier : problème de performance, bug potentiel dans la gestion des permissions
  • si une autre application métier vient se greffer par la suite, il faudra à nouveau dupliquer la gestion des habilitations dans cette nouvelle application
  • que se passera-t-il lorsque les utilisateurs devront accéder directement à GeoServer ?

# Un exemple

Par exemple, c’est cette seconde option qui a été retenue pour le développement de l’application Remocra. L’accès aux données cartographiques est défini en fonction des zones (au sens géographique) de compétence d’un utilisateur. L’application Remocra est en proxy devant GeoServer : les zones, utilisateurs et permissions sont définis dans Remocra qui filtrera les données cartographiques hébergées par GeoServer en fonction des zones de compétence d’un utilisateur.

En fonction de la cible et du niveau de fonctionnalités visé, ces deux options peuvent être viables. Sauf que nous rencontrons de plus en plus souvent des SIG composés d’un ensemble de services plutôt qu’une unique application monolithique. La centralisation de la gestion des utilisateurs et des rôles devient alors un élément clé de l’architecture globale du SIG

# Et si on centralisait tout ça ?

Cette centralisation a pour but de faciliter la gestion des habilitations au sein d’un même SIG. Les utilisateurs et les rôles sont définis à un unique endroit et sont repris ensuite dans les applications pour y définir les permissions.
Par exemple, un rôle “Administrateur” appliqué à un utilisateur permettra à celui-ci d’avoir les droits d’administrateur dans toutes les applications. Charge ensuite à chaque application de définir les autorisations liées au rôle “Administrateur”.

Effet de bord plutôt pratique (et souvent recherché), cette centralisation permet également l’authentification unique (SSO) entre toutes les applications du SIG.

Le principe

# Des protocoles standards

Pour notre POC, nous allons mettre en œuvre les protocoles OpenID Connect, pour la partie authentification, et OAuth 2.0, pour la partie autorisation.
Le protocole d’authentification OpenID Connect, basé sur OAuth, se prête particulièrement bien à l’exercice. Protocole standard, il est géré par la plupart des applications, soit nativement, soit via des plugins. Ce protocole est utilisé par GeoServer et le Viewer pour connaître l’identité de la personne souhaitant accéder à l’application. Le Viewer est ici une application JS cliente, sans backend, on va donc utiliser un client public avec le flow Authorization Code avec PKCE.
Le protocole OAuth 2.0 est utilisé pour l’accès à GeoServer depuis le Viewer. Les requêtes HTTP émises par le Viewer et à destination du GeoServer contiennent les informations nécessaires au protocole OAuth 2.0 pour permettre à GeoServer d’autoriser, ou non, l’accès aux données (aux couches).

# Un service pour gérer ces protocoles

Côté gestion des identités et des accès, c’est Keycloak qui assurera ce rôle. Les utilisateurs et les rôles seront créés et gérés uniquement dans Keycloak. Les applications délègueront la partie authentification à Keycloak via le protocole OpenID Connect.

# Deux applicatifs à intégrer

Comme indiqué en introduction, GeoServer sera notre serveur cartographique. Plusieurs couches seront définies dans GeoServer et chacune sera accessible à un rôle spécifique créé dans Keycloak. L’accès à la console d’administration web de GeoServer sera protégée par une authentification gérée par Keycloak. Le mécanisme d’authentification suivra le protocole OpenID Connect avec un passage de l’utilisateur vers Keycloak pour authentification.


Et finalement, notre application cartographique. Application purement JavaScript, s’exécutant uniquement côté navigateur. Cette application a pour but d’authentifier l’utilisateur sur Keycloak, puis d’afficher les couches hébergées par GeoServer, couches accessibles aux rôles portés par utilisateur authentifié.
Lors de l’accès à l’application, celle-ci va renvoyer automatiquement l’utilisateur vers Keycloak pour récupérer un Token. Keycloak authentifiera l’utilisateur si besoin, puis renverra des informations sur l’utilisateur et le fameux Token à notre Viewer. Ce Token sera ensuite utilisé pour échanger avec GeoServer. Lors des appels à GeoServer (que ce soit aux services OCG, à son API ou à son interface web), le Token sera ajouté à la requête et charge à GeoServer de contacter Keycloak pour valider ce Token.

Lors de la validation du Token par Keycloak, celui-ci renverra les rôles associés à l’utilisateur lié au Token. GeoServer sera donc capable de renvoyer la bonne liste des couches en fonction des rôles de l’utilisateur. Les accès à ces couches seront autorisés et les demandes d’accès aux autres couches seront refusées.

On note ici que GeoServer n’a pas besoin de « connaître » les utilisateurs. GeoServer se base directement sur les rôles de l’utilisateur indiqués par Keycloak.


Keycloak

L’installation de Keycloak est des plus classiques. Rien de spécial à noter. Aucune extension complémentaire n’est nécessaire pour ce POC.

Dans la configuration Keycloak, il faut créer deux nouveaux clients OpenID Connect :

  • un client “confidential” pour GeoServer (nommé “geoserver”)
  • un client “public” pour notre application cartographique (nommé “viewer”)

La création et la configuration de ces deux clients ne nécessite aucune action spécifique pour la partie authentification. Il faut juste penser à bien renseigner les URLs pour chaque client afin que Keycloak puisse valider les URL de redirection.

Concernant le client “geoserver”, il sera nécessaire d’activer les comptes de services et d’ajouter le bon rôle du client “realm-management” afin d’autoriser la lecture des rôles associés à ce client. Cela permettra à GeoServer de connaître les rôles présents dans Keycloak et de les proposer dans son interface d’administration pour la gestion des droits d’accès.

Côté Keycloak, il ne reste plus qu’à créer quelques comptes et rôles de tests. Point à noter, il faut que les rôles soient créés au niveau du client “geoserver”, sinon GeoServer ne verra pas les rôles. Si vous souhaitez utiliser des rôles communs à tous les clients, il est également possible de créer des rôles au niveau du Realm et d’utiliser ensuite le principe de rôle composite pour lier ces nouveaux rôles aux rôles du client. Cela permet notamment d’attribuer à un utilisateur un rôle « ADMIN » au niveau du Realm et de la mapper vers un rôle client sur lequel il peut y avoir une règle de nommage à respecter (par exemple « ROLE_ADMIN »), cela peut s’avérer très pratique si plusieurs clients ont des contraintes de nommage sur les rôles.

GeoServer

L’installation de GeoServer est également classique. Sur une installation de base, on lui ajoutera uniquement un module communautaire pour le brancher sur Keycloak.

# Un module externe

Le module Keycloak de GeoServer gère les deux protocoles OpenID Connect et OAuth 2.0 en fonction du cas d’usage :

  • OpenID Connect pour l’authentification au niveau de Keycloak (pour l’accès à la console d’administration de GeoServer par exemple)
  • OAuth 2.0 pour la validation, au niveau de Keycloak, du Token envoyé par le Viewer

# Un filtre d’authentification

Au niveau de GeoServer, cela se traduit par :

  • l’ajout et la configuration d’un filtre d’authentification de type “Keycloak OpenID Authentication” (nommé “keycloak_adapter”)
  • l’utilisation de ce nouveau filtre d’authentification “keycloak_adapter” au niveau de la chaîne d’authentification “default” (en remplacement de tous les filtres déjà présents sur cette chaîne)

# La gestion des rôles

Une autre fonctionnalité est présente pour récupérer, dans GeoServer, les rôles définis au niveau de Keycloak. Pour cela, le module va se connecter directement à l’API REST de Keycloak avec le compte de service activé précédemment et lister les rôles associés au client “geoserver”.

Au niveau de GeoServer, cela se traduit par :

  • l’ajout et la configuration d’un Service Rôle de type “Keycloak role service”
  • la modification des couches pour appliquer des permissions aux rôles récupérés depuis Keycloak

Cette récupération des rôles existants utilise une API propre à Keycloak. La récupération des rôles de l’utilisateur connecté se base, elle, sur une extension d’OpenID Connect propre à Keycloak également, le protocole ne fournissant pas de moyen standard d’y arriver.
Notre GeoServer est donc maintenant fortement lié à Keycloak. Dans le cas où Keycloak serait remplacé par un autre fournisseur d’identité (par exemple Azure AD), il faudrait utiliser les APIs et extensions propres à celui-ci, mais la mécanique générale serait identique.

# C’est fait !

À ce stade, il est possible de tester l’accès à la console d’administration web de GeoServer en passant par l’authentification Keycloak.
Si l’authentification est faite avec un utilisateur ayant le rôle “Administrateur”, l’accès est autorisé et l’ensemble des menus de configuration de GeoServer sont toujours disponibles. Au contraire, si l’authentification est faite avec un compte ne possédant pas le rôle “Administrateur”, l’accès est toujours possible mais uniquement certaines entrées du menu sont disponibles (dont la liste des couches configurées dans GeoServer, liste filtrée en fonction des rôles de l’utilisateur et des permissions appliquées aux couches).

Application cartographique

L’application cartographique est ici un simple viewer de données cartographiques, basé sur OpenLayers, et développé en React. Pour ce POC, l’accès requiert une authentification, c’est donc la première chose que fera l’application au chargement : authentifier l’utilisateur auprès de Keycloak, et récupérer un Token pour pouvoir communiquer avec GeoServer.

# Client Javascript de Keycloak

Pour l’authentification, nous avons utilisé le client JavaScript fourni par Keycloak, pour sa simplicité d’usage, mais rien dans le Viewer n’est spécifique à Keycloak (ce ne serait probablement pas le cas dans une réelle application métier, qui voudrait alors récupérer les rôles de l’utilisateur connecté). De même, nous sommes dans le cas d’une application sans aucun traitement serveur, donc toute l’authentification est gérée côté client, et l’application accède directement à GeoServer via CORS.

# Appels authentifiés à GeoServer

Notre Viewer appelle ensuite le GetCapabilities de GeoServer pour récupérer la liste des couches disponibles et les afficher, avec leur légende pour permettre de les masquer ou gérer leur opacité (l’interface est donc entièrement dynamique et data-driven). Cette requête GetCapabilities est un simple fetch, authentifié en passant notre Token dans une entête Authorization Bearer
Nous avons donc besoin ici de configurer OpenLayers pour passer notre Token à chaque requête qu’il aura besoin de faire à GeoServer. Pour celà, OpenLayers permet de redéfinir le chargement par défaut de chaque couche en lui passant une tileLoadFunction ou imageLoadFunction en fonction du type de couche. Au lieu de faire un simple chargement d’image, comme le fait l’implémentation par défaut, nous allons donc faire un fetch authentifié là encore, comme pour le GetCapabilities, et récupérer l’image dans un Blob, qu’on pourra alors charger comme une image via URL.createObjectURL(blob).

C’est simple ?

Afin de rester concentré sur le cœur du sujet (centraliser la gestion des utilisateurs et de leurs rôles entre GeoServer un Viewer cartographique) nous avons volontairement mis de côté quelques points annexes qu’il nous semble intéressant d’évoquer, même rapidement, maintenant.

# Flux de connexion sur les requêtes ajax

Lorsqu’une requête non authentifiée arrive à GeoServer, le protocole prévoit un redirection de l’utilisateur vers Keycloak pour qu’il se connecte, cela se fait avec une réponse 302 location à la requête HTTP.

Ce fonctionnement pose problème lorsqu’il s’agit d’une requête ajax, puisque la redirection n’est pas effectuée.

Pour gérer correctement le cas de requêtes ajax non authentifiées, il faut :

  • Passer une entête X-Requested-With: XMLHttpRequest dans les appels fetch dans le Viewer.
  • Configurer le filtre d’authentification “Keycloak OpenID Authentication” dans GeoServer en précisant “autodetect-bearer-only: true” dans la configuration JSON exportée depuis Keycloak.

Ainsi, le module Keycloak dans GeoServer détecte les requêtes comme bearer only (en se basant sur l’entête X-Requested-With) et renvoie un code de statut HTTP 401 en cas d’erreur plutôt que de faire une redirection vers Keycloak

# Création des images dans le Viewer

Nous avons vu que la récupération des images dans le Viewer passe par un fetch nous obligeant à charger ensuite l’image avec URL.createObjectURL(blob).

Idéalement, ces URLs devraient être mises de côté pour pouvoir ensuite libérer la mémoire via URL.revokeObjectURL, mais pour notre POC nous ne sommes pas allés jusque là, on se contentera donc de laisser le navigateur libérer la mémoire quand on quittera l’application.

# Scopes OAuth pour restreindre les permissions

Dans la solution mise en place pour le POC nous n’avons pas géré de scopes OAuth, le token obtenu donne donc accès à toutes les applications configurées dans Keycloak (même si on n’a ici que GeoServer).

Il est envisageable voire recommandé que le token demandé par le Viewer soit limité en audience à GeoServer, voire même qu’il limite les rôles de l’utilisateur que GeoServer verra à des rôles de lecture seule (pour empêcher l’utilisation du token pour faire des mises à jour sur GeoServer) ; ceci en précisant un scope OAuth, configuré côté Keycloak.

Cela permettrait d’avoir une meilleure sécurisation de l’application.

# Gestion des erreurs côté client

Le périmètre du Viewer est volontairement réduit et ne gère pas tout un tas de cas qu’il faudrait prévoir dans une application réelle.

Par exemple, si la récupération de données depuis GeoServer renvoie un code 401, cela peut arriver si le token envoyé n’est plus valide, le Viewer ne va pas traiter cette erreur. Il faudrait intégrer un processus de réauthentification et de récupération d’un nouveau token.

L’utilisation de refresh-token serait probablement retenue.

# Gestion de l’authentification côté client ?

Pour ce POC nous avons choisi de gérer l’authentification du Viewer côté Javascript. Cela permettait d’avoir un Viewer simplifié (uniquement du code côté client) et permettait d’aborder l’ensemble des problématiques souhaitées (flux entre les applications, gestion des token, etc).

Dans une réelle application métier, on opterait probablement pour une authentification gérée côté serveur avec ensuite un mécanisme de session standard, et l’accès à GeoServer passerait par le serveur applicatif qui authentifiera l’utilisateur via sa session et ajoutera alors le Token avant de passer la requête à GeoServer (le Token resterait donc uniquement côté serveur pour une sécurité accrue).

Le bilan

Ce POC a permis de démontrer la faisabilité technique de déporter l’authentification et la gestion des utilisateurs/rôles en dehors de GeoServer. Le choix s’est porté sur Keycloak car il s’agit d’une brique déjà connue à Atol C&D et qui remplit tous les critères pour ce POC. 

Juste un point d’attention, le remplacement de Keycloak par une autre brique équivalente (comme CAS, LemonLDAP::NG ou ADFS) ne sera pas forcément aisé.

Il faudra revoir l’intégration dans GeoServer, avec Keycloak nous avons pu bénéficier d’un plugin existant. De même la récupération des rôles dans GeoServer utilise la REST API de Keycloak, ce point n’est pas standard (contrairement à l’authentification via OpenID Connect), il faudra donc trouver un équivalent.