This article shows how to implement a database store for the IdentityServer4 configurations for the Client, ApiResource and IdentityResource settings using Entity Framework Core and SQLite. This could be used, if you need to create clients, or resources dynamically for the STS, or if you need to deploy the STS to multiple instances, for example using Service Fabric. To make it scalable, you need to remove all session data, and configuration data from the STS instances and share this in a shared resource, otherwise you can run it only smoothly as a single instance.
Information about IdentityServer4 deployment can be found here:
http://docs.identityserver.io/en/release/topics/deployment.html
Code: https://github.com/damienbod/AspNetCoreIdentityServer4Persistence
Implementing the IClientStore
By implementing the IClientStore, you can load your STS client data from anywhere you want. This example uses an Entity Framework Core Context, to load the data from a SQLite database.
using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; namespace AspNetCoreIdentityServer4Persistence.ConfigurationStore { public class ClientStore : IClientStore { private readonly ConfigurationStoreContext _context; private readonly ILogger _logger; public ClientStore(ConfigurationStoreContext context, ILoggerFactory loggerFactory) { _context = context; _logger = loggerFactory.CreateLogger("ClientStore"); } public Task<Client> FindClientByIdAsync(string clientId) { var client = _context.Clients.First(t => t.ClientId == clientId); client.MapDataFromEntity(); return Task.FromResult(client.Client); } } }
The ClientEntity is used to save or retrieve the data from the database. Because the IdentityServer4 class cannot be saved directly using Entity Framework Core, a wrapper class is used which saves the Client object as a Json string. The entity class implements helper methods, which parses the Json string to/from the type Client class, which is used by Identityserver4.
using IdentityServer4.Models; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Threading.Tasks; namespace AspNetCoreIdentityServer4Persistence.ConfigurationStore { public class ClientEntity { public string ClientData { get; set; } [Key] public string ClientId { get; set; } [NotMapped] public Client Client { get; set; } public void AddDataToEntity() { ClientData = JsonConvert.SerializeObject(Client); ClientId = Client.ClientId; } public void MapDataFromEntity() { Client = JsonConvert.DeserializeObject<Client>(ClientData); ClientId = Client.ClientId; } } }
Teh ConfigurationStoreContext implements the Entity Framework class to access the SQLite database. This could be easily changed to any other database supported by Entity Framework Core.
using IdentityServer4.Models; using Microsoft.EntityFrameworkCore; namespace AspNetCoreIdentityServer4Persistence.ConfigurationStore { public class ConfigurationStoreContext : DbContext { public ConfigurationStoreContext(DbContextOptions<ConfigurationStoreContext> options) : base(options) { } public DbSet<ClientEntity> Clients { get; set; } public DbSet<ApiResourceEntity> ApiResources { get; set; } public DbSet<IdentityResourceEntity> IdentityResources { get; set; } protected override void OnModelCreating(ModelBuilder builder) { builder.Entity<ClientEntity>().HasKey(m => m.ClientId); builder.Entity<ApiResourceEntity>().HasKey(m => m.ApiResourceName); builder.Entity<IdentityResourceEntity>().HasKey(m => m.IdentityResourceName); base.OnModelCreating(builder); } } }
Implementing the IResourceStore
The IResourceStore interface is used to save or access the ApiResource configurations and the IdentityResource data in the IdentityServer4 application. This is implemented in a similiar way to the IClientStore.
using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace AspNetCoreIdentityServer4Persistence.ConfigurationStore { public class ResourceStore : IResourceStore { private readonly ConfigurationStoreContext _context; private readonly ILogger _logger; public ResourceStore(ConfigurationStoreContext context, ILoggerFactory loggerFactory) { _context = context; _logger = loggerFactory.CreateLogger("ResourceStore"); } public Task<ApiResource> FindApiResourceAsync(string name) { var apiResource = _context.ApiResources.First(t => t.ApiResourceName == name); apiResource.MapDataFromEntity(); return Task.FromResult(apiResource.ApiResource); } public Task<IEnumerable<ApiResource>> FindApiResourcesByScopeAsync(IEnumerable<string> scopeNames) { if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames)); var apiResources = new List<ApiResource>(); var apiResourcesEntities = from i in _context.ApiResources where scopeNames.Contains(i.ApiResourceName) select i; foreach (var apiResourceEntity in apiResourcesEntities) { apiResourceEntity.MapDataFromEntity(); apiResources.Add(apiResourceEntity.ApiResource); } return Task.FromResult(apiResources.AsEnumerable()); } public Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeAsync(IEnumerable<string> scopeNames) { if (scopeNames == null) throw new ArgumentNullException(nameof(scopeNames)); var identityResources = new List<IdentityResource>(); var identityResourcesEntities = from i in _context.IdentityResources where scopeNames.Contains(i.IdentityResourceName) select i; foreach (var identityResourceEntity in identityResourcesEntities) { identityResourceEntity.MapDataFromEntity(); identityResources.Add(identityResourceEntity.IdentityResource); } return Task.FromResult(identityResources.AsEnumerable()); } public Task<Resources> GetAllResourcesAsync() { var apiResourcesEntities = _context.ApiResources.ToList(); var identityResourcesEntities = _context.IdentityResources.ToList(); var apiResources = new List<ApiResource>(); var identityResources= new List<IdentityResource>(); foreach (var apiResourceEntity in apiResourcesEntities) { apiResourceEntity.MapDataFromEntity(); apiResources.Add(apiResourceEntity.ApiResource); } foreach (var identityResourceEntity in identityResourcesEntities) { identityResourceEntity.MapDataFromEntity(); identityResources.Add(identityResourceEntity.IdentityResource); } var result = new Resources(identityResources, apiResources); return Task.FromResult(result); } } }
The IdentityResourceEntity class is used to persist the IdentityResource data.
using IdentityServer4.Models; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Threading.Tasks; namespace AspNetCoreIdentityServer4Persistence.ConfigurationStore { public class IdentityResourceEntity { public string IdentityResourceData { get; set; } [Key] public string IdentityResourceName { get; set; } [NotMapped] public IdentityResource IdentityResource { get; set; } public void AddDataToEntity() { IdentityResourceData = JsonConvert.SerializeObject(IdentityResource); IdentityResourceName = IdentityResource.Name; } public void MapDataFromEntity() { IdentityResource = JsonConvert.DeserializeObject<IdentityResource>(IdentityResourceData); IdentityResourceName = IdentityResource.Name; } } }
The ApiResourceEntity is used to persist the ApiResource data.
using IdentityServer4.Models; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Threading.Tasks; namespace AspNetCoreIdentityServer4Persistence.ConfigurationStore { public class ApiResourceEntity { public string ApiResourceData { get; set; } [Key] public string ApiResourceName { get; set; } [NotMapped] public ApiResource ApiResource { get; set; } public void AddDataToEntity() { ApiResourceData = JsonConvert.SerializeObject(ApiResource); ApiResourceName = ApiResource.Name; } public void MapDataFromEntity() { ApiResource = JsonConvert.DeserializeObject<ApiResource>(ApiResourceData); ApiResourceName = ApiResource.Name; } } }
Adding the stores to the IdentityServer4 MVC startup class
The created stores can now be used and added to the Startup class of the ASP.NET Core MVC host project for IdentityServer4. The AddDbContext method is used to setup the Entity Framework Core data access and the AddResourceStore as well as AddClientStore are used to add the configuration data to IdentityServer4. The two interfaces and also the implementations need to be registered with the IoC.
The default AddInMemory… extension methods are removed.
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ConfigurationStoreContext>(options => options.UseSqlite( Configuration.GetConnectionString("ConfigurationStoreConnection"), b => b.MigrationsAssembly("AspNetCoreIdentityServer4") ) ); ... services.AddTransient<IClientStore, ClientStore>(); services.AddTransient<IResourceStore, ResourceStore>(); services.AddIdentityServer() .AddSigningCredential(cert) .AddResourceStore<ResourceStore>() .AddClientStore<ClientStore>() .AddAspNetIdentity<ApplicationUser>() .AddProfileService<IdentityWithAdditionalClaimsProfileService>(); }
Seeding the database
A simple .NET Core console application is used to seed the STS server with data. This class creates the different Client, ApiResources and IdentityResources as required. The data is added directly to the database using Entity Framework Core. If this was a micro service, you would implement an API on the STS server which adds, removes, updates the data as required.
static void Main(string[] args) { try { var currentDirectory = Directory.GetCurrentDirectory(); var configuration = new ConfigurationBuilder() .AddJsonFile($"{currentDirectory}\\..\\AspNetCoreIdentityServer4\\appsettings.json") .Build(); var configurationStoreConnection = configuration.GetConnectionString("ConfigurationStoreConnection"); var optionsBuilder = new DbContextOptionsBuilder<ConfigurationStoreContext>(); optionsBuilder.UseSqlite(configurationStoreConnection); using (var configurationStoreContext = new ConfigurationStoreContext(optionsBuilder.Options)) { configurationStoreContext.AddRange(Config.GetClients()); configurationStoreContext.AddRange(Config.GetIdentityResources()); configurationStoreContext.AddRange(Config.GetApiResources()); configurationStoreContext.SaveChanges(); } } catch (Exception e) { Console.WriteLine(e.Message); } Console.ReadLine(); }
The static Config class just adds the data like the IdentityServer4 examples.
Now the applications run using the configuration data stored in an Entity Framwork Core supported database.
Note:
This post shows how just the configuration data can be setup for IdentityServer4. To make it scale, you also need to implement the IPersistedGrantStore and CORS for each client in the database. A cache solution might also be required.
IdentityServer4 provides a full solution and example: IdentityServer4.EntityFramework
Links:
http://docs.identityserver.io/en/release/topics/deployment.html
https://damienbod.com/2016/01/07/experiments-with-entity-framework-7-and-asp-net-5-mvc-6/
https://docs.microsoft.com/en-us/ef/core/get-started/netcore/new-db-sqlite
https://docs.microsoft.com/en-us/ef/core/
http://docs.identityserver.io/en/release/reference/ef.html
https://github.com/IdentityServer/IdentityServer4.EntityFramework
https://elanderson.net/2017/07/identity-server-using-entity-framework-core-for-configuration-data/
http://docs.identityserver.io/en/release/quickstarts/8_entity_framework.html