IdentityServer 3 Starter kit - Part 1: Installing IdentityServer 3, ASP.NET Identity and Entity Framework

identityserver3-starterkit-header

When setting up a new instance of IdentityServer3 some things that you do, you do for every project.

This will be a small tutorial / series following my steps when setting up a simple start kit for IdentityServer3 that also contains IdentityManager for users and admin for clients.

This is to have as a starting point to quickly get started with IdentityServer when setting up a new projects. This can also be used as a guide for getting started and setting up IdentityServer 3 from scratch and as a guide to understanding all the lose parts.

Setting up the project

We'll start by creating an empty ASP.NET Web Application, that will be the host application for the project.

1-new-project

Since we don't need any default template for the project we can choose to create an Empty project.

2-new-project-blank

Next up, is setting up a development environment using https. For this, we need to open the properties for the project and set the Web -> Project url to an https:// url

3-project-url

Remember to click the Create Virtual Directory button to enable the url.

4-project-url

To be able to handle the built in assets and more we need to enable a setting in the Web.config file.

<configuration>
  ...
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true" />
  </system.webServer>
</configuration>

If when you start your IdentityServer you encounter a welcome page without any valid css, js or images you've probably missed the "RunAllManagedModulesForAllRequests" setting.

Now we are ready to continue and start installing packages.

Setting up IdentityServer 3 core features

Installing basic packages

After we are done with project setup we'll install a few packages, IdentityServer3 to install the base for IdentityServer, IdentityServer3.AspNetIdentity to give support for ASP.NET Identity, IdentityServer3.EntityFramework to enable to have persistent data from an SQL Server using EntityFramework, Microsoft.AspNet.Identity.EntityFramework to create the ASP.NET Identity Entities and Microsoft.Owin to be able to setup IdentityServer at startup

Install-Package IdentityServer3 
Install-Package IdentityServer3.AspNetIdentity 
Install-Package IdentityServer3.EntityFramework
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.AspNet.Identity.EntityFramework

If you want the server to be self-hosted you can create an console application and install the packages Microsoft.Owin.Host.HttpListner and Microsoft.Owin.Hosting instead of Microsoft.Owin.Host.SystemWeb

Setting up some prerequisites

Constants

First we are going to setup Constants.cs, a file that will be used to contain some constants through the process. Here we define a name for the connection string that will be used across the starter kit and also the core endpoint for IdentityServer.

// <copyright file="Constants.cs">
//    2017 - Johan Boström
// </copyright>
 
namespace IdentityServer3.StarterKit
{
  public static class Constants
  {
    public const string ConnectionStringName = "AspId";
 
    public class Routes
    {
        public const string Core = "/ids";
        public const string IdMgr = "/idm";
        public const string IdAdm = "/ida";
    }
  }
}

Certificate

We also need to have a certificate that we can use to sign tokens etc. For test-purposes I've downloaded the test certificate from IdentityServers github, that can be found here. The certificate is then added to the project and the build action is changed to Embedded Resource.

Now we need to be able to read the certificate and this can be done by creating the class certificate and reading the file as a stream and then creating a X509Certificate2.

// <copyright file="Certificate.cs">
//    2017 - Johan Boström
// </copyright>

using System.IO;
using System.Security.Cryptography.X509Certificates;
 
namespace IdentityServer3.StarterKit.Config
{
    public static class Certificate
    {
        public static X509Certificate2 Get()
        {
            var assembly = typeof(Certificate).Assembly;
            using (var stream = assembly.GetManifestResourceStream("IdentityServer3.StarterKit.Config.idsrv3test.pfx"))
            // Should be the path to the embeded certificate
            {
                return new X509Certificate2(ReadStream(stream), "idsrv3test");
            }
        }
        
        private static byte[] ReadStream(Stream input)
        {
            var buffer = new byte[16 * 1024];
            using (var ms = new MemoryStream())
            {
                int read;
                while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
                    ms.Write(buffer, 0, read);
                return ms.ToArray();
            }
        }
    }
}

Database models

Now we can start setting up models to use for the user when storing data to the databases, this model can be used to keep extra information on the entries. On the user we will add a first name and a last name to be able to enter later. Other than that there are no need for more info right now since the model will inherit all it needs from IdentityUser.

// <copyright file="User.cs">
//    2017 - Johan Boström
// </copyright>
 
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace IdentityServer3.StarterKit.Models
{
  public class User : IdentityUser
  {
    public string FirstName { get; set; }
    public string LastName { get; set; }
  }
}

Context

Next up is to setup the database context, this is done by creating a class and inheriting from IdentityDbContext and use the user model that we created together with some models that come directly from Microsoft.AspNet.Identity.EntityFramework.

// <copyright file="Context.cs">
//    2017 - Johan Boström
// </copyright>

using IdentityServer3.StarterKit.Models;
using Microsoft.AspNet.Identity.EntityFramework;

namespace IdentityServer3.StarterKit.Db
{
    public class Context : IdentityDbContext<User, IdentityRole, string,IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
    {
        public Context(string connString)
            : base(connString)
        {
        }
    }
}

Stores

Now that we have the context in place we can create a UserStore and RoleStore that the system can use for creating, finding, updating and deleting users and stores. We will create them by inheriting from the stores located in Microsoft.AspNet.Identity.EntityFramework.

// <copyright file="UserStore.cs">
//    2017 - Johan Boström
// </copyright>
 
using IdentityServer3.StarterKit.Db;
using IdentityServer3.StarterKit.Models;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace IdentityServer3.StarterKit.Stores
{
  public class UserStore : UserStore<User, IdentityRole, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
  {
    public UserStore(Context context)
        : base(context)
    {
    }
  }
}


// <copyright file="RoleStore.cs">
//    2017 - Johan Boström
// </copyright>
 
using IdentityServer3.StarterKit.Db;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace IdentityServer3.StarterKit.Stores
{
  public class RoleStore : RoleStore<IdentityRole>
  {
    public RoleStore(Context context)
        : base(context)
    {
    }
  }
}

Managers

// <copyright file="UserManager.cs">
//    2017 - Johan Boström
// </copyright>
 
using IdentityServer3.StarterKit.Factories;
using IdentityServer3.StarterKit.Models;
using IdentityServer3.StarterKit.Stores;
using Microsoft.AspNet.Identity;
 
namespace IdentityServer3.StarterKit.Managers
{
  public class UserManager : UserManager<User, string>
  {
    public UserManager(UserStore store)
        : base(store)
    {
        ClaimsIdentityFactory = new ClaimsFactory();
    }
  }
}


// <copyright file="RoleManager.cs">
//    2017 - Johan Boström
// </copyright>
 
using IdentityServer3.StarterKit.Stores;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;

namespace IdentityServer3.StarterKit.Managers
{
    public class RoleManager : RoleManager<IdentityRole>
    {
        public RoleManager(RoleStore store)
            : base(store)
        {
        }
    }
}

Creating the ClaimsIdentity

AspNet Identity uses the ClaimsIdentity as the identity type, and for us to be able to create an instance of this from the user model we created before we need to setup a ClaimsIdentityFactory. With this we will be be able to extract the first- and last name that we added to the user model, and add these as possible claims.

// <copyright file="ClaimsIdentityFactory.cs">
//    2017 - Johan Boström
// </copyright>
 
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer3.StarterKit.Models;
using Microsoft.AspNet.Identity;
 
namespace IdentityServer3.StarterKit.Factories
{
    public class ClaimsIdentityFactory : ClaimsIdentityFactory<User, string>
    {
        public ClaimsIdentityFactory()
        {
            UserIdClaimType = Core.Constants.ClaimTypes.Subject;
            UserNameClaimType = Core.Constants.ClaimTypes.PreferredUserName;
            RoleClaimType = Core.Constants.ClaimTypes.Role;
        }

        public override async Task<ClaimsIdentity> CreateAsync(UserManager<User, string> manager, User user, string authenticationType)
        {
            var ci = await base.CreateAsync(manager, user, authenticationType);
 
            if (!string.IsNullOrWhiteSpace(user.FirstName))
                ci.AddClaim(new Claim("given_name", user.FirstName));

            if (!string.IsNullOrWhiteSpace(user.LastName))
                ci.AddClaim(new Claim("family_name", user.LastName));

            return ci;
        }
    }
}

User service

Now we just need an implementation of AspNetIdentityUserService that is part of the IdentityServer3.AspNetIdentity package that we installed before, so that it will use out new user model. And so that we can start putting all the lose parts together.

// <copyright file="UserService.cs">
//    2017 - Johan Boström
// </copyright>

using IdentityServer3.AspNetIdentity;
using IdentityServer3.StarterKit.Managers;
using IdentityServer3.StarterKit.Models;
 
namespace IdentityServer3.StarterKit.Services
{
    public class UserService : AspNetIdentityUserService<User, string>
    {
        public UserService(UserManager userManager)
            : base(userManager)
        {
        }
    }
}

Configuring IdentityServer

Setting up default scopes, users and clients

To get started we can setup some values that can be used as a starting point for the data that we can manipulate and add from. Theses values below will only be added if there are no other entities in the tables. For the scopes I decided to add the default scopes to the database, I added a user so that we have a user to login with and try the system. I also added a client that we can use to build from in later parts of the start kit series.

// <copyright file="DefaultSetup.cs">
//    2017 - Johan Boström
// </copyright>
 
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using IdentityServer3.Core.Models;
using IdentityServer3.EntityFramework;
using IdentityServer3.StarterKit.Db;
using IdentityServer3.StarterKit.Managers;
using IdentityServer3.StarterKit.Models;
using IdentityServer3.StarterKit.Stores;
using Microsoft.AspNet.Identity;
 
namespace IdentityServer3.StarterKit.Config
{
    public class DefaultSetup
    {
        public static void Configure(EntityFrameworkServiceOptions options)
        {
            using (var db = new ScopeConfigurationDbContext(options.ConnectionString, options.Schema))
            {
                if (!db.Scopes.Any())
                {
                    foreach (var s in StandardScopes.All)
                    {
                        var e = s.ToEntity();
                        db.Scopes.Add(e);
                    }
 
                    foreach (var s in StandardScopes.AllAlwaysInclude)
                    {
                        var e = s.ToEntity();
                        db.Scopes.Add(e);
                    }
 
                    db.SaveChanges();
                }
            }
 
            using (var db = new Context(options.ConnectionString))
            {
                if (!db.Users.Any())
                {
                    using (var userManager = new UserManager(new UserStore(db)))
                    {
                        var defaultUserPassword = "skywalker"; // Must be atleast 6 characters
                        var user = new User
                        {
                            UserName = "administrator",
                            FirstName = "Luke",
                            LastName = "Skywalker",
                            Email = "[email protected]",
                            EmailConfirmed = true
                        };
                        userManager.Create(user, defaultUserPassword);
                        userManager.AddClaim(user.Id,
                        new Claim(Core.Constants.ClaimTypes.WebSite, "https://www.johanbostrom.se/"));
                    }
 
                    db.SaveChanges();
                }
            }
 
            using (var db = new ClientConfigurationDbContext(options.ConnectionString, options.Schema))
            {
                if (!db.Clients.Any())
                {
                    var defaultHybridClient = new Client
                    {
                        ClientName = "Default Hybrid Client",
                        ClientId = "default.hybrid",
                        Flow = Flows.Hybrid,
                        ClientSecrets = new List<Secret>
                        {
                            new Secret("default.hybrid.password".Sha256())
                        },
                        AllowedScopes = new List<string>
                        {
                            Core.Constants.StandardScopes.OpenId,
                            Core.Constants.StandardScopes.Profile,
                            Core.Constants.StandardScopes.Email,
                            Core.Constants.StandardScopes.Roles,
                            Core.Constants.StandardScopes.Address,
                            Core.Constants.StandardScopes.Phone,
                            Core.Constants.StandardScopes.OfflineAccess
                        },
                        ClientUri = "https://localhost:44300/",
                        RequireConsent = false,
                        AccessTokenType = AccessTokenType.Reference,
                        RedirectUris = new List<string>(),
                        PostLogoutRedirectUris = new List<string>
                        {
                            "https://localhost:44300/"
                        },
                        LogoutSessionRequired = true
                    };
 
                    db.Clients.Add(defaultHybridClient.ToEntity());
                    db.SaveChanges();
                }
            }
        }
    }
}

Mapping, registration and configuration

Now all that is left is putting everything together and mapping the identityserver core. I decided to create an extensions for IAppBuilder that takes a certificate. Here we configure EntityFramework (EF) from the constants we setup, create a IdentityServerServiceFactory and register services based on the EF configurations. We also register the UserService service we created and run our default configuration setup. Finally it runs the UseIdentityServer extension with the factory and some debugging parameters.

// <copyright file="AppBuilderExtensions.cs">
//    2017 - Johan Boström
// </copyright>
 
using System.Security.Cryptography.X509Certificates;
using IdentityServer3.Core.Configuration;
using IdentityServer3.Core.Services;
using IdentityServer3.EntityFramework;
using IdentityServer3.StarterKit.Config;
using IdentityServer3.StarterKit.Db;
using IdentityServer3.StarterKit.Managers;
using IdentityServer3.StarterKit.Services;
using IdentityServer3.StarterKit.Stores;
using Owin;
 
namespace IdentityServer3.StarterKit.Extensions
{
    public static class AppBuilderExtensions
    {
        public static IAppBuilder MapCore(this IAppBuilder app, X509Certificate2 signingCertificate)
        {
            app.Map(Constants.Routes.Core, coreApp =>
            {
                var efConfig = new EntityFrameworkServiceOptions
                {
                    ConnectionString = Constants.ConnectionStringName
                };
 
                var factory = new IdentityServerServiceFactory();
 
                factory.RegisterConfigurationServices(efConfig);
                factory.RegisterOperationalServices(efConfig);
                factory.RegisterClientStore(efConfig);
                factory.RegisterScopeStore(efConfig);

                factory.Register(new Registration<UserManager>());
                factory.Register(new Registration<UserStore>());
                factory.Register(new Registration<Context>(resolver => new Context(Constants.ConnectionStringName)));

                factory.UserService = new Registration<IUserService, UserService>();

            DefaultSetup.Configure(efConfig);
 
                coreApp.UseIdentityServer(new IdentityServerOptions
                {
                    Factory = factory,
                    SigningCertificate = signingCertificate,
                    SiteName = "IdentityServer3 Starter Kit",
                    LoggingOptions = new LoggingOptions
                    {
                        EnableKatanaLogging = true
                    },
                    EventsOptions = new EventsOptions
                    {
                        RaiseFailureEvents = true,
                        RaiseInformationEvents = true,
                        RaiseSuccessEvents = true,
                        RaiseErrorEvents = true
                    }
                });
            });
 
            return app;
        }
    }
}

Adding the last bit of the puzzle, the Owin startup. I created a file called Startup.cs and just ran the core mapping extension for the IAppBuilder.

// <copyright file="Startup.cs">
//    2017 - Johan Boström
// </copyright>
 
using IdentityServer3.StarterKit;
using IdentityServer3.StarterKit.Config;
using IdentityServer3.StarterKit.Extensions;
using Microsoft.Owin;
using Owin;
 
[assembly: OwinStartup(typeof(Startup))]
 
namespace IdentityServer3.StarterKit
{
  public class Startup
  {
    public void Configuration(IAppBuilder app)
    {
      var certificate = Certificate.Get();
      app.MapCore(certificate);
    }
  }
}

Done!

Well, finally everything should be done. When running we should now be able to go to https://localhost:44300/ids and see the welcome screen. We should also be able to click the link to the permission page and login with our administrator user.

All the code examples and the complete starter kit project can be found over at my GitHub page, and since this is part of a series the code will be continuously updated with more features from later parts.

Share post
About Johan Boström

I'm a system architect, developer and solution expert, with experience in web-, application-, server- and windows service development with main focus on .NET / C#. Special skills with many kinds of CMS-tools, like EPiServer, Umbraco and Litium Studio.

Comments

Related posts

Create url and seo friendly slugs in c#

So, the other day I stumbled upon a discussion on StackOverflow about generating a url friendly slug. I found the problem quite interesting and decided to give it a go on solving this in .NET Core.

IdentityServer + OpenID Connect <3 EPiServer

A simple guide and example code for setting up the basics for working with EPiServer and OpenID Connect. There will be a few steps about IdentityServer as well but not a full setup guide.