This post is a continuation of a series of posts that follow my initial looking into using IdentityServer4 in ASP.NET Core with an API and an Angular front end. The following are the related posts.
Identity Server: Introduction
Identity Server: Sample Exploration and Initial Project Setup
Identity Server: Interactive Login using MVC
Identity Server: From Implicit to Hybrid Flow
Identity Server: Using ASP.NET Core Identity
Identity Server: Using Entity Framework Core for Configuration Data (this post)
Identity Server: Usage from Angular
This post is going to take the existing solution this series has been using and switch from using hard coded configuration data, found in the Config class of the Identity Application and moving it to a database using Entity Framework Core. As with prior entries, this will be following the intent of one of the official quick starts for Using Entity Framework Core for configuration data. This post is fairly different just because our example project already uses entity framework so a lot of steps can be skipped. The starting point of the code can be found here. All the changes in this post will be taking place in the Identity Application.
Identity Application
Thankfully the creators of IdentityServer provide a NuGet package that includes all the bits needed to move configuration data and operational data to Entity Framework Core. Start by added the following NuGet package.
- IdentityServer4.EntityFramework
Startup
With the above NuGet package installed the ConfigureServices function of the Startup class needs to be changed to tell IdentityServer the new place to pull data from. The following is the new version of the AddIdentityServer call updated to use Entity Framework Core.
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; services .AddIdentityServer() .AddTemporarySigningCredential() .AddAspNetIdentity<ApplicationUser>() .AddConfigurationStore(builder => builder .UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), options => options.MigrationsAssembly(migrationsAssembly))) .AddOperationalStore(builder => builder .UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), options => options.MigrationsAssembly(migrationsAssembly)));
Notice that the following have all been replaced by AddConfigurationStore and AddOperationalStore.
- AddInMemoryPersistedGrants
- AddInMemoryIdentityResources
- AddInMemoryApiResources
- AddInMemoryClients
The other thing of note is the migrationsAssembly and its usage via options.MigrationsAssembly. This is moving the management of the migrations from the assembly that the contexts are defined to the Identity Application. This is needed in this case since the two contexts in question are defined in a NuGet package.
Migrations
Now that the configuration is done for the new contexts migrations need to be added to them. As always there are two ways to handle this either via the Package Manager Console or from a command prompt. I am going to use the command prompt this round to match the IdentityServer docs. Run the following two commands from the same path as the Identity Application’s csproj file.
dotnet ef migrations add InitConfigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/Configuration dotnet ef migrations add InitPersistedGrant -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrant
This is the first time I have used the -o argument which controls where the migration is output and following the docs example I am putting the migrations that are for entities outside of the control of the application into a subdirectory. Speaking of the entities being outside of the control of the main application, this means anytime the NuGet package that contains the entity is updated a check will need to be made to see if new migrations are needed.
Database Migrations and Seed Data
Since the DbContext classes that need migrations run are outside of the control our application if automatic migrations must be handled in a different way than with the identity-related context used previously in this series. Following the official docs, I am going to create an InitializeDatabase function that will apply any needed migrations as well as add seed data. To do this I am adding a new IdentityServerDatabaseInitialization class in the Data/IdentityServer directory. The following is the full class.
public static class IdentityServerDatabaseInitialization { public static void InitializeDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices .GetService<IServiceScopeFactory>() .CreateScope()) { PerformMigrations(serviceScope); SeedData(serviceScope); } } private static void PerformMigrations(IServiceScope serviceScope) { serviceScope.ServiceProvider .GetRequiredService<ConfigurationDbContext>() .Database .Migrate(); serviceScope.ServiceProvider .GetRequiredService<PersistedGrantDbContext>() .Database .Migrate(); } private static void SeedData(IServiceScope serviceScope) { 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(); } } }
The InitializeDatabase takes an IApplicationBuilder in order to be able to control the lifetime of the two DbContext classes needed. Normally this wouldn’t be needed and the lifetime would be controlled automatically, but since this code is being called from the Startup class instead of during a request (which is how the DI system does auto scoping) the scope is being created by the app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope() call.
The PerformMigrations function pulls the two DbContext objects from the container and applies migrations. Finally in SeedData if the DbSets don’t already contain data then the seed data is pulled from the Config class and saved to the database.
Back to Startup
In the Configure function of the Startup class add the following call to make sure migrations and seed data are run when the application starts.
IdentityServerDatabaseInitialization.InitializeDatabase(app);
Wrapping up
With the above changes, the Identity Application is now using the database for all its persistence. The missing bits are of course UI to manage the related data, but those can be built out as needed. The code in its completed state can be found here.
The next steps for this project will be utilizing IdentityServer from Angular in the Client Application instead of the temporary IdentityController that has had to be used in all the examples so far.
Great article. What is the correct procedure for updating the configuration data?
I used this exact technique to create and seed my database initially and it worked just fine. I then realised that there were a couple of things I wanted to change before going live (token lifetime, secret etc) but I can’t work out a way of doing it using EF. If I rerun the code it obviously skips the population due to the !context.Clients.Any(). I’ve tried commenting out this check and changing context.Clients.Add to context.Clients.Update but I always get errors relating to primary keys etc.
Any ideas?
I bet the part you are missing when trying to update is using EF to select your data first. That should allow the change tracker to see the record as updated.
When I was testing I ended up just deleting the database and starting over but guessing that isn’t an option for you.
Another option is to edit the tables using something like SQL server management studio.
Thanks for your reply Eric. I could delete the database and start over if I needed to as we’re not running live yet but I’d rather set it up “correctly” for the future. My knowledge of EF is very limited so I might take a look at the subject as a whole and try out your suggestion. Cheers.
In IdentityServer4 sample in config.cs you have code to add Client, IdentityResources, etc, I want to know how I can add IdentityClaims also. Because only find option to add Clients, IdentityResources, ApiResources only. How it can be achieved, any ideas? Thanks
Try looking at the Profile Service? It looks like it is pulling claims so hopefully, it will lead you to where there are stored. http://docs.identityserver.io/en/latest/reference/profileservice.html