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
Identity Server: Using Entity Framework Core for Configuration Data
http://docs.identityserver.io/en/release/quickstarts/8_entity_framework.html
[…] Using an EF Core database for the IdentityServer4 configuration data (Damien Bowden) […]
How can I do all of the above and extend my ApplicationDbContext – I seem to have multiple ApiResource tables being created and really want it on the single context. To do this I seem to have to derive ApplicationDbContext from IConfigurationDbContext but then what do my overrides have to do – it is very confusing having the same classes defined in two name spaces.
I have some questions and problems. Can you help me?
I don’t want to use “.AddInMemoryClients(ConfigureIdentityServer.GetClients())
.AddInMemoryIdentityResources(ConfigureIdentityServer.GetIdentityResources())”
so, I used your code, but I have this problem:
System.InvalidOperationException: ‘Unable to resolve service for type ‘IdentityServer.ConfigurationStore.ConfigurationStoreContext’ while attempting to activate ‘IdentityServer.ConfigurationStore.ResourceStore’.’
I don´t know what do I need to do?
Nice post! Really a nice list is presented by you. In my opinion, this list helps the programmer to understand the programming. I hope to you to present this type of post in the future also.
Thanks for sharing the information.
[…] a production app you can use EF Core to store these in a database (or old skool ADO.NET if you don’t like EF). I’ll leave it […]