Configure IdentityServer with Entity Framework (OIDC Part 6)

Share on facebook
Share on google
Share on twitter
Share on linkedin

In this post, we are going to build upon our IdentityServer setup with ASP.NET Core Identity for user management by moving the previously hardcoded IdentityServer configuration data to the database. This enables dynamic change of how IdentityServer is configured instead of needed a rebuild of the server for every configuration change. For this, we are gonna use Entity Framework and are going to write a seed script that takes the Config.cs configuration data and populates the database with it. As in the last post, this post only requires changes to the authorization server – the client app and the resource API stay the same.

The OpenID connect with IdentityServer4 and Angular series

This series is learning you OpenID connect with Angular in these parts:

AuthorizationServer – Setup IdentityServer configuration management with Entity Framework

We are so fortunate that IdentityServer has a package on Nuget that gives us DbContexts, that we are using for creating the database for saving IdentityServer configuration data. We install the package on Nuget called:

IdentityServer4.EntityFramework

The database is updated with:

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 where we persist OpenID connect configuration data such as ApiResources, claims, Clients and Grants etc.

We are not gonna hardcode the configuration data anymore, so we will now delete the following middleware from the Startup.cs file in the AuthorizationServer project:

  • AddInMemoryPersistedGrants
  • AddInMemoryIdentityResources
  • AddInMemoryApiResources
  • AddInMemoryClients

Using the AddConfigurationStore middleware we are going to setup the Startup.cs as:

var migrationsAssembly = typeof (Startup).GetTypeInfo ().Assembly.GetName ().Name;
services.AddIdentityServer ()
    .AddDeveloperSigningCredential ()
    .AddAspNetIdentity<ApplicationUser> ()
    .AddConfigurationStore (options => {
        options.ConfigureDbContext = builder =>
            builder.UseSqlServer (Configuration.GetConnectionString ("DefaultConnection"),
                db => db.MigrationsAssembly (migrationsAssembly));
    })
    .AddOperationalStore (options => {
        options.ConfigureDbContext = builder =>
            builder.UseSqlServer (Configuration.GetConnectionString ("DefaultConnection"),
                db => db.MigrationsAssembly (migrationsAssembly));
    });

Here we are setting up an Configuration store with AddConfigurationStore and an OpertionalStore with AddOperationalStore. This works by only providing it with the SqlServer connection string and the migrationsassembly.

Notice that we need to provide the assemblyName for it to locate the migrations assembly. With this, we setup IdentityServer to lookup configuration data in the database.

The IdentityServer config data is now loaded from the Config.cs file into the database with a seed script:

public class SeedData {
    public static void EnsureSeedData (IServiceProvider serviceProvider) {
        Console.WriteLine ("Seeding database...");
        PerformMigrations (serviceProvider);

        EnsureSeedData (serviceProvider.GetRequiredService<ConfigurationDbContext> ());
        Console.WriteLine ("Done seeding database.");
    }

    private static void PerformMigrations (IServiceProvider serviceProvider) {
        serviceProvider.GetRequiredService<ApplicationDbContext> ().Database.Migrate ();
        serviceProvider.GetRequiredService<ConfigurationDbContext> ().Database.Migrate ();
        serviceProvider.GetRequiredService<PersistedGrantDbContext> ().Database.Migrate ();
    }

    private static void EnsureSeedData (ConfigurationDbContext context) {
        if (!context.Clients.Any ()) {
            Console.WriteLine ("Clients being populated");
            foreach (var client in Config.GetClients ().ToList ()) {
                context.Clients.Add (client.ToEntity ());
            }
            context.SaveChanges ();
        } else {
            Console.WriteLine ("Clients already populated");
        }

        if (!context.IdentityResources.Any ()) {
            Console.WriteLine ("IdentityResources being populated");
            foreach (var resource in Config.GetIdentityResources ().ToList ()) {
                context.IdentityResources.Add (resource.ToEntity ());
            }
            context.SaveChanges ();
        } else {
            Console.WriteLine ("IdentityResources already populated");
        }

        if (!context.ApiResources.Any ()) {
            Console.WriteLine ("ApiResources being populated");
            foreach (var resource in Config.GetApiResources ().ToList ()) {
                context.ApiResources.Add (resource.ToEntity ());
            }
            context.SaveChanges ();
        } else {
            Console.WriteLine ("ApiResources already populated");
        }
    }
}

This seed script is called from program.cs with:

...
using (var scope = host.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    try
    {
        SeedData.EnsureSeedData(services);

    }
    catch (Exception ex)
    {
        var logger = services.GetRequiredService<ILogger<Program>>();
        logger.LogError(ex, "An error occurred while migrating and seeding the database.");
    }
}
...

and adds or updates the configuration data in the database when the AuthorizationServer is run.

AuthorizationServer/Program.cs

If we run the authorization server seed script and look at the SQL server explorer, we can now see that we have created the following tables:

Running it all

As usual, we can select the AuthorizationServer, AppClient, and ResourceAPI and run them in Visual Studio. The app should run like before, except we can change configuration data dynamically now by changing the database tables containing the IdentityServer configuration data. On the first run we run our seed methods that run our migrations and seed our tables with the IdentityServer configuration data.

For example we can see our client now in the Clients table:

Wrapping up

In this post, we made the IdentityServer configuration dynamic by using the IdentityServer Entity Framework library to store OpenID connect configuration data in the database. This created new tables for storing the IdentityServer configuration data and we seeded these with a seed script, running upon start.

In the next post, we are going to implement the whole setup with Angular application in AppClient, using an implicit flow client on the Authorization server and being able to access authorized data from ResourceApi.

Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Related Posts and Comments

How to Handle Errors in a Reactive Angular App

In this post, we will cover how to handle errors in a reactive Angular app. To provide a good user experience you should always let your users know what state your application is in. That includes showing a loading spinner when it’s loading and showing error messages if there are any errors. It is a

Read More »

How to Set Up Git Hooks in an Nx Repo

Git hooks can be used to automate tasks in your development workflow. The earlier a bug is discovered, the cheaper it is to fix (and the less impact it has). Therefore it can be helpful to run tasks such as linting, formatting, and tests when you are e.g. committing and pushing your code, so any

Read More »

The Stages of an Angular Architecture with Nx

Long gone are the times when the frontend was just a dumb static website. Frontend apps have gotten increasingly complex since the rise of single-page application frameworks like Angular. It comes with the price of increased complexity and the ever-changing frontend landscape requires you to have an architecture that allows you to scale and adapt

Read More »

The Best Way to Use Signals in Angular Apps

Since Angular 16, Angular now has experimental support for signals and there is a lot of confusion in the community about whether this is going to replace RxJS or how it should be used in an app in combination with RxJS. This blog post sheds some light on what I think is the best way

Read More »

High ROI Testing with Cypress Component Testing

Testing is one of the most struggled topics in Angular development and many developers are either giving up testing altogether or applying inefficient testing practices consuming all their precious time while giving few results in return. This blog post will change all this as we will cover how I overcame these struggles the hard way

Read More »