IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core 2 which can be used to manage authentication for web applications. In my case I wanted to set up OAuth 2.0 authentication using a SQL backend for an API, this isn’t too tricky when you know what you’re doing but took me a little while to figure out initially.

The quickstart guide for using IdentityServer4 with EntityFramework is here, this contains most of the details required and the below code is based on this with additions for API authentication and securing the tokens with a certificate.

IdentityService

First off create an empty ASP.NET Core Web application and install the following NuGet packages.


IdentityServer4
IdentityServer4.EntityFramework

Once installed update the Startup.cs file for the project.

Startup.cs


public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        const string connectionString = @"SQL CONNECTION STRING";
        var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

        // configure identity server with in-memory stores, keys, clients and resources
        services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            // this adds the config data from DB (clients, resources)
            .AddConfigurationStore(options =>
            {
                options.ConfigureDbContext = builder =>
                    builder.UseSqlServer(connectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
                options.DefaultSchema = "IdentityService";
            })
            // this adds the operational data from DB (codes, tokens, consents)
            .AddOperationalStore(options =>
            {
                options.ConfigureDbContext = builder =>
                    builder.UseSqlServer(connectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
                options.DefaultSchema = "IdentityService";

                // this enables automatic token cleanup. this is optional.
                options.EnableTokenCleanup = true;
                options.TokenCleanupInterval = 30;
            });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // this will do the initial DB population
        InitializeDatabase(app);

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseIdentityServer();
    }

    private void InitializeDatabase(IApplicationBuilder app)
    {
        using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
        {
            serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

            var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
            context.Database.Migrate();
        }
    }
}

This sets the SQL schema of the IdentityServer4 tables to be “IdentityService”.

To create the migration files necessary to create the Entity Framework SQL tables run the following commands at the command line scoped to your project.


dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb

The InitializeDatabase method in the Startup.cs code then creates the required SQL tables from these migration files at startup.

This is enough to create a basic instance of IdentityServer, however I also wanted to authenticate against an SSL certificate and add in some logging using Serilog so then updated Startup.cs to the following.

Startup.cs


public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        string migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

        #region X509
        X509Certificate2Collection certCollection = null;
        X509Store store = null;
        X509Certificate2 SigningCertificate = null;
        string certThumb = Configuration["CertificateValidation:Thumbprint"];
        store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        Log.Debug("STORENAME: " + store.Name);
        Log.Debug("STORELOCATION: " + store.Location);
        store.Open(OpenFlags.ReadOnly);

        certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, certThumb, false);
        Log.Debug("CERTS FOUND: " + certCollection.Count.ToString());
        foreach (X509Certificate2 x509 in certCollection)
        {
            try
            {
                Log.Debug("FOUND THUMBPRINT: " + x509.Thumbprint);
                Log.Debug("SEARCHED THUMBPRINT: " + certThumb);
                if (x509.Thumbprint.ToUpper() == certThumb.ToUpper())
                {
                    SigningCertificate = x509;
                    break;
                }
            }
            catch (CryptographicException ex)
            {
                Log.Error(ex.Message);
            }
        }
        #endregion

        try
        {
            if (SigningCertificate != null)
            {
                Log.Information("SIGNING CERTIFICATE AQUIRED WITH THUMBPRINT:  \"" + certThumb + "\"");
                services.AddIdentityServer()
                .AddSigningCredential(SigningCertificate) // Use this to add trusted certificate or rsa key
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration["ConnectionStrings:IdentityServiceConnection"]);
                    options.DefaultSchema = "IdentityService";
                }
                )
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration["ConnectionStrings:IdentityServiceConnection"], sql => sql.MigrationsAssembly(migrationsAssembly));
                    options.DefaultSchema = "IdentityService";
                }
                );
            }
            else
            {
                Log.Warning("UNABLE TO AQUIRE SIGNING CERTIFICATE WITH THUMBPRINT:  \"" + certThumb + "\" USING TEMPORARY SIGNING CREDENTIAL");
                services.AddIdentityServer()
                .AddDeveloperSigningCredential() // Use this for test purposes only
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration["ConnectionStrings:IdentityServiceConnection"]);
                    options.DefaultSchema = "IdentityService";
                }
                )
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration["ConnectionStrings:IdentityServiceConnection"], sql => sql.MigrationsAssembly(migrationsAssembly));
                    options.DefaultSchema = "IdentityService";
                }
                );
            }
        }
        catch (Exception ex)
        {
            Log.Error(ex.Message);
        }

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        app.UseIdentityServer();
        app.UseHttpsRedirection();
        app.UseMvc();
    }
}

Deployment

To get this working in Azure it needs to be deployed as a web application with access to the SQL backend which I also hosted in Azure. Once deployed assign it a custom domain name and add an SSL certificate to secure the site. To allow this SSL certificate to be loaded by the web application it thumbprint needs to be added to the application settings (I also added it to the appsettings.json).


WEBSITE_LOAD_CERTIFICATES: YOUR_SSL_THUMBRINT

Leave a Reply

Your email address will not be published. Required fields are marked *