Créer un modèle d'auth on-premises conciliant IdP et autonomie

À chaque début de projet on-premises, tout un tas de questions se posent autour de l’authentification et des autorisations.

Le récent développement de Medatarun, logiciel open-source et on-premises, justement, a été pour moi l’occasion de me reposer ces questions.

Comment les personnes vont se logger ? Qui crée les accès ? Où sont les rôles ? Comment faire avec les outils, les scripts, la CI, les agents ? Quels efforts du téléchargement à l’utilisation ? Et la phase post-installation, pas trop longue ? Cela va tenir sur la durée ?

Oui, cela fait beaucoup de sujets mais c’est la réalité à laquelle on doit faire face.

Tous les choix que nous devrons faire auront des conséquences en termes d’organisation, pour nous, nos utilisateurs, quelquefois engendreront des questions politiques, des conséquences sur la courbe d’adoption et sur l’argumentaire commercial.

En même temps, on est tous contraints par ce fameux time to market. Comme ces sujets sont aussi vastes et qu’inconfortables, on a repoussé le moment des décisions : strict minimum aujourd’hui, on livre et on verra plus tard… trop tard.

Ici, le sujet n’est pas « les 10 recettes de l’authentification facile ».

Le sujet est sur comment décider où une application doit s’arrêter.

D’expérience, je me dis que la plupart des problèmes d’authentification on-premises, ce ne sont pas les fonctionnalités manquantes ni les mauvaises implémentations. Ils viennent surtout de ce que l’on a fait, et que l’on aurait certainement jamais dû faire en premier lieu.

Aujourd’hui, notre manière de réfléchir les applications est une accumulation et un héritage de tout ce qu’il s’est passé depuis le début des années 2000. Les modèles survivent, mais il serait peut-être temps de les refactorer un peu et d’en abandonner une partie.

Dans ce qui a grandement changé, c’est que nos applications dépendent de plus en plus de systèmes externes complets. Cela pose la question:

Comment être autonome sans recréer un système d’identité complet ?

D’autre part, il faut se marteler en tête une chose pour continuer :

Mon application, Medatarun, n’est PAS un Identity Provider.

Son rôle est de gérer le sens des données, pas d’implémenter toutes les formes de sécurité possibles.

Dit autrement :

Oui la sécurité est au cœur du produit,

mais ce n’est pas le cœur du produit.

Pourquoi être dépendant d’un IdP n’est pas un bon choix

Première solution qui nous vient en tête :

On va juste demander aux utilisateurs de configurer des fournisseurs de tokens JWT et de configurer leur fournisseur OpenId Connect, typiquement Azure, Google, Auth0 ou Keycloak.

Sauf que, en réalité:

  • la plupart des équipes n’ont pas d’Identity Provider (IdP) du tout;
  • quand on en a déjà un (Google, Azure), il faut en avoir les droits d’accès;
  • un IdP SaaS tiers style Auth0, même pour tester, c’est une création de compte chez je ne sais pas qui, passer du temps dessus, pas simple pour tout le monde;
  • Keycloak ou un autre dans le même style, ok pour un développeur, et encore. C’est connaître Docker, du temps dans la doc, même pour le mode « dev ». Une fois que ça marche chez soi, le passage en préprod et en prod ce n’est plus la même limonade. Pour le multi-utilisateurs, prévoir SSL, proxy et PostgreSQL, c’est lourd.

Même si éventuellement ça passerait pour certaines petites équipes tech, dans un environnement contrôlé de plus grosses entreprises, c’est ingérable : réunions, dépendre d’autres services et personnes, règles d’entreprise.
Bref, une latence infinie.

Pire que tout: il faut faire tout cela avant même d’ouvrir mon application.

C’est un point bloquant, je ne veux pas mettre en péril l’adoption.

Dépendance IdP = pas d’IdP, pas de produit.

Pourquoi le mode user-centric n’est pas un bon choix

C’est le classique : base de données utilisateurs (juste pour démarrer, ou pérenne).

Oui, c’est facile pour les utilisateurs: on lance l’application, crée un compte admin, on crée d’autres utilisateurs et on affecte les rôles. Ce n’est pas négligeable en termes de coûts de développement, certes, on a l’habitude. Une fois que c’est fait, on retourne à notre “code métier”, crée l’IHM et on livre.

Oui, on livre les premières itérations relativement vite.

Mais la suite est malheureusement classique aussi :

  • API Keys vite implémentées, moche, mais ça passe, c’est du test qui satisfait les premiers clients qui partent en prod comme cela.
  • On sécurise avec OAuth, le fournisseur JWT est fait maison ou on utilise un fournisseur tiers
  • Pour les clients déjà en prod avec API Keys, il faut les faire migrer sur OAuth2…
  • SSO est demandé. On crée deux chemins dans l’IHM, on garde le formulaire login/mot de passe historique et on crée le chemin OpenId Connect.
  • Les utilisateurs « externes qui n’ont pas de mot de passe chez nous » cohabitent tant bien que mal avec les utilisateurs historiques.
  • L’IdP devient obligatoire en interne et sécurité renforcée partout, on joue entre la moitié de nos utilisateurs sont en SSO et l’autre avec login/password. Politiquement, cela devient compliqué.
  • L’IdP doit fournir les rôles : on jongle entre mappings et flux Idp-application
  • Les rôles doivent être associés aux clefs d’API, non prévu au départ
  • On veut ajouter des capacités MCP pour les agents IA, mais les canaux précédents sont surchargés et pas adaptés
  • enfin, il faut renforcer le logging et l’audit pour respecter la réglementation

Commencer user-centric, c’est créer plusieurs sources d’identité concurrentes à chaque nouveau besoin.

Chaque source a un coût individuel, mais il faut aussi ajouter ceux liés à la cohérence sur la durée, la maintenance, les fragilités et exceptions à tester, et des plans de refactorings successifs pour tenir un modèle stable. On ralentit, on a du mal à absorber, c’est disproportionné par rapport au cœur de métier.

Beaucoup d’entre nous en avons souffert. Aujourd’hui, on a une opportunité de changer pour un nouveau produit, alors utilisons-la.

User-centric = rapide à faire mais dette technique immédiate.

En voyant ainsi comment ce modèle a du mal à évoluer dans la durée, une nouvelle question se pose :

est-ce que ce ne serait pas notre définition d’un utilisateur qui serait défaillante ?

C’est “qui” un utilisateur aujourd’hui ?

Avant, un utilisateur c’était une personne, celui qui bouge la souris. Des fois, un service (mais on trichait un peu), d’où les comptes de service. L’outillage et les intégrations techniques ont contribué à bouger les lignes (et la triche). Plus on avance, plus les types de connexions s’accumulent, plus la définition semble floue. Aujourd’hui, même l’IHM n’est même plus manipulée que par des humains, on a des outils de test, des scripts, des agents IA.

Vu ainsi, faire passer les humains par un chemin et les machines par un autre n’a plus de sens. C’est d’ailleurs plus une conséquence de l’évolution du user-centric qu’une volonté d’architecture. Le temps passant, cela provoque des exceptions de comportement, des règles métier non documentées, des problèmes d’audit, ce même audit qui peine quand les outils s’authentifient au nom d’un humain.

Le principal changement ne serait-il pas cette indéfinition ? D’une certaine manière, le fait qu’une application définisse ce qu’est un utilisateur ne serait-il pas déjà un contre-sens ?

Posons une ligne :

Les applications ne devraient plus imposer implicitement ce qu’est un utilisateur

En tenant cette ligne, on évite de se battre avec des concepts où la notion d’utilisateur ne match pas vraiment: fournisseurs, automates, accès temporaires, comptes de services, agents IA, etc.

Ce problème du “qui”, on a des outils pour le résoudre, typiquement les tokens JWT.

Si le “qui”, n’est plus forcément humain et nous n’avons pas à définir ce “qui” dans nos applications, nous n’avons pas à savoir d’où il vient, si c’est un IdP, une base de données ou plusieurs IdP ou quelque chose d’autre.

Un token JWT nous dit « ok, voici 888-999-111, il peut accéder à ton application, tu peux l’appeler Pierre sur tes écrans et il vient de ton Microsoft Active Directory » et on peut le vérifier. Le point intéressant est que l’on ne parle plus d’utilisateur mais de subject, tout comme on en extrait l’origine de confiance, le issuer.

On peut en définir que cette nouvelle identité, humaine ou outil, qu’importe, s’appelle actor.

actor = issuer + subject

J’insiste un peu ici, mais j’ai vu plusieurs logiciels ne se baser que sur subject ou l’email du token JWT, pour définir l’identité. Cela a posé problème lors de changements d’IdP. C’est le piège à éviter.

Cet actor, on peut lui ajouter des caractéristiques, comme son nom d’affichage, son email ou toute information qu’un jeton JWT peut transporter. Il devient alors partie intégrante de notre application, comme nouvelle identité (avec son propre identifiant d’actor par exemple).

idissuersubjectdisplay_nameemail
1login.microsoftonline.com888-999-111Pierre Martinpierre@…
2accounts.google.com123-456-789CI Bot
3medatarun.local/oidc123456John Doe

Dans le “qui fait quoi”, passons au quoi.

Rôles: système ou application

On sait que l’on doit séparer authentification (login/password, accès) de l’autorisation (les rôles et permissions). Néanmoins, il faut un point où ce qui ressemble à un utilisateur et permissions se rencontrent.

Ce “qui fait quoi sur quoi” doit être défini quelque part.

Premier problème : est-ce que les rôles doivent être définis dans le système d’information ou dans les applications ou un mix des deux ?

Ce dont on est sûrs c’est que l’application doit connaître les rôles et permissions dont elle a besoin, à un moment donné, et c’est dans le code que cela se passe. La majorité des applications ne peuvent pas opérer sur des rôles qu’elles ne connaissent pas et qui ne sont pas fixés dès le début.

Mais rôles et permissions ne sont pas les seuls modèles d’autorisation : certes RBAC (rôle based) est populaire, mais on peut faire de l’attribute-based (ABAC), des ACL, PBAC, DAC, MAC, etc. Certaines politiques nécessitent aussi d’être évaluées à la volée avec du contexte applicatif.

Dans plusieurs entreprises, j’ai vu des projets de sécurité visant à remonter les rôles applicatifs dans l’IdP. Selon les cas, cela implique que les rôles puissent transiter par JWT (pas toujours possible), ou que l’IdP maintienne une forme de résolution des droits en restant connecté au contexte ou aux règles applicatives (architectures PDP et PEP). D’autres solutions existent, mais mais globalement, cela devient très lourd à implémenter.

Un autre point important, du point de vue d’une application, et à part quelques rôles liés à son système :

une décision d’autorisation applicative est toujours une décision métier

Par là, on entend que les stratégies d’autorisation sont souvent décidées localement du côté des équipes, par des utilisateurs finaux, par exemple les managers intermédiaires ou l’assistance technique, et pas des admin système.

C’est exactement ce qui se passe quand un manager crée un canal Teams et ajoute des personnes dedans, ou quand un responsable donne des droits d’action supplémentaires sur une zone géographique à un employé promu ou relocalisé, ou même un directeur achats qui autorise de passer par “ce” fournisseur à certaines boutiques et pas d’autres.

On a bien deux concepts ne doivent pas être mélangés:

l’autorisation système est différente de l’autorisation applicative

Alors où mettre la frontière entre les deux ?

Tout mettre dans l’IdP (quelle que soit la technique utilisée, du mapping de tokens, à la remontée à l’IdP, aux bridges ou au PDP/PEP) nous pose invariablement ces problèmes :

  • notre application devient dépendante de systèmes externes que l’on ne peut gérer nous-même
  • chaque décision de changement des autorisations devient politique, avec son lot de réunions, protocoles à suivre, donc de latence
  • les équipes qui gèrent l’IdP deviennent une dépendance et clairement, ça frotte très vite, car l’équipe n’arrive plus à tenir le rythme des équipes applicatives. Il arrive aussi souvent qu’une fois le projet IdP terminé et passé en mode maintenance, les ressources humaines sont désallouées et l’équipe IdP se retrouve en surcharge de demandes.
  • si on doit changer le modèle de sécurité de l’application c’est toute une architecture à reconsidérer

Dans notre cas, solution on-premises, avec un peu plus que RBAC, on ne peut pas présupposer de l’architecture, ni de l’environnement, ni de l’organisation d’entreprise dans lesquels notre application va tourner. Gérer les rôles via les tokens serait se tirer une balle dans le pied. Il nous faut d’autres ouvertures.

Stabilisation

Tout ceci était dit, on ne peut pas débattre indéfiniment sur ces questions. IdP ou pas, utilisateurs ou pas, rôles ici ou là, humains ou services ou agents.

Avant d’écrire la moindre ligne de code, il faut stabiliser nos besoins.

Voilà comment j’ai figé le problème (toujours dans le cadre Medatarun/on-premises).

Je veux que l’application fonctionne immédiatement sans IAM, sans demander d’autorisation IAM à personne, sans demander aux utilisateurs d’installer un autre logiciel ou de souscrire à un service externe. Je ne veux pas que ces choix deviennent un problème plus tard.

Je ne dois pas définir implicitement ce qu’est un utilisateur, car à chaque fois que cela a été fait, on se retrouve empêtré et déconnectés de la réalité.

Je ne veux pas de chemins différents pour les humains via l’IHM, API, CLI, agents via MCP. Je veux anticiper toute exception qui créerait des angles morts plus tard.

Je veux une gestion d’identité boring : juste un mécanisme stable et minimaliste pour identifier et gérer les acteurs. Rien d’autre.

Je veux que la gestion de l’autorisation reste où elle fait sens, dans l’application, parce que c’est du métier, pas de l’infrastructure. Les rôles seront rattachés aux actors. Pas plus.

On pose cela. Surtout, pas de compromis sur ces lignes directrices.

Maintenant que c’est clair, nos cerveaux arrêtent de ruminer. Cela devient “juste” un problème d’implémentation.

Et là, ça devient intéressant.

Une solution concrète sans compris

Quand on a exclu les modes “IdP-dependent” et “user-centric”, il ne nous reste pas grand-chose comme modèle.

Et si l’idée était de dire plutôt : IAM-agnostic et actor-centric ?

Le principe: on fait du OAuth et de l’OpenId Connect natif. Uniquement cela, rien d’autre.

Cela veut dire :

  • un filtre de sécurité unique JWT pour les appels d’API (ou CLI ou agents via MCP), qui n’accepte que des tokens JWT issus des fournisseurs (au pluriel) configurés. Aucun autre mode d’identification.
  • OpenId Connect exigé pour se connecter via l’IHM, dès le début, et rien que OpenId Connect.

Une base de données stocke les actors découverts ou créés (issuer + subject) ainsi que les rôles qui leur sont affectés. Des APIs sont disponibles pour gérer les actor (list, fetch, change roles, …).

Le filtre JWT synchronise les actors découverts dans les tokens et en profite pour synchroniser leur nom et leur email avec notre base actor, histoire que l’on puisse afficher leurs noms à l’écran.

Jusque-là c’est volontairement minimal.

Petite précision car la question revient: jamais l’application n’authentifie des actor. JAMAIS. Donc, si le fournisseur JWT ou l’IdP bloque une authentification pour quelqu’un ou quelque chose, l’actor que l’on a synchronisé devient inerte dans notre base. Ainsi, on préserve l’historique et le sens (qui a fait quoi avec des infos lisibles), juste, il ne peut plus s’authentifier.

Mais, on n’avait pas dit IAM-agnostic ? Où sont les utilisateurs en fin de compte ? Les voilà :

Techniquement, on a créé un module dédié: externe pour être sûrs que l’on n’a pas créé de dépendances non souhaitées vers ce module, mais livré quand même par défaut avec l’application.

Il contient :

  • une base de données utilisateurs, des APIs pour les créer et les gérer. Pas de rôles ici, juste le minimum vital pour démarrer quelque chose.
  • un fournisseur de tokens JWT qui se comporte exactement comme un fournisseur externe
  • et (ce n’est pas une blague), notre propre fournisseur OpenId Connect, qui se comporte exactement comme un OpenId Connect externe, avec sa propre page de login et compatible avec le protocole “token flow”

Là, on boucle notre contrat d’application: rien à installer, utilisable tout de suite. Ces composants sont le “JWT” et le “OpenId Connect” par défaut. Vous ne les utilisez pas si vous n’en voulez pas. Si vous configurez autre chose, vous n’en entendez plus parler.

Et c’est là tout le jeu:

Il n’y a pas de mode “avant” ou “après” IdP, juste un changement fichier de config qui dit qui envoie des tokens et où va pointer l’IHM.

D’un point de vue organisationnel: si une société veut synchroniser ses rôles depuis son IAM, elle peut. Si elle veut préremplir les actor, les désactiver, les faire tourner, les renommer, changer les issuers parce qu’on migre vers un nouvel IdP et on veut garder l’historique, ou faire le ménage, ou anonymiser, elle peut.

La complexité existe oui, mais elle est explicite, scriptée, api-sée mais surtout du côté de l’entreprise, pas cachée dans l’application, car c’est sa complexité, sa responsabilité et sa propre organisation.

C’est là que le module embarqué utilisateurs/OAuth/OIDC est une béquille, pas une fondation. Si on n’en veut pas, alors pas besoin de migration, de mapping ou d’adapter l’application. Il s’arrête juste d’être utilisé.

Vous allez peut-être me rétorquer: tu mets de l’IAM dans ton produit, même en béquille, tu n’es donc pas IAM-agnostic ?

Si, on est IAM-agnostic, y compris à nous-même.

Et ça, cela change tout.

Dernier point, nous n’avons pas encore parlé du démarrage du produit. On a dit que l’on ne voulait pas de chemin spécial pour les administrateurs. Donc le choix est le suivant. Quand l’application se lance la première fois, un secret est affiché dans les logs. Soit il est aléatoire, soit on peut le configurer par variables d’environnement. Son seul rôle est d’être utilisé, une fois et une seule (après il disparaît), pour créer un compte admin. Cet admin est alors un utilisateur du système embarqué qui se transforme immédiatement en tout premier actor de l’application, avec un rôle admin. Si plus tard, vous souhaitez le modifier pour n’utiliser que les admin de votre IdP, supprimez le et il n’y aura plus d’admin en base de données du tout.

Conclusion

Vous le voyez, il n’y a rien de magique ici. C’est une question de discipline et de décider où on pose les limites de ce que l’on fait ou pas dans l’application.

En refusant de faire beaucoup de choses, mais aussi en acceptant de faire ces composants embarqués de cette manière, on uniformise et on renforce les mécanismes de sécurité. Le tout est plus robuste, sans chemins différents, sans exceptions.

Le paradoxe, c’est qu’en faisant à proportion moins de choses que d’habitude (pour du on-premises, toujours), finalement, on rend le système à la fois utilisable pour des petites équipes, prêt pour des déploiements plus larges, et assez stable pour faire mieux que survivre à l’automatisation, aux agents, aux audits et au temps.

Quand on accepte de pas forcément tout faire, ou clairement pas les mêmes choses, tout en décidant à l’avance, le tout devient plus simple.

Ce qui est marquant aussi, ce sont les coûts.

Oui, c’est plus que l’IdP-dependent, pas de débat.

Comparé au user-centric, oui, sur la première itération, c’est plus long et plus compliqué (surtout OpenId Connect, pas trop le reste). Par contre, cela absorbe les coûts des itérations suivantes (API, SSO, migrations, etc.).

Au sujet de l’implémentation, vous serez surpris en regardant le code, que tout est fait sans framework, avec peu de code, très compact et autonome (en l’occurrence, c’est du Kotlin pur avec Ktor). L’idée n’est pas de dire qu’il ne faut utiliser de framework, mais que cela ne requiert pas une artillerie spécifique et que c’est à la portée de chacun, quel que soit l’environnement, sans appui externe obligatoire.

En dehors du coût et de la solidité ou de la simplification indue, on constate d’autres améliorations.

Par exemple, on peut valider dès le premier jour toute l’application sur des IdP réels (Google Workspace pour moi en dev, notre Keycloak en prod, base locale quelquefois pour d’autres usages).

On peut livrer sans se préoccuper trop de l’authentification (pour les tests par exemple) et ensuite passer à quelque chose de plus solide.

Enfin, lors des présentations, quand la question SSO se pose, on ne répond pas « on peut faire » mais « on l’a, maintenant ». Quand on nous dit « je n’ai pas d’IdP », on peut répondre « pas besoin, c’est autonome ».