Article guideContents, topics, tags, and RSS
IdentityServer3 projects shared a set of setup tasks, so I created this starter kit to keep those pieces in one place.
The original project combined IdentityServer3, IdentityManager for users, an administration UI for clients, ASP.NET Identity, and Entity Framework persistence. Those projects are now represented by the archived IdentityServer organization.
The tutorial remains useful for understanding or maintaining that specific 2017 stack. For new development, start with a supported OpenID Connect provider and the current Duende IdentityServer documentation.
Setting up the project
The original setup started with an empty ASP.NET Web Application as the host.

Because no default template was needed, the project used the Empty template.

Next, the development environment was configured for HTTPS. In the project properties, the Web → Project URL was set to an https:// URL.

Click Create Virtual Directory to enable the URL.

The following Web.config setting enabled the built-in assets.
<configuration>
...
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
</configuration>
If the IdentityServer welcome page appears without CSS, JavaScript, or images, check the runAllManagedModulesForAllRequests setting.
The project was then ready for its packages.
Setting up IdentityServer 3 core features
Installing basic packages
The project installed IdentityServer3 itself, IdentityServer3.AspNetIdentity for ASP.NET Identity integration, IdentityServer3.EntityFramework for SQL Server persistence, Microsoft.AspNet.Identity.EntityFramework for the identity entities, and Microsoft OWIN packages for startup.
Install-Package IdentityServer3
Install-Package IdentityServer3.AspNetIdentity
Install-Package IdentityServer3.EntityFramework
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.AspNet.Identity.EntityFramework
For the historical self-hosted option, the application could use Microsoft.Owin.Host.HttpListener and Microsoft.Owin.Hosting instead of Microsoft.Owin.Host.SystemWeb.
Setting up some prerequisites
Constants
Constants.cs collected the connection-string name used throughout the starter kit and the core IdentityServer endpoint.
// <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
IdentityServer also needed a certificate for signing tokens. The original sample embedded a test certificate in the project. That is acceptable only for local experimentation; use managed, protected key material in a real environment.
The following helper reads that embedded file as a stream and creates an 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
The user model extended IdentityUser with first and last names while inheriting the standard identity fields.
// <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
The database context inherited from IdentityDbContext and used the custom user model with the types supplied by 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
With the context in place, the project added UserStore and RoleStore implementations for creating, finding, updating, and deleting users and roles.
// <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
ASP.NET Identity uses ClaimsIdentity. A custom ClaimsIdentityFactory mapped the first and last names from the user model into 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
The project then implemented AspNetIdentityUserService from IdentityServer3.AspNetIdentity so IdentityServer could use the custom user model.
// <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
The default setup seeded scopes, a test user, and a client only when the corresponding tables were empty. The credentials in this historical sample are deliberately simple and must not be reused outside a disposable local environment.
// <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
The remaining step mapped the IdentityServer core through an IAppBuilder extension that accepts the signing certificate. It configured Entity Framework, registered the stores and user service, ran the seed setup, and called UseIdentityServer.
// <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;
}
}
}
Finally, the OWIN startup called the core mapping extension.
// <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!
In the original project, https://localhost:44300/ids displayed the welcome screen and the seeded administrator could sign in.
The complete historical starter kit remains available in zarxor/IdentityServer3.StarterKit.
