Pages

Monday, May 8, 2017

IdentityServer3 with PKCE Part 3 - Persist IdentityServer Configuration

This series simulates a native application accessing a protected Web API resource, using OAuth2 via IdentityServer3. It demonstrates using Proof Key for Code Exchange (PKCE), and is in four parts:

  1. Build a simple authorization server, consumed by native application.
  2. Build a protected resource.
  3. Persist server configuration to database.
  4. Persist user data to database using Microsoft.Identity and SQL Server.

Important
This series does not create an OpenID Connect (OIDC) server. It is OAuth-only, since the PKCE specification doesn't require OIDC.

Where We're At

Until now, we've been using in-memory configuration data (clients and scopes). But we'd like a way to persist the configuration to a database.

IdentityServer and Entity Framework

References

Entity Framework support for Clients, Scopes, and Operational Data
Operational Data
Clients and Scopes
Caching results for client, scope, and user stores

Add these packages.

Install-Package IdentityServer3.EntityFramework -Project AuthAndApi

Add the App_Data folder. This is required if you want to use the connection string below.

Add a connection string for the configuration database. The sample uses SQL localDb 2016.

<connectionStrings>
    <add name="IdConfigDb" 
         connectionString="Server=(localDb)\MSSQLLocalDb;AttachDbFilename=|DataDirectory|IdConfig.mdb;Database=IdConfig;Integrated Security=true;" 
         providerName="System.Data.SqlClient" />
  </connectionStrings>

Add an IdentityServerConfiguration folder, and a Factory.cs class. The factory class takes care of setting the configuration database source, and also initially populating the database using our in-memory client and scope collections.

Note
This sample doesn't provide a way to edit configuration data. There is a NuGet package that adds web-based configuration administration to a project. I haven't tried it, but here's the link. IdentityServer3.Admin

Factory.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
//Added
using IdentityServer3.Core.Configuration;
using IdentityServer3.EntityFramework;
using IdentityServer3.Core.Models;
using AuthAndApi.IdOptions;
using AuthAndApi.IdUsers;

namespace AuthAndApi.IdentityServerConfiguration
{
    public class Factory
    {
        public static IdentityServerServiceFactory Configure(string optionsConnectionString)
        {
            //Configure persisting IdentityServer operational services
            //The database will be created if needed. The default schema is being used.
            var efConfig = new EntityFrameworkServiceOptions
            {
                ConnectionString = optionsConnectionString
            };

            // these two calls just pre-populate the test DB from the in-memory config.
            // clf note: there's probably a better way to do this for a production app.
            ConfigureClients(Clients.Get(), efConfig);
            ConfigureScopes(Scopes.Get(), efConfig);

            var factory = new IdentityServerServiceFactory();
            factory.RegisterConfigurationServices(efConfig);
            factory.RegisterOperationalServices(efConfig);
            factory.ConfigureClientStoreCache();
            factory.ConfigureScopeStoreCache();
            //Still using in-memory users for now
            factory.UseInMemoryUsers(Users.Get());

            return factory;
        }

        public static void ConfigureClients(IEnumerable<Client> clients, EntityFrameworkServiceOptions options)
        {
            using (var db = new ClientConfigurationDbContext(options.ConnectionString, options.Schema))
            {
                if (!db.Clients.Any())
                {
                    foreach (var c in clients)
                    {
                        var e = c.ToEntity();
                        db.Clients.Add(e);
                    }
                    db.SaveChanges();
                }
            }
        }

        public static void ConfigureScopes(IEnumerable<Scope> scopes, EntityFrameworkServiceOptions options)
        {
            using (var db = new ScopeConfigurationDbContext(options.ConnectionString, options.Schema))
            {
                if (!db.Scopes.Any())
                {
                    foreach (var s in scopes)
                    {
                        var e = s.ToEntity();
                        db.Scopes.Add(e);
                    }
                    db.SaveChanges();
                }
            }
        }
    }
}

Update Startup.cs, replacing the previous IdentityServer configuration that explicitly configured the Factory with in-memory data, with the new call to Factory.Configure.

            //Configure IdentityServer for issuing authentication tokens
            var options = new IdentityServerOptions
            {
                Factory = Factory.Configure("IdConfigDb"),
                //SSL MUST be used in production
#if DEBUG
                RequireSsl = false
#endif
            };
            app.UseIdentityServer(options);

ASIDE!

Running the app at this point, I got an error in Startup.cs that I was missing a dependency.

 

2017-05-06_134432

When I installed the package IdentityServer3.EntityFramework, Newtonsoft.Json version 8 was uninstalled, because the package requires version 9 or greater. The package installer output confirms this.

Successfully uninstalled 'Newtonsoft.Json.8.0.3' from AuthAndApi
Successfully installed 'Newtonsoft.Json 9.0.1' to AuthAndApi

No package requires version 8 or lower, so what gives? For some reason, the solution's web.config assembly binding section wasn't updated properly. It still had this:

     <dependentAssembly>
       <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
       <bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
     </dependentAssembly>

You could manually change the max version number to 9.0.0.0, reinstall the Newtonsoft package which will do it for you.

Update-Package -Reinstall Newtonsoft.Json -Project AuthAndApi

When I run the application, it behaves exactly as before. Checking in the App_Data folder, I see my database was created.

2017-05-06_140602

One difference, though. After the first time running the app, I no longer was prompted to authorize the scope. This might be related to these factory settings:

factory.ConfigureClientStoreCache();
factory.ConfigureScopeStoreCache();

Wrap Up

Our server's configuration is now being persisted to a database. The only thing (for this sample!) left is to persist the user's credentials and claims.

No comments:

Post a Comment