0
IdentityServer4 & EntityFramework Core
Programming Server Side

How to configure IdentityServer4 to use EntityFramework Core with SQL Server as the storage mechanism

In this short walk-through I’ll show you how to move IdentityServer4’s configuration data (resources and clients) and operational data (tokens, codes, and consents) into a database in QuickApp. QuickApp uses the in-memory implementations of these and you have the option to move these data into a persistent store such as a db using EntityFramework Core.

This gets rid of the notification during build: “You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch to a different store implementation.”

QuickApp is a clean, easy-to-use, responsive Asp.net core/AngularX project template that has common application features such as login, user management, role management, etc. fully implemented for Rapid Application Development.
QuickApp is available for free on GitHub for both commercial and private use: https://github.com/emonney/quickapp

See this guide on readthedocs.io for more info on this topic.

  1. We start by adding IdentityServer4.EntityFramework Nuget package to our QuickApp project
    Add IdentityServer4.EntityFramework nuget package
  2. Then we head over to Startup.cs and from the ConfigureServices() method we’ll reconfigure IdentityServer to use Sql Server to save its data.
    We change the lines below:

    // Adds IdentityServer.
    services.AddIdentityServer()
    // The AddDeveloperSigningCredential extension creates temporary key material for signing tokens.
    // This might be useful to get started, but needs to be replaced by some persistent key material for production scenarios.
    // See http://docs.identityserver.io/en/release/topics/crypto.html#refcrypto for more information.
    .AddDeveloperSigningCredential()
    .AddInMemoryPersistedGrants()
    // To configure IdentityServer to use EntityFramework (EF) as the storage mechanism for configuration data (rather than using the in-memory implementations),
    // see https://identityserver4.readthedocs.io/en/release/quickstarts/8_entity_framework.html
    .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
    .AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
    .AddInMemoryClients(IdentityServerConfig.GetClients())
    .AddAspNetIdentity<ApplicationUser>()
    .AddProfileService<ProfileService>();

    To this:

    // Adds IdentityServer.
    services.AddIdentityServer()
      // The AddDeveloperSigningCredential extension creates temporary key material for signing tokens.
      // This might be useful to get started, but needs to be replaced by some persistent key material for production scenarios.
      // See http://docs.identityserver.io/en/release/topics/crypto.html#refcrypto for more information.
      .AddDeveloperSigningCredential()
      .AddConfigurationStore(options =>
      { 
        options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); 
      })
      .AddOperationalStore(options =>
      { 
        options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); 
         
        // this enables automatic token cleanup. this is optional. 
        options.EnableTokenCleanup = true; 
        options.TokenCleanupInterval = 30; 
      })
      .AddAspNetIdentity<ApplicationUser>()
      .AddProfileService<ProfileService>();

    This modification configures IdentityServer to use an SQL database and we pass in the connection string of the database we want to use. In this case we use the existing database in the project.

  3. Now we can add a new EntityFramework migrations to add IdentityServer’s tables to our database. We do this from the command line at the web project’s root:
    dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Migrations/IdentityServer/PersistedGrantDb
    dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Migrations/IdentityServer/ConfigurationDb
  4. Now that we have the migrations for the two stores in place, let’s write some code to apply these migrations at application startup and also seed the file based configurations into the database.
    Rather than creating another DatabaseInitializer class to seed our db with IdentityServer’s configurations I’ll simply inherit from the existing DatabaseInitializer class.
    The complete IdentityServerDBInitializer class looks like this:

    // ====================================
    // info@ebenmonney.com
    // www.ebenmonney.com/quickapp-standard
    // ====================================
    
    using DAL.Models;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using DAL.Core;
    using DAL.Core.Interfaces;
    using IdentityServer4.EntityFramework.DbContexts;
    using DAL;
    using IdentityServer4.EntityFramework.Mappers;
    
    namespace QuickApp.Standard
    {
        public class IdentityServerDbInitializer : DatabaseInitializer
        {
            private readonly PersistedGrantDbContext _persistedGrantContext;
            private readonly ConfigurationDbContext _configurationContext;
            private readonly ILogger _logger;
    
            public IdentityServerDbInitializer(
                ApplicationDbContext context,
                PersistedGrantDbContext persistedGrantContext,
                ConfigurationDbContext configurationContext,
                IAccountManager accountManager,
                ILogger<IdentityServerDbInitializer> logger) : base(context, accountManager, logger)
            {
                _persistedGrantContext = persistedGrantContext;
                _configurationContext = configurationContext;
                _logger = logger;
            }
    
    
            override public async Task SeedAsync()
            {
                await base.SeedAsync().ConfigureAwait(false);
    
                await _persistedGrantContext.Database.MigrateAsync().ConfigureAwait(false);
                await _configurationContext.Database.MigrateAsync().ConfigureAwait(false);
    
    
                if (!await _configurationContext.Clients.AnyAsync())
                {
                    _logger.LogInformation("Seeding IdentityServer Clients");
    
                    foreach (var client in IdentityServerConfig.GetClients())
                    {
                        _configurationContext.Clients.Add(client.ToEntity());
                    }
                    _configurationContext.SaveChanges();
                }
    
                if (!await _configurationContext.IdentityResources.AnyAsync())
                {
                    _logger.LogInformation("Seeding IdentityServer Identity Resources");
    
                    foreach (var resource in IdentityServerConfig.GetIdentityResources())
                    {
                        _configurationContext.IdentityResources.Add(resource.ToEntity());
                    }
                    _configurationContext.SaveChanges();
                }
    
                if (!await _configurationContext.ApiResources.AnyAsync())
                {
                    _logger.LogInformation("Seeding IdentityServer API Resources");
    
                    foreach (var resource in IdentityServerConfig.GetApiResources())
                    {
                        _configurationContext.ApiResources.Add(resource.ToEntity());
                    }
                    _configurationContext.SaveChanges();
                }
            }
        }
    }

    Note that we use the override keyword for our SeedAsync() method. That means we have to add the virtual keyword to our base implementation in the file DatabaseInitializer.cs ( i.e. virtual public async Task SeedAsync()).
    Also we now have additional DbContexts in our solution (i.e. IdentityServer’s PersistedGrantDbContext and ConfigurationDbContext), so we need to modify the constructor of our own ApplicationDbContext class and restrict the parameter it can accept to the type DbContextOptions<ApplicationDbContext>, else dependency injection will pass in the wrong DbContextOptions that belongs to one of the other DbContext.
    We do this by changing the line public ApplicationDbContext(DbContextOptions options) : base(options) to public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)

  5. Then finally from Startup.cs we tell Dependency Injection to use our new seeding class when creating instances of IDatabaseInitializer (IDatabaseInitializer is called from Program.csmain() method at application startup to initialize and seed the database).
    I.e. we change services.AddTransient<IDatabaseInitializer, DatabaseInitializer>(); to services.AddTransient<IDatabaseInitializer, IdentityServerDbInitializer>();

And that’s all we need to do to switch from an in-memory store to an EntityFramework Core store. You’ll find the completed project in a folder named “kitchen-sink” in the QuickApp Pro and QuickApp Standard download packages.

You Might Also Like...

1 Comment

  • Reply
    George
    May 17, 2023 at 11:10 am

    This manual is not suitable for the latest version of the Quickapp.
    1)Do not insatll IdentityServer4.EntityFramework package
    2)Update your Program.cs file

    builder.Services.AddIdentityServer(options =>
    {
    options.IssuerUri = authServerUrl;
    })
    .AddDeveloperSigningCredential()
    .AddConfigurationStore(options =>
    {
    options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sql =>
    sql.MigrationsAssembly(migrationsAssembly));
    })
    .AddOperationalStore(options =>
    {
    options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sql =>
    sql.MigrationsAssembly(migrationsAssembly));

    // this enables automatic token cleanup. this is optional.
    options.EnableTokenCleanup = true;
    options.TokenCleanupInterval = 3600;
    })
    .AddAspNetIdentity()
    .AddProfileService();

    3) Create migrations as shown above in the original instruction
    4)Add the following code to the IdentityServerDbInitializer.cs

    if (!await _configurationContext.ApiScopes.AnyAsync())
    {
    _logger.LogInformation(“Seeding IdentityServer API Scopes”);
    foreach (var scopes in IdentityServerConfig.GetApiScopes())
    {
    _configurationContext.ApiScopes.Add(scopes.ToEntity());
    }
    _configurationContext.SaveChanges();
    }

Leave a Reply