TestOIDCBlazorWASM (épisode 3) : IAM côté serveur et gestion des autorisations

Objectif

Comme annoncé lors du précédent épisode (http://gouigoux.com/blog-fr/?p=3781), le présent article va se concentrer sur la mise en place des API et, pour continuer la phase d’authentification sur le client déjà réalisée, propager l’identification vers le serveur et mettre en place un système d’autorisations de type RBAC et en mode “défense en profondeur”.

Le principe du “Role-Based Access Control” est de donner des autorisations d’accès aux ressources en fonction du ou des rôles portés par un compte utilisateur ou par un groupe auxquels ce compte appartient. Ces rôles sont désignés par un identifiant et définissent des ensembles de droits sur des ressources (par exemple, lire des contrats, ou bien enregistrer des données de personnes, etc.). Dans l’épisode 1 de cette série d’articles (http://gouigoux.com/blog-fr/?p=3775), deux rôles ont été définis dans Keycloak, à savoir administrateur et lecteur. Comme leur nom l’indique, le premier a tous les droits et le second des droits sur les ressources, mais uniquement en consultation.

Par “défense en profondeur”, on entend qu’un seul rideau de gestion des autorisations ne suffit pas et que des strates successives de vérification seront mises en place pour améliorer la sécurité d’accès. Ainsi, les droits seront validés pour accéder à un menu, puis sur le routage correspondant, mais également au niveau du composant Blazor qui s’affiche, ainsi qu’au moment de l’appel sur l’API. Ainsi, si une mesure de sécurité au niveau du client est mise en défaut, une autre pourra implémenter le mécanisme d’autorisation. Et dans tous les cas, le serveur implémentera bien sûr son propre rideau, car les autorisations ne peuvent pas être confiées au client : lorsqu’il les traite, c’est uniquement pour faciliter le parcours d’interface utilisateur en masquant des menus ou composants sur lesquels l’utilisateur ne pourrait de toute façon pas réaliser une action.

Propagation de l’identification

Pour varier les plaisirs par rapport au client (et pour simplifier la lecture de cet épisode assez long), les paramètres du fournisseur d’identité ne seront pas rajoutés dans le fichier appsettings.json du projet TestOIDCBlazorWASM.Server mais en dur. Le code à rajouter dans Program.cs est donc le suivant (il faut le placer avant l’appel à la fonction Build() sur la variable builder) :

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o =>
{
     o.Authority = “http://localhost:8080/realms/Coucou/”;
     o.Audience = “account”;
});

Ce code permet de faire en sorte que le serveur utilise la même IAM que le client, en se basant pour récupérer l’identité en cours sur le JSON Web Token qui lui sera passé par l’intermédiaire du header HTTP Authorization, préfixé par le texte Bearer suivi d’un espace (comme cela est fait pour l’authentification de base, avec Basic suivi d’un espace). Pour qu’il compile, il est nécessaire de rajouter le package nommé Microsoft.AspNetCore.Authentication.JwtBearer et de positionner un using avec le même contenu en haut du code source.

Pour l’instant, nous ne rajoutons rien de plus à ce niveau du code. Attention toutefois à ne surtout pas se tromper sur l’orthographe de la valeur de l’audience, sous peine d’erreurs 401 incompréhensible, comme le rédacteur a pu s’en rendre compte…

Mise en place d’une API

Pour mettre en œuvre des autorisations, il faut des ressources à protéger : c’est ce que nous allons ensuite rajouter en mettant en place un contrôleur d’API qui exposera des données de personnes.

Pour cela, un contrôleur est ajouté sur le dossier Controllers du projet Server :

Le contrôleur sera de type API, et on démarre avec un contenu vide, et un nom de fichier (étape suivante de l’assistant) qui sera PersonnesController.cs :

L’implémentation dans un premier temps est quasi-vide, le but étant uniquement de tester dans cette étape du blog que les autorisations fonctionnent bien en lien avec l’identification du compte connecté :

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using TestOIDCBlazorWASM.Shared;


namespace TestOIDCBlazorWASM.Server.Controllers
{
     [Route(“api/[controller]”)]
     [ApiController]
     public class PersonnesController : ControllerBase
     {
         [HttpGet]
         public IActionResult LecturePersonnes(
             [FromQuery(Name = “$skip”)] int skip = 0,
             [FromQuery(Name = “$top”)] int top = 10
         )
         {
             return new OkObjectResult(new List<Personne>());
         }


        [HttpPost]
         public IActionResult CreationPersonne([FromBody] Personne personne)
         {
             throw new NotImplementedException(“Fonction de création de personne à venir”);
         }
     }
}

Pour que ce code compile, il faut ajouter une classe Personne, que nous mettrons dans le projet Shared (ne pas confondre avec le dossier du même nom dans le projet Client), et dont le contenu sera comme suit :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace TestOIDCBlazorWASM.Shared
{
     public class Personne
     {
         public string? ObjectId { get; set; }


        public string? Patronyme { get; set; }


        public string? Prenom { get; set; }


        public string? URLFiche { get; set; }
     }
}

Consommation sur le client

Pour vérifier le fonctionnement, une page doit être ajoutée sur le projet Client. Le fichier correspondant sera positionné dans le répertoire Pages :

Le nom du fichier sera Personnes.razor, car il affichera une liste de personnes, et son contenu en première intention sera comme suit :

@page “/personnes”
@using Microsoft.AspNetCore.Authorization
@using TestOIDCBlazorWASM.Shared
@inject IHttpClientFactory HttpFactory


<PageTitle>Personnes</PageTitle>


<AuthorizeView>
     <Authorized>
         @if (personnes == null)
         {
             <p><em>Loading…</em></p>
         }
         else
         {
             <table class=”table”>
                 <thead>
                     <tr>
                         <th>Prénom</th>
                         <th>Nom</th>
                         <th>Fiche</th>
                     </tr>
                 </thead>
                 <tbody>
                     @foreach (var p in personnes)
                     {
                         <tr>
                             <td>@p.Prenom</td>
                             <td>@p.Patronyme</td>
                             <td><a href=”@p.urlFiche” target=”_blank”>Fiche</a></td>
                         </tr>
                     }
                 </tbody>
             </table>
         }
     </Authorized>
     <NotAuthorized>
         <h1>Personnes</h1>
         <div>L’accès à cette page nécessite d’être authentifié</div>
     </NotAuthorized>
</AuthorizeView>


@code {
     private Personne[]? personnes;


    protected override async Task OnInitializedAsync()
     {
         var client = HttpFactory.CreateClient(“WebAPI”);
         personnes = await client.GetFromJsonAsync<Personne[]>(“api/personnes”);
     }
}

Il faudra rajouter une dépendance à la librairie Microsoft.Extensions.Http. pour que la référence à l’interface IHttpClientFactory puisse fonctionner. Ainsi, la fonction CreateClient en fin du code de la classe ci-dessous pourra fournir un client HTTP. La valeur du paramètre WebAPI doit se retrouver sur le code qui va être rajouté dans Program.cs, à savoir :

builder.Services
     .AddHttpClient(“WebAPI”, client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
     .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

Ce code remplace la ligne suivante, qui injecte un HttpClient pour tous les usages, alors que nous souhaitons avoir une instance pour les appels autorisés et une autre pour les appels hors connexion :

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

Fonctionnement des autorisations sur le client

La balise <AuthorizeView> utilisée plus haut permet de faire en sorte que seuls des accès authentifiés sont réalisés sur la liste des personnes. Ainsi, si on lance l’application et qu’on navigue sur /personnes, l’affichage est le suivant :

Alors qu’une fois authentifié, l’accès est autorisé (même si l’API ne renvoie pour l’instant qu’une liste de personnes vide) :

Dans le premier cas, il y avait en plus une erreur dans l’exécution de l’application côté client :

Ceci est lié à l’implémentation très basique de l’appel à l’API, qui se réalise systématiquement à l’appel de la page, et sans gestion d’erreur pour l’instant. Mais nous allons remédier (partiellement) à ceci avec une approche un peu plus élégante.

Pré-autorisation côté client

En fait, ce que nous faisons sur le client n’est pas réellement de l’autorisation, car tant que le serveur n’est pas protégé (ce qui sera fait un peu plus loin), la donnée peut être accédée par un appel direct à l’API. Tout ce que nous faisons sur le client n’est qu’une sorte de pré-autorisation, bref une amélioration d’ergonomie pour ne pas rendre visibles des moyens d’accéder à la donnée. Il ne faut toutefois pas négliger cet aspect, pour l’utilisabilité de l’application avant tout, mais aussi parce que cela constitue toujours un rideau de défensif supplémentaire (aisément contournable par des sachants, mais tous les utilisateurs ne le sont pas).

Nous allons justement multiplier ces premiers rideaux de défense comme il se doit, en ajoutant au passage un menu pour accéder à cette page que nous avons pour l’instant appelé manuellement.

L’ajout d’un menu se fait en rajoutant le code suivant dans Shared/NavMenu.razor :

<div class=”nav-item px-3″>
     <NavLink class=”nav-link” href=”personnes”>
         <span class=”oi oi-list-rich” aria-hidden=”true”></span> Personnes
     </NavLink>
</div>

Mais pour montrer un premier rideau de blocage (absence du menu si pas d’authentification), nous allons l’entourer des mêmes balises que dans Personnes.razor. Le code est ainsi comme suit (extrait de la classe complète) :

<div class=”@NavMenuCssClass” @onclick=”ToggleNavMenu”>
     <nav class=”flex-column”>
         <div class=”nav-item px-3″>
             <NavLink class=”nav-link” href=”” Match=”NavLinkMatch.All”>
                 <span class=”oi oi-home” aria-hidden=”true”></span> Home
             </NavLink>
         </div>
         <div class=”nav-item px-3″>
             <NavLink class=”nav-link” href=”counter”>
                 <span class=”oi oi-plus” aria-hidden=”true”></span> Counter
             </NavLink>
         </div>
         <div class=”nav-item px-3″>
             <NavLink class=”nav-link” href=”fetchdata”>
                 <span class=”oi oi-list-rich” aria-hidden=”true”></span> Fetch data
             </NavLink>
         </div>
         <AuthorizeView>
             <Authorized>

                 <div class=”nav-item px-3″>
                     <NavLink class=”nav-link” href=”personnes”>
                         <span class=”oi oi-list-rich” aria-hidden=”true”></span> Personnes
                     </NavLink>
                 </div>
             </Authorized>
          </AuthorizeView>

     </nav>
</div>

Pour aller jusqu’au bout des rideaux de défense, la page Personnes.razor va également être protégée du point de vue du routage. Ainsi, même si quelqu’un ne voyant pas le menu continue à saisir l’URL, c’est l’accès à l’ensemble de la page qui sera interdit (et pas seulement le contenu différencié comme réalisé plus haut). Pour cela, en haut de ce fichier, nous ajoutons l’instruction ci-dessus :

@page “/personnes”

@attribute [Authorize]

Ces manipulations ont pour effet que le menu n’apparaît que lorsqu’on est connecté :

Et si un utilisateur tape directement dans l’URL l’adresse correspondante à la page, c’est désormais le mécanisme d’authentification de .NET qui prend la main et réagit de manière plus appropriée, en redirigeant vers l’IAM :

Application des autorisations sur le contrôleur

La partie client est réalisée, mais il nous reste tout de même à gérer l’essentiel, à savoir la sécurité des données elles-mêmes, en protégeant le contrôleur. En effet, pour l’instant, si on appelle directement l’API, le retour est visible (bien qu’il s’agisse pour l’instant d’une liste vide, car nous n’avons rien branché derrière le contrôleur) :

Pour sécuriser le contrôleur, rien de plus simple ; il suffit d’ajouter une balise [Authorize] comme sur le client pour signifier que cet ensemble de méthodes ne peut être accédé qu’avec un compte identifié :

[Authorize]
[Route(“api/[controller]”)]
[ApiController]
public class PersonnesController : ControllerBase

Le résultat est une interdiction, mais pas tellement comme attendu, car nous n’avons pas encore appliqué les mécanismes d’autorisation sur le projet lui-même :

Pour que le mécanisme fonctionne, il faut également indiquer dans le fichier Program.cs les éléments de code suivants pour activer l’authentification et les mécanismes d’autorisation :

app.UseAuthentication();
app.UseAuthorization();

A noter qu’il serait possible de protéger les méthodes une par une au lieu d’appliquer l’attribut à l’ensemble du contrôleur. Nous allons justement utiliser ce mode de fonctionnement un peu plus loin, lorsque nous ajusterons les droits en fonction des rôles de la personne connectée.

Gestion des rôles

Mais dans un premier temps, il est nécessaire de nous assurer que nous disposons bien de ces rôles. Ceci peut être vérifié en récupérant les tokens d’authentification :

En les mettant dans un décodeur comme https://jwt.io, on retrouve bien les claims pour les rôles tels qu’ils avaient été définis lorsque l’IAM a été mise en place dans un épisode principal de cette série (et en particulier pour notre application, dont le ClientID est TestOIDCBlazorWASM) :

Pour indiquer au projet Client où sont les claims pour les rôles, la ligne suivante doit être ajoutée dans l’appel à la fonction AddOidcAuthentication du fichier Program.cs :

options.UserOptions.RoleClaim = “resource_access.TestOIDCBlazorWASM.roles”;

Un peu plus bas, la ligne suivante permet de brancher le mécanisme d’autorisation sur ces claims :

builder.Services.AddApiAuthorization().AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();

Pour que cette ligne compile, la classe RolesClaimsPrincipalFactory doit être ajoutée dans le projet (un Principal est une structure .NET qui porte une identité), avec le contenu suivant :

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using System.Security.Claims;
using System.Text.Json;


namespace TestOIDCBlazorWASM.Client
{
     public class RolesClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
     {
         public RolesClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
         {
         }


        public override async ValueTask<ClaimsPrincipal> CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options)
         {
             var user = await base.CreateUserAsync(account, options);
             if (user?.Identity != null && user.Identity.IsAuthenticated)
             {
                 var identity = (ClaimsIdentity)user.Identity;
                 var resourceaccess = identity.FindAll(“resource_access”);
                 string Contenu = resourceaccess.First().Value;


                JsonElement elem = JsonDocument.Parse(Contenu).RootElement;
                 if (elem.ValueKind == JsonValueKind.Array)
                 {
                     foreach (JsonElement e in elem.EnumerateArray())
                         if (e.TryGetProperty(“TestOIDCBlazorWASM”, out var prop))
                         {
                             foreach (JsonElement role in e.GetProperty(“TestOIDCBlazorWASM”).GetProperty(“roles”).EnumerateArray())
                                 identity.AddClaim(new Claim(options.RoleClaim, role.GetString() ?? String.Empty));
                             break;
                         }
                 }
             }
             return user;
         }
     }
}

A noter que cette classe est une version très simplifiée, sans logs ni gestion d’erreur, et avec toutes les valeurs passées en dur pour faciliter la compréhension. Pour avoir une version un peu plus propre, le lecteur est invité à se rendre sur le dépôt Github https://github.com/jp-gouigoux/TestOIDCBlazorWASM.

Exploitation des rôles côté client

Cette classe se chargeant d’incorporer les rôles dans le Principal à partir des claims, nous pouvons maintenant nous servir des rôles sur le client pour, par exemple, autoriser l’accès à la liste des personnes uniquement si le rôle lecteur est présent. Comme nous avons cassé le fonctionnement de la page Fetch data lorsque nous avons modifié le comportement de l’injection par défaut d’un HttpClient (en ne créant qu’une instance pour les besoins connectés), nous en profiterons pour supprimer ce menu qui ne sert plus. De plus, pour montrer la différence entre la gestion connectée et les rôles, nous masquerons le menu pour la page Counter si la personne n’est pas authentifiée ; seul le menu Home sera accessible en cas d’absence de connexion, ce qui est plus conforme au principe de moindre privilège.

Le code correspondant au menu est le suivant :

<div class=”@NavMenuCssClass” @onclick=”ToggleNavMenu”>
     <nav class=”flex-column”>
         <div class=”nav-item px-3″>
             <NavLink class=”nav-link” href=”” Match=”NavLinkMatch.All”>
                 <span class=”oi oi-home” aria-hidden=”true”></span> Home
             </NavLink>
         </div>
        <AuthorizeView>
             <Authorized>

                 <div class=”nav-item px-3″>
                     <NavLink class=”nav-link” href=”counter”>
                         <span class=”oi oi-plus” aria-hidden=”true”></span> Counter
                     </NavLink>
                 </div>
                 @if (context.User.IsInRole(“lecteur”))
                 {
                     <div class=”nav-item px-3″>
                         <NavLink class=”nav-link” href=”personnes”>
                             <span class=”oi oi-list-rich” aria-hidden=”true”></span> Personnes
                         </NavLink>
                     </div>
                 }
             </Authorized>
          </AuthorizeView>

     </nav>
</div>

Logiquement, lors d’un accès non authentifié, le menu est alors réduit à sa plus simple expression :

La connexion avec un compte autorisé permet de voir tous les menus restants :

Et si nous créons dans Keycloak un compte sans aucun rôle, le comportement est le suivant :

Bien sûr, il est également possible de jouer avec les rôles d’un seul utilisateur, en les modifiant dans Keycloak, mais il faudra pour chaque test se déconnecter puis reconnecter :

Toujours sur le client, on peut également modifier l’entête du fichier Personnes.razor pour mettre en œuvre le Role Based Access Control :

@page “/personnes”

@attribute [Authorize(Roles = “lecteur”)]

Exploitation des rôles côté serveur

Le même principe s’applique côté serveur, mais afin de montrer la richesse du mécanisme, nous allons cette fois mettre en place des Policies, qui sont des règles d’autorisations. Ce mécanisme, bien qu’utilisé de manière très simple ci-dessous (lien un pour un avec un rôle) permet de créer des politiques de sécurité très complexes si nécessaire.

Pour cela, nous allons modifier le fichier Program.cs du projet Server comme suit :

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o =>
{
     IConfigurationSection ConfigOIDC = builder.Configuration.GetSection(“OIDC”);
     o.Authority = ConfigOIDC[“Authority”];
     o.Audience = ConfigOIDC[“Audience”];
     o.TokenValidationParameters.RoleClaimType = “resource_access.TestOIDCBlazorWASM.roles”;
});


builder.Services.AddTransient<IClaimsTransformation, ClaimsTransformer>();


builder.Services.AddAuthorization(o =>
{
     o.AddPolicy(“Administration”, policy => policy.RequireClaim(“user_roles”, “administrateur”));
     o.AddPolicy(“Lecture”, policy => policy.RequireClaim(“user_roles”, “lecteur”));
});

Ce qui nous intéresse particulièrement est la génération de deux Policies, étant entendu qu’elles reprennent ici simplement les codes des rôles. Encore une fois, il s’agit d’un exemple simpliste pour montrer le principe puisque, si nous avions besoin uniquement des rôles, nous pourrions utiliser la même grammaire que plus haut. Mais on voit bien que, comme la façon d’écrire la Policy est avec une lambda, il est possible de combiner toutes les règles imaginables. Les libellés sont toutefois légèrement différents, pour que le lecteur fasse bien la différence par la suite, lorsque les Policies seront utilisées.

Pour que le code compile, il faudra la rajouter la classe suivante, qui se charge de la deuxième fonctionnalité du code présenté, à savoir l’extraction des rôles depuis les claims transmis dans le token :

using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
using System.Text.Json;


namespace TestOIDCBlazorWASM.Server
{
     public class ClaimsTransformer : IClaimsTransformation
     {
         public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
         {
             ClaimsIdentity claimsIdentity = (ClaimsIdentity)principal.Identity;
             if (claimsIdentity.IsAuthenticated)
             {
                 foreach (var c in claimsIdentity.Clone().FindAll((claim) => claim.Type == “resource_access”))
                 {
                     JsonDocument doc = JsonDocument.Parse(c.Value);
                     foreach (JsonElement elem in doc.RootElement.GetProperty(“TestOIDCBlazorWASM”).GetProperty(“roles”).EnumerateArray())
                         claimsIdentity.AddClaim(new Claim(“user_roles”, elem.GetString() ?? String.Empty));
                 }
             }
             return Task.FromResult(principal);
         }
     }
}

Enfin, l’utilisation des Policies est globalement la même que pour les rôles, seule la grammaire changeant. Dans l’application ci-dessous, le contrôleur est déclaré accessible sur le GET si la politique Lecture est satisfaite, et sur le POST si la politique Administration est satisfaite :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using TestOIDCBlazorWASM.Shared;


namespace TestOIDCBlazorWASM.Server.Controllers
{
     [Authorize]
     [Route(“api/[controller]”)]
     [ApiController]
     public class PersonnesController : ControllerBase
     {
         [Authorize(Policy = “Lecture”)]
         [HttpGet]
         public IActionResult LecturePersonnes(
             [FromQuery(Name = “$skip”)] int skip = 0,
             [FromQuery(Name = “$top”)] int top = 10
         )
         {
             return new OkObjectResult(new List<Personne>());
         }


        [Authorize(Policy = “Administration”)]
         [HttpPost]
         public IActionResult CreationPersonne([FromBody] Personne personne)
         {
             throw new NotImplementedException(“Fonction de création de personne à venir”);
         }
     }
}

Lors du lancement, l’erreur suivante apparaît :

Comme nous ne souhaitons pas rentrer dans la complexité d’une exposition HTTPS de Keycloak (pour l’instant, en tout cas), nous suivons le contournement proposé et rajoutons le code suivant dans le code du serveur :

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o =>
{
     IConfigurationSection ConfigOIDC = builder.Configuration.GetSection(“OIDC”);
     o.Authority = ConfigOIDC[“Authority”];
     o.Audience = ConfigOIDC[“Audience”];
     o.TokenValidationParameters.RoleClaimType = “resource_access.TestOIDCBlazorWASM.roles”;
     o.RequireHttpsMetadata = false;
});

Le lancement est ensuite conforme à celui attendu :

Conclusion

Au prochain épisode, branchement sur la base de données et ajout d’une page pour créer des personnes, de façon à boucler la partie de base de l’application avec son IAM associée.

Vue la complexité pour écrire le présent article avec Open LiveWriter qui date de plus de 10 ans et le résultat moche pour le code source, je pense qu’il est temps de basculer vers un autre système de bloc, avec peut-être du Markdown pour générer un blog statique… A suivre sur le prochain article de cette série, que j’espère écrire plus vite que celui-ci, longtemps remis à plus tard.

Posted in C# | Tagged , , | Leave a comment

Chute des monnaies électroniques : ça démarre pour de bon

Après de sérieuses alertes sur la volatilité des cours dans les dernières années, il semblerait qu’on passe un pas de plus dans la dégradation des cryptomonnaies, avec le dépôt de bilan de FTX (source), la deuxième plus grosse plateforme du monde. Après le même évènement pour TerraUSD et de nombreuses sociétés associées, ça commence à faire désordre…

Mais est-ce vraiment étonnant quand on sait que ces outils sont apparus suite au surplus de liquidités injectées sur les marchés lors de la crise de 2008, et qu’ils sont en fait des outils de spéculation ? Malgré tout ce qu’on nous a vendu sur le caractère émancipateur par rapport aux monnaies d’état et sur la capacité d’échange entre individus sans passer par des tiers de confiance, la réalité des cryptomonnaies commence à être de plus en plus visible.

Un signe avant-coureur de l’effondrement à venir ? Les pages de publicité il y a quelques mois dans certains magazines :

20220728_182735

Quand une monnaie soit-disant libre en est réduite à faire de la publicité pour récupérer des liquidités (comprendre : attirer encore plus de gens et leur argent dans le système pour essayer de compenser l’effondrement des cours), on peut se dire que ça sent le sapin.

Personnellement, je n’ai jamais eu de cryptomonnaie, alors que je connais le principe de la blockchain depuis le tout début. Plein de connaissances me disent que j’aurais dû en acheter à l’époque, que ça aurait été une super affaire. Mais outre que je suis un vrai chat noir, je suis bien content de ne pas avoir à me poser la question de quand tout cela va péter à la figure des pauvres propriétaires !

Rendez-vous dans 5 ans pour savoir si je me suis trompé, mais je parie sur un effondrement complet des cryptomonnaies existantes.

PS : un excellent article du Monde ce jour, qui explique toutes les raisons de l’écroulement de FTX.

Posted in Uncategorized | Leave a comment

Pourquoi utiliser OIDC plutôt que SAML ?

Pourquoi cet article ?

La question me revient souvent en ce moment, de la part de mes étudiants, de collègues, de clients et lors d’échange avec mes pairs CTO / RSSI, et en particulier lorsque je fais des démos de l’intermédiation d’authentification avec Apache Keycloak (en utilisant une application cliente utilisant le protocole OpenID Connect). Je me suis dit que le plus simple, au lieu de continuer à répondre par oral, par mail ou dans des documents, serait de regrouper toutes les explications, normatives comme techniques, dans un seul article en ligne…

Bien définir la question

Ne vous attendez pas dans cet article à des déclarations à l’emporte-pièce sur le fait que tel ou tel protocole est obsolète ou que “SAML is dead” (référence). Que les choses soient claires dès le début : la question est juste de savoir pourquoi il vaut mieux privilégier OIDC à SAML pour de nouveaux déploiements. Si vous utilisez déjà du SAML, le protocole a beau être plus ancien, il ne possède pas de faiblesse qui nécessite son remplacement et l’IAM est un sujet trop important pour changer de mécanisme sans une excellente raison.

La question à laquelle cet article répond est pourquoi utiliser OIDC plutôt que SAML si vous avez le choix, à savoir si vous n’êtes pas déjà équipé / si vous n’avez pas des conditions particulières qui vous forcent à prendre un des choix / etc. Nous expliquerons d’ailleurs brièvement ces cas particuliers qui pourraient vous amener à un choix plutôt qu’un autre.

Pas qu’une question de modernité

Comme rapidement expliqué, SAML est un protocole plus ancien qu’OIDC. Sa nouvelle version 2.0 a permis de dépasser les limites de la première version, mais elle date tout de même de 2005. Par comparaison, OAuth 2.0, qui est la brique de base d’OIDC, date de 2012. Une petite dizaine d’années en informatique, c’est un monde et les usages ont particulièrement évolué lors de ces sept ans, avec une démultiplication sans précédent des applications informatiques et en particulier web.

Mais dans des domaines aussi importants que l’Identity and Authorization Management, les principes et les implémentations évoluent finalement peu vite donc l’âge d’un protocole n’est pas vraiment un sujet en soi. Et les fonctionnalités elles-mêmes sont assez proches (référence). Voyons donc plutôt les autres critères.

Ouverture aux standards

SAML a été une des premières approches standardisées, par opposition aux mécanismes propriétaires de SSO. Mais OIDC est lui aussi standardisé, et présente l’avantage de s’appuyer sur de nombreux autres protocoles eux-mêmes normalisés.

Un des points très importants dans la capacité des SI à évoluer (l’urbanisation) est la séparation correcte des responsabilités, qui permet l’alignement business / IT. OIDC porte ce principe plus haut que SAML dans le sens où plusieurs normes et protocoles sont mis en œuvre pour les différentes fonctionnalités nécessaires au fonctionnement d’ensemble. OAuth est utilisé pour la gestion d’autorisation d’accès à une ressource, JWT est utilisé pour la normalisation du transport des données, RSA pour la signature, etc. A l’inverse, SAML définit en une seule norme toutes ces fonctionnalités, ce qui ne le rend pas plus simple, mais plus rigide, comme cela peut être constaté sur le faible nombre d’évolutions du standard.

Les standards se mélangent, toutefois, et on peut par exemple utiliser du OAuth avec des tokens SAML, même si, globalement, ceci complexifie plus les interactions dans un SI qu’autre chose. Au final, l’approche modulaire et standardisée à la fois d’OIDC est clairement un plus du protocole par rapport à SAML, qui accuse son âge sur ce point précis.

Adoption

Relative égalité sur ce point, OIDC étant sur une dynamique plus forte, mais SAML ayant une base installée encore très large. Une des raisons pour ceci, d’ailleurs, est simplement que le temps de déploiement d’une architecture d’authentification est très long et qu’une fois qu’une organisation a investi dedans, il ne suffit pas que le prochain protocole soit meilleur, il faut qu’il justifie un second investissement du même type. Toute entreprise ayant déployé un SSO avec tous les efforts que cela signifie n’a en général pas envie de recommencer l’année suivante.

Ainsi, SAML ayant commencé sa carrière plus tôt, il est logique que son parc installé soit étendu. Il est même notable que l’adoption augmente encore (référence), mais cela ne veut pas dire grand-chose dans notre comparatif, puisque ceci est complètement général à toutes les solutions d’IAM. Et ce fort heureusement car le taux d’équipement pour l’instant est dangereusement faible, avec toutes les problématiques de sécurité associées. La forte implantation d’Azure AD par le biais de la diffusion massive d’Office 365 dans les entreprises a beaucoup fait réduire ces risques. Azure AD a d’ailleurs le bon goût de proposer des endpoints SAML comme OIDC.

Fonctionnalités

SAML est particulièrement adapté à une gestion d’identité en mode “fermé”, c’est-à-dire dans le cadre interne de grandes entreprises, de réseaux d’universités, etc. A l’inverse, OIDC est pleinement adapté et pensé pour une utilisation sur des réseaux ouverts et bien sûr internet. Comme la bascule est de plus en plus forte sur le deuxième cas, OIDC devient le choix par défaut, internet intervenant maintenant dans presque tous les processus métier collaboratifs. OIDC va être plus adapté aux nouveaux usages comme les Single Page Applications, la mobilité, les applications natives hors navigateur, etc. (référence).

C’est peut-être sur ce point que la différence est la plus forte entre les deux produits et qui peut forcer un choix technologique. Toute entreprise qui a l’intention d’ouvrir, d’une manière ou d’un autre, sa gestion d’identité sur internet (que ce soit en accueillant des comptes B2C, en autorisant une authentification déléguée sur des IDP grand public, etc.) aura fortement intérêt à partir sur OIDC.

Le mécanisme d’anti-rejeu fourni par OIDC n’est pas implémenté en SAML. De plus, le fait qu’OIDC permettent de déléguer l’authentification à un autre IDP fait que le vol de crédentiels est plus complexes puisque dans ce cas, ils ne sont même pas connus du relais d’authentification lui-même. Enfin, OIDC permet, lors du protocole, de négocier les claims de manière beaucoup plus fine que SAML, même si les IDP pourraient encore améliorer ceci et permettre, par exemple, de refuser d’envoyer certaines informations demandées sans annuler complètement le mécanisme d’authentification. Le fait que l’identification soit incomplète par rapport aux claims demandés par le client devrait théoriquement relever de la responsabilité de ce dernier, quitte à ce qu’il puisse en retour refuser la connexion s’il considère que ce refus de livraison de claims est bloquant.

Mise en œuvre

De ce point de vue, les fonctionnalités standards de l’IAM (authentification, identification, production d’attributs dédiés à l’autorisation, voire même Policy Information Point, gestion de crédentiels et supports d’authentification, etc.) sont généralement offertes par OIDC de manière moins complexe que par SAML. Ce dernier est en effet plus verbeux, et pas seulement parce qu’il utilise du XML (un peu comme SOAP quand on le compare aux approches REST/JSON, sachant qu’on peut tout à fait faire du REST/XML). Par exemple, le chiffrement est intégré dans SAML, alors qu’OIDC se débarrasse complètement du sujet en le laissant à la couche HTTPS. Encore une fois, ceci participe d’un meilleur partage des responsabilités, caractéristique clé des architectures évolutives.

Ainsi, intégrer des clés FIDO ou Yubikey est un jeu d’enfant (bon, d’accord, un jeu d’adolescent bien motivé) avec OIDC. Et la simple mise en place d’un Shibboleth est bien plus complexe que de lancer un Keycloak. D’un côté, plusieurs heures au minimum et des documentations assez complexes à lire pour mettre en place le système de clés ; de l’autre, une simple commande Docker pour lancer l’image d’un Keycloak résolument plus moderne, et une interface graphique web pour paramétrer l’ensemble, plutôt que des fichiers de configuration.

En conclusion

SAML est loin d’être obsolète et va continuer à se déployer. Toutefois, il reste une solution legacy par rapport à OIDC. Si vous avez mis en place un SSO SAML, il n’y a aucune raison pour en changer. Si toutefois vous démarrez un projet d’IAM et que vous n’êtes pas encore équipé, à moins que certaines fonctionnalités spécifiques à SAML soient essentielles pour vous, le choix le plus couvrant et pérenne est OIDC. Il s’agit du protocole qui vous ouvre le plus de choix, de manière aussi sécurisée et avec un bien meilleur support des standards, donc plus de pérennité pour votre investissement.

Et pour aller plus loin

Mais le mieux est encore de ne pas avoir à choisir. Pour cela, intégrer un module d’IAM dans votre SI est l’approche la plus souple. En pratique, une solution comme Keycloak vous donnera la capacité d’intermédiation et vous permettra de connecter une application avec n’importe quel fournisseur d’authentification, qu’il soit compatible OIDC, SAML ou toute autre norme, voire même avec des protocoles complètement propriétaires (même si ceci nécessite de coder un petit JAR pour la médiation). Et si vous choisissez un annuaire, rappelez-vous que, pour peu que vous utilisiez Office 365 comme de nombreuses entreprises, vous bénéficiez déjà d’un annuaire en ligne compatible avec les deux normes du marché, ce qui vous ouvre à quasiment tous les usages.

Posted in Sécurité | Tagged | Leave a comment

TestOIDCBlazorWASM (épisode 2) : branchement de l’application cliente sur l’IAM

Objectif

Le but de cet article est de montrer comment l’IAM préparée sur le précédent article (http://gouigoux.com/blog-fr/?p=3775) va être utilisée par l’application cliente pour son authentification / identification. Dans cet article, nous n’utiliserons pas encore l’identification pour réaliser des autorisations dans le client, et nous ne propagerons pas l’authentification sur le serveur. Ces deux sujets seront traités dans d’autres articles à venir. Pour l’instant, le but est juste de montrer comment authentifier proprement un client Blazor sur une source OpenID Connect.

Vous êtes certainement déjà tombés sur des articles comme https://betterprogramming.pub/stop-using-json-web-tokens-for-authentication-use-stateful-sessions-instead-c0a803931a5d, mettant en garde sur la mauvaise utilisation des tokens JWT, et il faut effectivement faire attention à ne pas créer des failles de sécurité. C’est pour cette raison que, dans l’application exemple, nous collerons au maximum à l’utilisation la plus standard possible des librairies .NET. C’est ce qui nous permettra en particulier d’éviter le maximum de pièges de sécurité, car les implémentations fournies sont sécurisées par défaut. Par exemple, le type de flux utilisé sera le mode “code grant” d’OpenID Connect, qui est plus sécurisé que le mode “implicit grant”, désormais abandonné par la vesion OAuth 2.1. Et pour ce qui est des tokens, l’implémentation .NET par défaut gère correctement les id token, access token et refresh token, ce qui donne également une bonne assurance d’évolutivité, par exemple vers la possibilité de révocation d’une session authentifiée.

Création du squelette d’application standard

Toujours pour des raisons de standardisation maximum, nous partirons du modèle Application WebAssembly Blazor de Visual Studio (version 2022 utilisée pour les articles) :

image

Si vous souhaitez suivre directement les exercices, vous pouvez nommer l’application TestOIDCBlazorWASM, de façon à pouvoir copier directement les ensembles de code faisant référence aux namespaces, sans avoir à les modifier :

image

Afin de montrer précisément tous les ajouts nécessaires à la bonne gestion de l’authentification et d’expliquer point par point toutes les classes et fonctionnalités pour ce faire, nous partirons, même si cela peut paraître plus de travail, d’une application sans gestion de l’authentification. Et comme le but est d’avoir une application la plus réaliste possible, nous utiliserons le mode HTTPS :

Ajout des composants Blazor à utiliser pour l’authentification

La première étape est d’ajouter le package nécessaire à la gestion de l’authentification dans une application Blazor WASM, à savoir Microsoft.AspNetCore.Components.WebAssembly.Authentication. Pour cela, on va dans la gestion des packages du projet Client :

Et on installe cette librairie qui, au moment de l’écriture de cet article, était en 6.0.8 :

Ensuite, nous allons créer trois composants Razor, dont le code est récupéré sur les templates avec authentification proposés par Microsoft, mais que nous reprendrons ici manuellement :

Le premier sera placé dans Pages, s’appellera Authentication.razor et contiendra le code suivant, nécessaire pour la gestion des routes d’authentification :

@page “/authentication/{action}”
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action=”@Action” />


@code{
     [Parameter] public string? Action { get; set; }
}

Attention à ne pas modifier la route exposée, en tout cas sans ajuster ce qui avait été fait dans le précédent article sur le paramétrage de l’IAM Keycloak, car nous avions déclaré les sous-routes login-callback et logout-callback dans /authentication comme les URLs valides pour l’IAM, et il faut que la correspondance soit gardée sinon l’IAM refusera ces adresses en redirection.

Le second composant sera placé dans Shared et nommé LoginDisplay.razor, avec ce contenu :

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication


@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager


<AuthorizeView>
     <Authorized>
         <a href=”authentication/profile”>Hello, @context.User.Identity?.Name !</a>
         <button class=”nav-link btn btn-link” @onclick=”BeginSignOut”>Log out</button>
     </Authorized>
     <NotAuthorized>
         <a href=”authentication/register”>Register</a>
         <a href=”authentication/login”>Log in</a>
     </NotAuthorized>
</AuthorizeView>


@code{
     private async Task BeginSignOut(MouseEventArgs args)
     {
         await SignOutManager.SetSignOutState();
         Navigation.NavigateTo(“authentication/logout”);
     }
}

Ce composant est celui qui gère l’affichage des commandes d’authentification. Tant que l’utilisateur n’est pas identifié, il affiche le lien pour s’enregistrer comme utilisateur (attention, ça ne veut pas dire que l’IAM supporte cette fonctionnalité, et si ce n’est pas le cas, on aura une information en ce sens) et le lien pour se connecter. Dans le cas inverse, l’affichage montrera le contenu du nom fourni par l’instance d’identité générée par .NET à partir des claims d’identification reçus par le ticket JWT, ainsi que le lien pour se déconnecter (d’où l’importance, dans l’article précédent, de bien déclarer aussi l’URL en logout-callback comme valide).

Le troisième composant est également dans Shared, et s’appelle RedirectToLogin.razor. Il contient le code ci-dessous :

@inject NavigationManager Navigation
 
@code {
     protected override void OnInitialized()
     {
         Navigation.NavigateTo($”authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}”);
     }
}

Comme le nom l’indique, ce composant se charge uniquement de la redirection vers la page de login, en passant en paramètre l’URL sur laquelle rediriger l’utilisateur si la connexion a bien été réalisée (en général, la page d’où l’utilisateur vient, et qui l’a renvoyé sur la page de login comme elle n’a pas détectée d’authentification valide).

Paramétrage de l’authentification

Les composants graphiques étant prêts, nous allons passer à la mise en place de la mécanique d’authentification qui va les mettre en œuvre. Dans un premier temps, il faut rajouter un “Fichier de paramètres d’application” dans le projet Client. Pour les applications Blazor, comme ce fichier doit être lu lors de l’exécution, et donc depuis le navigateur, il doit être ajouté dans wwwroot.

La section ci-dessous est ensuite rajoutée dans le fichier :

“OIDC”: {
  “Authority”: “
http://localhost:8080/realms/Coucou/“,
  “ClientId”: “TestOIDCBlazorWASM”,
  “ResponseType”: “code”
}

Attention à ne pas se tromper sur le protocole ni le port d’exposition de Keycloak, et si le conteneur a été arrêté depuis que vous l’avez déployé lors de l’article précédent, il faudra penser à faire un docker start iam. De même, si vous avez nommé le realm autrement que Coucou et le client d’authentification autrement que TestOIDCBlazorWASM, il faut bien sûr ajuster. Noter que le fait que ce dernier soit exactement le nom de l’application est une pure convention pour s’y retrouver, mais rien ne le force. Il est simplement assez logique de les aligner, car le client au sens OIDC est une application appelée à consommer les services d’authentification, et ce sera dans notre cas l’application que nous sommes en train d’initialiser.

Dans le fichier index.html, la ligne ci-dessous est à rajouter juste avant l’inclusion du script blazor.webassembly.js, en toute fin du body (c’est une bonne pratique de toujours inclure les scripts en fin de page) :

<script src=”_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js”></script>

Cette ligne de code permet d’ajouter au client toutes les fonctionnalités Javascript nécessaires pour que l’authentification se déroule correctement (en Blazor WebAssembly, tout n’est pas fait en .NET, et il reste encore des fonctionnalités réalisées en Javascript).

Dans le fichier _Imports.razor, il faut rajouter le using ci-dessous pour que tout compile bien :

@using Microsoft.AspNetCore.Components.Authorization

Le fichier App.razor va nécessiter un peu plus de travail. Déjà, l’ensemble du code existant doit être entouré dans des balises <CascadingAuthenticationState>, ce qui permettra de pousser automatiquement les données d’authentification dans tous les composants à l’intérieur de l’application. Ensuite, la ligne <RouteView…> sera remplacée par le code suivant, qui nous permettra de gérer la redirection automatique (ou bien l’affichage d’un message de blocage) lorsqu’une ressource n’est désormais pas accessible :

<AuthorizeRouteView RouteData=”@routeData” DefaultLayout=”@typeof(MainLayout)”>
     <NotAuthorized>
         @if (context.User.Identity?.IsAuthenticated != true)
         {
             <RedirectToLogin />
         }
         else
         {
             <p role=”alert”>You are not authorized to access this resource.</p>
         }
     </NotAuthorized>
</AuthorizeRouteView>

C’est ce code qui appellera le RedirectToLogin créé plus haut. Le code d’App.razor doit être comme suit :

Il est également nécessaire pour pouvoir se connecter d’incorporer l’appel au composant LoginDisplay créé plus haut dans la barre de menu. On intervient donc dans MainLayout.razor pour ajouter la ligne sélectionnée dans la capture ci-dessous :

Le tout dernier ajout de code dans le client se trouve dans Program.cs et il s’agit du code qui récupère les settings du fichier de paramétrage pour déclencher la prise en compte de l’authentification OIDC par les composants .NET :

builder.Services.AddOidcAuthentication(options => {
     builder.Configuration.Bind(“OIDC”, options.ProviderOptions);
});

Le code doit être placé avant l’instruction .Build() existante dans le code généré par le template Microsoft. Si vous avez modifié le nom de la section dans le fichier de settings créé plus haut, il faut bien sûr ajuster ci-dessus. Le montage réalisé revient exactement au même que si on avait indiqué les valeurs directement dans le code comme suit (mais il est bien sûr plus propre de passer par des paramètres) :

builder.Services.AddOidcAuthentication(options => {
     options.ProviderOptions.Authority = “
http://localhost:8080/realms/Coucou/”;
     options.ProviderOptions.ClientId = “TestOIDCBlazorWASM”;
     options.ProviderOptions.ResponseType = “code”;
});

Attention, à titre de remarque, à ne jamais rien mettre de secret dans ce fichier, car il est envoyé dans le navigateur.

Test de la configuration et ajustement

Un premier lancement de l’application et un clic sur le bouton Log in montre qu’il y a une erreur :

L’IAM est bien appelée puisque l’écran qu’on voit est fourni par Keycloak. Mais le message nous précise que l’URI de redirection n’est pas correcte. Le problème vient du fait que, lors de l’article précédent, quand nous avons configuré le client d’authentification, nous sommes partis du principe que l’application serait déployée sur http://localhost:88, alors qu’elle est rendue accessible par Visual Studio en débogage sur un autre port (et en HTTPS, en plus) :

Il est donc nécessaire de retourner dans l’IAM et de changer la configuration comme suit (si vous ne voyez pas comment faire, reportez vous à la page http://gouigoux.com/blog-fr/?p=3775) :

Une fois cette modification réalisée (inutile de relancer l’IAM, la modification est prise en compte immédiatement), on peut bien s’identifier dans le navigateur en cliquant à nouveau sur le bouton Log in de l’application :

Connectez-vous bien sûr avec l’utilisateur que vous avez créé dans l’IAM (nommé jpg dans le précédent article), mais pas l’administrateur déclaré au début pour Keycloak : c’est plus propre pour la sécurité. De plus, pour l’instant, le client n’a pas besoin de rôles ou de droits particuliers pour fonctionner, car nous n’avons branché que l’authentification, mais aucun mécanisme d’autorisation bloquant les appels à telle ou telle fonctionnalité en fonction de l’identité du compte, ou même du fait qu’il n’y ait pas de connexion effective (mode anonyme). En fait, le seul effet de la connexion est pour l’instant que la barre en haut à droite affiche le nom complet de la personne :

Si vous ne voyez pas de nom s’afficher, mais que le message dit tout de même “Hello”, c’est certainement que vous n’aviez pas fourni ces informations lors de la création du compte :

Un dernier test pour vérifier que la déconnexion fonctionne bien et nous sommes arrivés au bout de cet épisode :

Au menu des prochains épisodes, activation de l’autorisation sur le client, ce qui nous amènera à proposer de nouveaux composants métier, à déclarer des API sur le serveur, et ensuite à authentifier et autoriser ces accès serveurs également.

Posted in C# | Tagged , , | 2 Comments

TestOIDCBlazorWASM (épisode 1) : préparation de l’IAM Keycloak

Objectif

La toute première dépendance fonctionnelle de l’application test est l’Identity and Authorization Management : il faut gérer les utilisateurs, leur capacité à prouver leur identité et les attributs d’identification qui découlent de l’authentification, et qui vont servir ensuite pour la gestion des autorisations.

Il s’agit de la toute première découpe claire des responsabilités qu’il est essentiel d’avoir dans une application :

  • Identification : ce qui décrit le compte, et par delà le compte la personne qui le possède quand il s’agit de comptes individuels.
  • Authentification : preuve que la personne qui manipule le compte est bien celle qui est prévue pour cela. Un mot de passe ou toute autre preuve fait en sorte que l’identification soit validée électroniquement. Attention, on a parfois tendance à en déduire que c’est une preuve d’identité, mais pour cela, il faudrait aussi que le compte soit délivré par une autorité de confiance qui a vérifié que les attributs du compte étaient bien ceux d’une personne physique à laquelle le compte a été confiée. Et encore, cela ne suffit pas dans le sens où une personne peut très bien confier son compte à une autre. Mais dans ce cas, la charge de la preuve d’usurpation est à l’utilisateur (c’est la raison pour laquelle les conditions générales incluent souvent que vous ne devez pas confier votre compte à une autre personne : pour se couvrir en cas de dispute sur l’identité).
  • Autorisation : les droits que le compte obtient de par ses attributs d’identité, ou associés à son identité. Par exemple, des groupes d’appartenance, des caractéristiques particulières, etc.

L’IAM Keycloak est un produit open source qui va nous permettre de gérer l’authentification et l’identification des comptes pour notre application. La gestion des autorisations sera réalisée dans l’application, encore que comme elle est basée sur des attributs d’identité, on pourrait dire que l’IAM a une responsabilité partielle (nous y reviendrons, mais sur les 5 responsabilités des autorisations telles que définies dans XACML, l’application sera le PDP, le PRP, le PEP, tandis que l’IAM sera le PIP ; il n’y aura pas de PAP dans un premier temps).

L’authentification sera assurée par le protocole OpenID Connect. L’avantage d’utiliser un tel protocole standard est qu’il nous permettra d’élargir aisément vers des fournisseurs d’authentification différents comme Microsoft LiveID, Google ID, France Connect, etc.

L’identification sera assurée par un simple annuaire interne géré par Keycloak. Dans le futur, peut-être qu’un annuaire LDAP sera utilisé, mais c’est tout l’avantage de passer par le protocole normalisé fourni par Keycloak : l’application se branchera sur OIDC et consommera des tokens JWT, et c’est Keycloak qui fera l’indirection avec les implémentations sur des mots de passe et des caractéristiques stockées localement. Si l’implémentation change, ce sera absolument transparent pour l’application. Afin de tester ceci, nous pourrons par exemple remplacer Keycloak par un LemonLDAP-NG, et vérifier que, le paramétrage mis à jour, l’application fonctionne sans ajustement de code quelconque.

Démarrage de Keycloak

Grâce à Docker, rien de plus simple que de démarrer un server Keycloak :

docker run -p 8080:8080 -d -e KEYCLOAK_ADMIN=armoire -e KEYCLOAK_ADMIN_PASSWORD=vBWtB2PloopC042cszXZ –name iam quay.io/keycloak/keycloak:18.0.2 start-dev

Le nom d’utilisateur n’est pas admin ni administrator ni root ou n’importe quelle autre valeur qui puisse faciliter la vie à un éventuel attaquant. Le mot de passe est robuste (et unique pour ce service + stocké dans un outil comme Keepass). Le port 8080 est réexposé sur la machine locale avec le même numéro. Le conteneur est nommé pour faciliter les manipulations dessus. L’image est dans sa version la plus récente au moment de l’écriture de ce blog. C’est une bonne pratique d’utiliser des images fixes : bien sûr, il faut faire l’effort de réaliser les mises à jour régulières, mais au moins, on ne risque pas d’être surpris en production par une modification non compatible. Le choix dépend toutefois aussi de l’importance de la mise à jour et de la confiance que vous avez dans la capacité du fournisseur à respecter la compatibilité ascendante. Enfin, les arguments de ligne de commande correspondent pour l’instant à un démarrage en mode développement. Dans un article plus loin, nous passerons Keycloak dans un mode plus proche de la production (SSL, gestion de la persistance dans une base de données, etc.).

Pas d’inquiétude si le port ne répond pas tout de suite :

Keycloak est un peu lent à se mettre en route, comme vous pourrez le constater en suivant les logs en mode dynamique :

image

Une fois démarré pour de bon, la page d’accueil permet d’accéder à la console :

Et vous vous connecterez alors avec le compte et le mot de passe utilisé plus haut :

image

Nous pouvons passer au…

Paramétrage de Keycloak

Tout d’abord, nous allons paramétrer un tenant dans Keycloak, ce qui s’appelle un royaume (realm en anglais) dans le jargon de l’IAM :

Ce royaume pourrait servir pour authentifier plusieurs utilisateurs pour plusieurs applications : en tant que tenant, il faut vraiment se positionner comme si vous lanciez plusieurs fois l’application Keycloak, sauf que celle-ci vous permet en fait de n’avoir qu’un seul serveur qui tourne, mais avec des contextes complètement étanche que sont les tenants. Pour notre exemple, nous pourrions imaginer le nom d’une société fictice qui s’appellerait Coucou Incorporated. Le royaume sera donc nommé comme ceci :

image

A l’intérieur de ce royaume, nous allons créer un client. Cette fois, il s’agit de l’entité qui va supporter un mode d’authentification particulier dédié à une application. Comme notre but est d’apporter l’authentification et l’identification à l’application TestOIDCBlazorWASM, il est logique que le client s’appelle pareil :

A noter que le protocole SAML est également proposé, en plus d’OpenID Connect, mais il s’agit d’un protocole plus ancien, et proche de l’obsolescence même si, les choses changeant lentement dans le domaine de l’IAM, il est encore largement utilisé.

Dans le client, nous allons commencer par l’onglet Settings, dans lequel il faudra déclarer les URLs valides pour la redirection. OIDC fonctionne en effet en prenant la main sur le navigateur, en demandant les crédentiels, et ensuite en redirigeant vers la page web initiale tout en lui passant un code qui lui permettra d’appeler en retour le service d’IAM et d’obtenir un token JWT portant l’identification de la personne (en tout cas, dans le flow standard). Par sécurité, les URLs autorisées pour cette redirection doivent donc être spécifiées. Nous anticipons un peu, mais l’application sera déployée dans un premier temps en local, sur le port 88. Et comme nous utiliserons le mécanisme d’authentification intégré de .NET, les URLs à paramétrer sont comme ceci :

image

Ensuite, pour ne pas avoir de problème de CORS, il faut également déclarer que l’application web est une origine autorisée pour l’appel à l’IAM (sinon la requête avec le code fourni lors de la redirection ne sera pas autorisé). Ceci est fait un peu plus bas dans le même onglet :

image

Attention à ne surtout pas mettre un slash à la fin, sinon le mécanisme plante ! Oui, c’est très sensible…

Dans l’onglet Roles, déclarez ensuite deux rôles qu’on appellera administrateur et lecteur. Le but est en effet de faire en sorte que l’application soit propre du point de vue RBAC (Role Based Access Control), c’est à dire que les autorisations reposent sur la définitions de rôles associés aux groupes ou aux utilisateurs. La question de la persistance de ces définitions de rôles et de leur association aux attributs d’identité se pose : une séparation stricte des responsabilités voudrait qu’elles se trouvent dans l’application, car seul le métier fonctionnel peut au final définir clairement des autorisations sur les entités qu’il manipule. Mais la tentation de la centralisation de la gestion des rôles est en général forte dans les entreprises, et nous sacrifierons ici à cette approche. Nous reviendrons toutefois sur ce point lors de l’article sur la gestion complète des autorisations, avec XACML ou avec OPA. En attendant, les rôles sont déclarés dans Keycloak, mais sur le client défini (attention à ne pas définir des rôles globaux au niveau du royaume, en utilisant le menu sur la gauche ; c’est bien l’onglet Roles du client juste créé qui doit être utilisé) :

image

Pour que les rôles soient envoyés lors de l’authentification / identification, il faut créer un mapper qui va les associer au ticket (bouton Add builtin dans l’onglet Mappers). Pour cela, on générera un mapper de type client roles puis on accédera à ce mapper pour modifier le paramétrage et faire en sorte que les rôles soient disponibles aussi dans le token d’identification :

image

Cette manipulation est nécessaire parce que les rôles seront sinon uniquement dans l’access token, qui est le second JWT utilisé, et envoyé au serveur pour qu’il décide des autorisations. Dans notre cas, l’application est une SPA en Blazor et pour pouvoir n’afficher des menus que si les droits sont présents (ce qui ne veut bien sûr pas dire qu’on peut se permettre de ne pas revérifier les autorisations côté serveur), il faut bien que nous ayons cette info dès la phase d’authentification passée et le premier JWT (celui d’identification) récupéré.

Création d’un utilisateur

Le client (au sens OIDC, à savoir la définition dans l’IAM de l’application autorisée à utiliser le service) étant prêt, il reste à créer un utilisateur. A terme, nous ferons quelque chose de plus sophistiqué en branchant Keycloak sur un annuaire LDAP ou – mieux – sur une application qui simulerait un legacy, ce qui nous permettra de montrer l’extensibilité de Keycloak à l’aide d’un plugin intégré dans un JAR. Mais là encore, j’anticipe…

Pour créer un utilisateur, pas besoin d’explications particulières, ça se passe dans le menu Users. Dans un premier temps, on peut se contenter d’un utilisateur auquel on donnera les deux rôles clients administrateur et lecteur :

Quand vous donnez les crédentiels, inutile de laisser le mode de passe en temporaire : vu que vous utiliserez vous-même le compte, pas besoin qu’on vous demande à la première connexion de changer le mot de passe…

Test

Si vous retournez sur les informations générales du client, vous verrez que les endpoints sont en fait des liens :

En ouvrant le premier, qui est celui qui nous concerne, vous obtenez la définition formelle du endpoint OIDC :

En réduisant l’URL à juste le début, vous accédez à une page plus réduite qui expose la clé publique utilisée par le client, ainsi que l’adresse pour le token et l’adresse pour la gestion de compte :

C’est cette dernière qui nous intéresse pour tester que l’utilisateur fonctionne bien. En allant dessus, vous arrivez sur l’interface de gestion de son compte pour l’utilisateur. Et il peut bien sûr se connecter avec le bouton en haut à droite :

image

Si ça marche, c’est que l’utilisateur – au moins – est bien créé :

Ca ne veut pas dire que tout est bien paramétré, mais pour l’utilisation de cette IAM dans une application, nous verrons au prochain article !

Posted in C# | Tagged , , | Leave a comment

TestOIDCBlazorWASM (épisode 0) : une application Blazor WebAssembly avec KeyCloak, RabbitMQ, MongoDB et une GED Nuxeo accédée en CMIS

Cet article est le premier d’une série qui va expliquer en détails la conception et les choix architecturaux de l’application de test fournie sur https://github.com/jp-gouigoux/TestOIDCBlazorWASM.

Après avoir écumé de nombreuses ressources sur internet ou dans des livres techniques montrant chacune des bouts ou des implémentations approximatives de pans architecturaux, je souhaitais brancher tous ces éléments les uns avec les autres et créer une sortie d’architecture modèle de ce que j’avais en tête. L’application avait pour but de montrer un assemblage à peu près correct des technologies suivantes :

  • Blazor WebAssembly
  • API ASP.NET Core
  • Authentification OpenID Connect avec JWT
  • Authentification JWT avec tokens séparés pour le client (ID Token) et le serveur (Access Token)
  • Implémentation de l’identity provider avec KeyCloak, pour amener à un bon niveau d’indirection de l’IAM
  • Gestion des rôles pour les autorisations
  • Sécurité en rideaux, avec blocages multiples au niveau du client et sécurité sur le serveur basée sur les mêmes données uniques d’utilisateur
  • Conception dans l’esprit RBAC, avec une séparation des groupes et des rôles
  • Gestion asynchrone de certains traitements complexes, grâce à une file de messages RabbitMQ
  • Génération de PDF en code
  • Déploiement en Docker des dépendances (celui pour les modules principaux sera réalisé par la suite)
  • Persistance découplée, avec les données dans une base de données MongoDB et les documents dans une GED CMIS, en l’occurrence Nuxeo
  • etc.

Ces technologies et cette architecture ont été définies avec des critères d’industrialisation de l’approche, pour mettre en œuvre les principes de découplage et de séparation correcte des responsabilités dans une approche de services (voire microservices dans leur compréhension en tant qu’outils d’évolutivité simplifiée des applications complexes). Le fonctionnel n’a aucune importance et est volontairement fortement simplifié pour ne pas perturber l’analyse sur les caractéristiques techniques de l’ensemble.

Tout n’est pas encore finalisé, mais une première version a été publiée (https://github.com/jp-gouigoux/TestOIDCBlazorWASM/tree/v1.0.0) qui comprend la majorité des points de difficulté qui étaient attendus.

Les articles qui suivront traiteront chacun d’une partie de l’application, et tenteront de donner le maximum de détails à chaque fois pour reproduire cette application dans sa version actuelle à partir du modèle “vide” produit par Visual Studio. Mon but est que chaque ligne additionnelle soit expliquée en détails, car j’ai passé trop de temps à essayer de comprendre le pourquoi derrière de nombreux articles sur internet qui donnaient juste le code qu’il fallait entrer pour que ça marche, sans fournir d’explication. Le problème de cette approche est que, lorsque les versions ou les contextes changent, il est compliqué d’adapter si on n’a pas compris ce qui se passe derrière.

Le rythme de publication espéré est d’un article par semaine environ (hors congés) et j’imagine qu’il en faudra une petite dizaine pour arriver au bout de l’explication complète à l’issue de laquelle les lecteurs seront normalement en mesure de recréer entièrement l’application, et ce dans leur propre contexte (GED CMIS différente, fournisseur d’identité OIDC différent, etc.).

Posted in C# | Tagged , , , , , , , | Leave a comment

NDepend, la pleine maturité !

Il y a quelques bonnes années déjà, j’avais demandé à NDepend si je pouvais bénéficier d’une licence d’essai grâce à mon statut de MVP (beaucoup d’éditeurs le font) et Patrick, le responsable, avait bien voulu accéder à ma requête. Déjà, j’avais été impressionné et j’avais fait acheter une licence à une entreprise pour laquelle je travaillais à l’époque.

Il y a peu, j’ai eu envie de faire quelques tests de couplage d’une application par rapport à une autre, suite à une interop conçue il y a longtemps, dans une galaxie très lointaine où les développeurs ne connaissaient pas le concept de couplage lâche. Bon d’accord, je voulais savoir à quel point j’avais mal codé à l’époque, et si le travail que j’avais supervisé avec mon stagiaire était aujourd’hui regardable sans avoir honte. J’ai donc demandé à Patrick à nouveau s’il voulait me faire faire un test de son outil, et il a encore accepté, en me prévenant qu’il y avait du nouveau, en l’occurrence un graphe de dépendances réécrit. Pour être honnête, je pensais que ça allait surtout être du relookage, une ergonomie améliorée ou quelques efforts pour la performance… En fait, la refonte n’a pas usurpé son nom et le résultat est assez exceptionnel ! Je ne résiste pas au plaisir de renvoyer sur l’animation qui décrit ça dans le site NDepend :

Imaginez que vous puissiez d’un coup voir toutes les dépendances dans votre code, en zoomant à n’importe quel niveau et en revenant en arrière avec un simple scroll de souris. Ca paraît aussi magique que ces images dans lesquelles on zoome quasiment sans limite, sans que l’image serait dynamique en plus. Mais ça marche… et en plus avec des performances de dingue. De quoi voir votre code complètement différemment, car on peut sans arrêt jongler entre l’architecture de haut niveau et le pourquoi de la dépendance dans le code. La difficulté principale du logiciel, qui était liée à la difficulté de lire la matrice de dépendances, est donc complètement levée.

Retour sur un vieux projet

Il y a presque dix ans, le framework Bamboo permettait de faire de la prévalence (persistance de données en mémoire, y compris post-extinction du processus), ce qui était phénoménal en termes de performance et de capacité de développement rapide. Honnêtement, je n’ai jamais compris pourquoi cette approche n’a pas plus percée, mais je pense que le pas psychologique était trop grand. Trop en avance sur son temps, mais c’est comme toutes les technos, ça reviendra dans dix ans et on hurlera au génie et à l’innovation incroyable. Bref…

En son temps, je me disais que peut-être que rendre compatible Bamboo avec SQL permettrait de faciliter le passage à certains, et j’ai donc eu un stagiaire, excellent de surcroit, qui s’appelait Damien et qui a créé sous ma supervision un fournisseur ADO.NET pour Bamboo, comme j’en ai parlé ici. Le code est en open source et vous pourrez le trouver sur ce dépôt Github.

J’étais en train de chercher une solution pour du prototypage très rapide d’API métier avec des règles business et je me suis rappelé que la prévalence était le meilleur moyen de faire cela. Ayant besoin d’une compatibilité SQL, je me suis rappelé de ce projet, mais était-il vraiment au point ? Je me rappelais de l’avoir présenté au BreizhCamp avec Damien, mais était-ce encore à l’état de prototype ou bien terminé ? Gros blanc…

Comment savoir si ce code était à peu près correct ? En retournant dans le code source ? C’est certainement ce que j’ai fait de plus compliqué conceptuellement avec un stagiaire et je n’avais pas trop envie de me replonger dans le code. Quoi de mieux que NDepend pour reprendre la main.

Allô, Patrick ?

La même recette a encore fonctionné et le responsable de cet outil de merveille a été d’accord pour m’accorder encore une licence d’utilisation. Me voilà donc reparti à l’assaut du projet, mais bien équipé.

Il faut reconnaître que ce fameux graphe est assez incroyable :

icpglfhcaomnplnj

En un coup d’oeil, j’ai retrouvé le fonctionnement du provider, avec un analyseur de syntaxe Irony et un fournisseur complètement compatible avec ADO.NET.

Tiens, quand on zoome, on voit vite quelque chose d’intéressant : une
dépendance circulaire qui traine !

poomapmenohalmip

Je suis pourtant assez maniaque sur le sujet et le stagiaire était excellent, donc ça vaut le coup d’aller voir en faisant un clic droit et en envoyant dans la matrice de dépendances (un bête clic-droit sur la flèche, comme ça semble logique, tombe sous la main) :

fgodlmahidbpkngo

Oui, juste une petite dépendance mais l’exception aurait dû être dans un espace de nommage commun au client et à l’extracteur de code pour éviter ce problème…

Problème trouvé en… allez une minute top chrono, alors qu’une analyse de code sans l’outil ne m’aurais pas permis de le détecter. Et si je l’avais cherché avec les méthodes traditionnelles, j’aurais mis au moins un quart d’heure à faire le tour des classes concernées, sans parler des risques d’erreur, surtout que quand on se concentre sur les dépendances fonctionnelles, on a vite fait de passer complètement à côté de la partie non fonctionnelle et les exceptions en font partie.

Vision complète d’un codebase

Un autre graphique permet de voir en visite la structure globale du fonctionnement de l’application exemple, et donc de retrouver tout de suite comment fonctionne le test du provider ADO.NET :

phiaoojdgjnjniof

Evidemment, si on regarde tout au niveau méthodes et champs, on en bave, et ce même si on peut zoomer comme on veut !

nogcnaapjcandfei

Mais le miracle vient de la fonction de clustering, qui permet de trouver des ensembles intelligents de dépendances :

bcbnhbmpplgdpmlm

Ce qui est surtout très fort est que le graphe est très réactif. Tous les calculs sont faits en amont et même ça, ça ne prend qu’une dizaine de secondes pour un gros projet comme celui-ci.

L’outil de recherche nous permet de localiser en quelques secondes la classe à analyser dans tout ce plat de spaghettis :

poajehbgmjelkljf

C’est là qu’on se rend compte qu’on est bien de peu de chose avec nos petits projets à côte du framework .NET, sachant que là, on ne voit que System.Data !

jpnnpppefecfobjm

Eh oui, tout à gauche, le petit bout ridicule correspond au graphe
montré précédemment. Tout ce qui est à droite est en dépendance, et
on voit en bleu foncé les appels indirects. Là encore, on se rend
compte qu’un appel relativement simple pour nous mobilise en fait
toute une tringlerie dans le parser (Irony, dans le cas étudié) mais
aussi dans les dépendances de la BCL, en l’occurrence surtout
System.Data comme expliqué plus haut, le projet consistant à créer
un fournisseur de données ADO.NET pour le moteur de prévalence
Bamboo.

Une autre façon de voir ce ratio est sur les métriques en treemap :

nfpaieefpmllllfj

System.Data est énorme en termes d’instructions IL mais aussi en namespaces utilisés. Étonnamment, le framework Bamboo n’est pas si important qu’on pourrait le croire, et contient à peine deux fois plus d’instructions qu’Irony, lui-même correspondant en gros à la taille du provider, qui inclue toutefois la description de la grammaire à parser, en l’occurrence SQL, alors qu’elle vient elle-même d’Irony.

Gestion de la qualité du code

Le dashboard est ultra-complet :

njgmdhmlpkphmalh

Et encore, je n’avais pas activé la gestion de la couverture de code par les tests ni la comparaison par rapport à une baseline, vu que c’était la dernière version de code du provider et que je n’ai pas de version ancienne ou nouvelle à opposer à celle étudiée.

Je me suis toujours posé la question de la façon dont était étudiée la dette, donc c’est l’occasion de voir le mécanisme grâce à l’aide contextuelle (voir ci-dessus). Ce qui est bien est qu’on peut toujours revenir en arrière et changer facilement le contenu du projet d’analyse, ce qui dans mon cas était particulièrement important pour cette analyse de la dette technique, car je n’allais évidemment pas prendre en compte les 177 jours de résolution de System.Data et autres dépendances. En relançant pour la seule assembly correspondant au provider, on retombe sur quelque chose de plus logique… et rassurant :

lfehblnffbocbkjg

Encore deux règles critiques violées, je me demande ce que ça peut être. Là encore, l’ergonomie est top et un clic nous amène directement sur les deux problèmes :

ocoeckhofjmbbhmj

Encore une fois cette dépendance cyclique, et on sait maintenant qu’il y aura un peu de boulot, car il y a 20 occurrences. Un double-clic sur la règle, et nous voilà avec la liste tout prête des endroits où porter une correction :

gdcfbodhamghnlfl

On retrouve la dépendance en boucle vue plus haut et causée par le fait que l’exception n’a pas été externalisée.

Pour les autres, le problème est au niveau du namespace SampleApplication.Queries qui appelle SampleApplication. Pour voir ce qui se passe précisément, on peut copier les types dans la matrice :

nohaekaijkfagpoi

Un coup d’œil nous permet alors de trouver le point exact du problème :

jfobiobakpjhbdec

L’autre possibilité est de passer par l’ouverture du code piloté par NDepend :

fjhfhnfadojfcpeo

La fenêtre permet de choisir lequel des deux problèmes est à traiter :

belhepjoigjemilk

Et un clic nous renvoie alors directement sur le cœur du problème :

kgankpgkccmaimhj

L’exécution pourrait effectivement être plus propre si on n’avait pas à caster l’objet passé dans des types qui sont sur SampleApplication, à savoir le PersistenceSet et le PersistenceEngine.

Conclusion

Au final, je n’avais pas utilisé NDepend depuis quelques temps vu que je faisais plus d’architecture et d’urbanisation des SI que de code, mais je vois que l’outil a encore grandi et s’est renforcé. Sa puissance peut paraître difficile à maitriser au premier abord, mais l’interface fait intelligemment des liens multiples entre toutes les fonctions, ce qui fait qu’on s’y retrouve d’une façon ou d’une autre, et que finalement chacun aura peut-être une façon différente de naviguer entre les multiples outils, mais c’est sans importance.

La fonction de graphe, quant à elle, est proprement bluffante. La performance est même telle qu’on se demande s’il n’y aurait pas de l’accélération des traitements par la carte graphique, avec des approches vectorielles. La possibilité de zoomer quasi à l’infini fait penser à ces images à ultra-haute résolution, mais ce n’est visiblement pas cela qui est utilisé. Dans tous les cas, la méthode fait des miracles.

Bien sûr, si votre code est très complexe, il restera compliqué à lire et déboguer, même avec NDepend, mais vous aurez un pilote pour vous y retrouver. Et même dans le cas où le code est relativement bien connu, on peut retomber sur des choses à améliorer comme je l’ai constaté sur ce petit projet !

Je ne peux pas finir sans féliciter l’équipe NDepend, car cet outil est vraiment un bijou. Je ne sais pas si Patrick est tout seul derrière, mais j’ai du mal à imaginer ça, vue la puissance et l’utilisabilité du produit. Et encore, je n’ai pas parlé de toutes les règles et alertes, ainsi que les indicateurs sur la qualité du produit ! Bref, chapeau Smile.

En me baladant sur le site, je vois que NDepend est même intégré à Azure DevOps. Manque plus que le support de Visual Studio Code et on aura atteint l’absolue perfection !

Posted in .NET, Performance, Prevalence | Tagged | 2 Comments

De Windows 95 à Windows 10

image

Doit-on conclure que Windows 10 est 320 fois plus utile / performant / fonctionnel que Windows 95 ? Pour avoir connu les deux, clairement le nouveau est mieux, mais pas tellement plus rapide alors que ma machine est 100 fois plus puissante que l’ancienne…

Est-ce qu’à un moment, ça ne va pas valoir le coup d’abandonner trente ans de compatibilité ascendante et de repartir depuis les couches basses, de façon à avoir quelque chose d’un tant soit peu performant ?

Posted in Uncategorized | Leave a comment

Web2day Nantes !

J’aurais le plaisir de présenter un retour d’expérience à Nantes à la mi-juin, à l’occasion du Web2day “Digital Springbreak” (j’espère que ce sera moins bruyant qu’à Fort Lauderdale).

Le sujet :

Votre SI manque de souplesse ? Vous n’êtes pas le seul… La majorité des SI actuels utilisent 80% du budget pour la maintenance et seulement 20% pour évoluer. Il existe pourtant des solutions pour industrialiser l’informatique mais elles sont aujourd’hui peu diffusées. La solution est en partie technique (le trio MDM+BRMS+BPM, la Web Oriented Architecture) mais surtout fonctionnelle (alignement Business/IT, normalisation des échanges, DDD). Elle a été mise en œuvre par l’auteur sur plusieurs grands SI, et cette conférence vous expliquera comment.

Bannière-speaker-Jean-Philippe-Gouigoux

Posted in Uncategorized | Leave a comment

Teaser pour l’atelier BreizhCamp “Urbanisation du SI expliquée par les Lego”

Après un weekend complet à préparer des Lego reflétant au mieux les complexités d’un SI, je commence à avoir quelque chose de propre :

Orchestration, chorégraphie, Entreprise Integration Patterns, couche d’intermédiation, API gateway, robustesse d’intégration, coupe-circuit, on va parler de tout ça et poser les bonnes méthodes pour urbaniser un SI avec des microservices, tout en se basant sur des Lego pour que même les débutants prennent les bonnes pratiques d’architecture.

Mon fiston m’a bien aidé pour préparer plein d’EIP, ça va être un chouette atelier je pense !

Posted in Uncategorized | Leave a comment