IdentityServer peut être utilisé par les entreprises pour mettre en place une solution pour :
- la protection de leurs ressources ;
- l’authentification des utilisateurs via une base de données ou des fournisseurs externes d’identité (Microsoft, Google, Facebook, etc.) ;
- la gestion des sessions et la fédération (single sign-on) ;
- la génération des jetons pour les clients ;
- la validation des jetons et bien plus.
Ce billet est le neuvième que j’écris sur le sujet. Les billets précédents ont porté sur les points suivants :
Mise en place d’un STS avec IdentityServer4 pour sécuriser ses applications .NET
Sécurisation d’une Web API ASP.NET Core avec le STS IdentityServer4
IdentityServer4 : création et configuration du Client pour accéder à une Web API ASP.NET Core sécurisée
IdentityServer4 : Authentification d’un utilisateur avec OpenID Connect
IdentityServer4 : création et configuration d’un client utilisant OpenID Connect
IdentityServer4 : Autoriser l’application MVC à accéder à l’API, via le jeton obtenu du STS
IdentityServer4 : prise en charge du provider tiers Microsoft pour l'authentification
IdentityServer4 : prise en charge du provider tiers Github pour l'authentification
Depuis le début de notre ballade dans l’univers fascinant d’IdentityServer, nous avons utilisé une classe Config pour définir les informations de configuration de notre serveur de gestion d’identité :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | public class Config { public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "consoleappclient", AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "testapi" } }, // OpenID Connect implicit flow client (MVC) new Client { ClientId = "mvcappclient", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.HybridAndClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, RequireConsent = false, RedirectUris = { "https://localhost:5005/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:5005/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "testapi" }, AllowOfflineAccess = true } } ; } public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("testapi", "My Test API") }; } public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; } public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser{SubjectId = "818727", Username = "alice", Password = "alice", Claims = { new Claim(JwtClaimTypes.Name, "Alice Smith"), new Claim(JwtClaimTypes.GivenName, "Alice"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://alice.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json) } }, new TestUser{SubjectId = "88421113", Username = "bob", Password = "bob", Claims = { new Claim(JwtClaimTypes.Name, "Bob Smith"), new Claim(JwtClaimTypes.GivenName, "Bob"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://bob.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), new Claim("location", "somewhere") } } }; } } |
Avec cette approche, chaque fois que nous aurons une nouvelle application qui veut utiliser notre solution IdentityServer, nous devons modifier la classe Config pour ajouter ce nouveau client, ensuite régénérer et déployer notre application. Idem pour toute ressource (une API par exemple), que nous voulons sécuriser l’accès.
Cette approche n’est pas très pratique, car elle ne permet pas de livrer une solution clé en main.
Dans ce billet, nous verrons comment utiliser une base de données SQL Server pour le stockage des informations de configuration et Entity Framework pour l’accès.
Support d’Entity Framework dans IdentityServer 4
IdentityServer offre un package qui fournit des API permettant d’assurer facilement la persistante des données de configuration en utilisant EntityFramework. Ces API offrent des implémentations des interfaces IClientStore et IResourceStore pour la gestion de la persistance des clients, des ressources d’API et des ressources d’identité. Ces dernières utilisent la classe ConfigurationDbContext qui hérite du DbContext.
Pour commencer, nous allons installer le package IdentityServer4.EntityFramework à notre application IdentityServer, via le gestionnaire de packages :
Vous devez aussi installer le package Microsoft.EntityFrameworkCore.Sqlite.
Configuration du Store
Nous allons utiliser une base de données SQLite pour le stockage de nos données. Après l’ajout des packages, la seconde chose à faire est l’ajout d’un fichier de paramètres d’application (appsettings.json) pour la définition de la chaine de connexion :
Code json : | Sélectionner tout |
1 2 3 4 5 | { "ConnectionStrings": { "DefaultConnection": "Data Source=IdentityServer.db" } } |
Suite à cela, nous devons ajouter un constructeur à la classe Startup.cs afin que les données de configuration soient obtenues au démarrage de l’application :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 | public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } |
Ensuite, nous devons modifier la méthode ConfigureServices pour remplacer AddInMemoryClients, AddInMemoryIdentityResources et AddInMemoryApiResources avec notre nouveau store de configuration, en utilisant la méthode d’extension AddIdentityServer :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 | .AddConfigurationStore(options => { options.ConfigureDbContext = builder => builder.UseSqlite(Configuration.GetConnectionString("DefaultConnection"), sql => sql.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name)); }) |
Vous devez ajouter les namespaces suivants :
Code c# : | Sélectionner tout |
1 2 3 | using Microsoft.Extensions.Configuration; using Microsoft.EntityFrameworkCore; using System.Reflection; |
Le code complet de la classe Startup devient ceci :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); //configure identity server with in-memory stores, keys, clients and resources services.AddIdentityServer() .AddDeveloperSigningCredential() .AddConfigurationStore(options => { options.ConfigureDbContext = builder => builder.UseSqlite(Configuration.GetConnectionString("DefaultConnection"), sql => sql.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name)); }) .AddTestUsers(Config.GetUsers()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage() ; } app.UseStaticFiles(); app.UseIdentityServer(); app.UseMvcWithDefaultRoute(); } } |
Configuration de la migration
Le package IdentityServer4.EntityFramework contient les classes d’entité correspondant au modèle de données de configuration d’EntityFramework. Nous allons utiliser la migration et les outils Entity Framework en ligne de commande pour créer la base de données avec un schéma de données correspondant à celui défini par ce package.
Depuis la version 2.1.3 du kit de développement de .NET Core, les commandes dotnet ef sont intégrées par défaut et supportent Entity Framework Core 2.0 et versions ultérieures. Si vous avez une version égale ou supérieure à cette version, aucune installation supplémentaire n’est nécessaire pour pouvoir utiliser les outils en ligne de commande. Sinon, vous devez installer le package Microsoft.EntityFrameworkCore.Tools.DotNet.
L’approche utilisée ici est CodeFirst. Nous devons exécuter la commande « dotnet ef migrations add » en invite de commande à partir du dossier contenant le fichier .csproj. Cette commande va permettre de générer le code correspondant au modèle de données.
La commande complète à exécuter est la suivante :
Code : | Sélectionner tout |
1 2 | dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb |
Code : | Sélectionner tout |
Dotnet ef database update
Une fois cette commande exécutée, un fichier de base de données « IdentityServer.db » sera créé à la racine du dossier du projet. Si vous ouvrez ce fichier avec « DB Browser for SQLite », vous aurez les tables suivantes :
Initialisation de la base de données
Nous allons maintenant initialiser la base de données avec les données qui ont été définies dans le fichier Config.cs. Vous devez éditer le fichier Startup.cs et ajouter la méthode suivante :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | private void InitializeDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) { var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>(); if (!context.Clients.Any()) { foreach (var client in Config.GetClients()) { context.Clients.Add(client.ToEntity()); } context.SaveChanges(); } if (!context.IdentityResources.Any()) { foreach (var resource in Config.GetIdentityResources()) { context.IdentityResources.Add(resource.ToEntity()); } context.SaveChanges(); } if (!context.ApiResources.Any()) { foreach (var resource in Config.GetApiResources()) { context.ApiResources.Add(resource.ToEntity()); } context.SaveChanges(); } } } |
Puis appeler cette dernière dans la méthode Configure :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) { InitializeDatabase(app); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage() ; } app.UseStaticFiles(); app.UseIdentityServer(); app.UseMvcWithDefaultRoute(); } |
Cela fait, exécuter votre application. Toutes les données presentes dans le fichier Config.cs, en dehors de la liste des utilisateurs seront enregistrés dans la base de données.
Désormais, si vous avez une nouvelle ressource ou un nouveau client à configurer, vous devez simplement enregistrer ce dernier dans la base de données pour que celui-ci soit pris en charge par IdentityServer.
Dans le prochain billet, nous verrons comment utiliser IdentityServer avec ASP.NET Core Identity.