Getting Started with IdentityServer 4

Identity Server

Identity Server 4 is the newest iteration of IdentityServer, the popular OpenID Connect and OAuth Framework for .NET, updated and redesigned for ASP.NET Core and .NET Core. In this article we are take a quick look at why IdentityServer 4 exists, and then dive right in and create ourselves a working implementation from zero to hero.

IdentityServer 3 vs IdentityServer 4


A popular phrase going at the moment is 'conceptually compatible' but this rings true for Identity Server 4. The concepts are the same, it is still an OpenID Connect provider built to spec, however most of its internals and extensibility points have changed. When we integrate a client application with IdentityServer, we are not integrating to an implementation. Instead we are integrating using the OpenID Connect or OAuth specifications. This means any application that currently works with IdentityServer 3 will work with IdentityServer 4.

Identity Server is designed to run as a self-hosted component, which was difficult to achieve with ASP.NET 4.x with MVC still being tightly coupled to IIS, and System.Web, resulting in an internal view engine served up by the katana component. With Identity Server 4 running on ASP.NET Core, we can now use any UI technology and host IdentityServer in any environment ASP.NET Core can run in. This also means we can now integrate with existing login forms/systems, allowing for in place upgrades.

The Identity Server IUserService that was used to integrate your user store is also gone now, replaced with a new user store abstraction in the form of IProfileService and IResourceOwnerPasswordValidator.

IdentityServer 3 isn’t going anywhere, just as the .NET Framework isn’t going anywhere. Just as Microsoft has shifted most active development to .NET Core (see Katana and ASP.NET Identity), I imagine IdentityServer will eventually do the same, but we are talking about OSS here and whilst the project stays that way it will always be open to PRs for bug fixes and relevant new features. I for one won’t be abandoning it any time soon and commercial support will continue.

At the time of writing IdentityServer 4 as in RC1, IdentityServer 3 was at v2.5.3 with another major release (v3.0.0) planned for the future.

IdentityServer4 targets .NET standard 1.4, meaning it can target either .NET core or the .NET framework, although this article will target .NET Core only.

You can read more about the reasoning behind IdentityServer 4 in the IdentityServer 4 announcement post by Dominick Baier.

Implementing IdentityServer4 on ASP.NET Core and .NET Core

For our initial implementation we’ll use the In-Memory services reserved for demos and lightweight implementations. Later in the article we will switch to entity framework for a more realistic representation of a production instance of IdentityServer.

Before starting this tutorial, please ensure you are using the latest version of ASP.NET Core and the .NET Core tooling. When creating this tutorial I used Visual Studio 2015 Update 3.

To start with we’ll need a new ASP.NET Core project that uses .NET Core (in VS see 'ASP.NET Core Web Application (.NET Core)'). You’ll want to use the Empty template with no authentication.

Before we start coding, switch the project URL to HTTPS. There’s no scenario where you should be running an authentication service without TLS. Assuming you are using IIS Express, you can do this by opening up the properties of your project, entering the Debug tab and clicking 'Enable SSL'. Whilst we are here, you should make the generated HTTPS URL your App URL, so that when we run the project we start off on the right page.

If you experience certificate trust issues when using the IIS Express development certificate for localhost, try following the steps in this article. If you find issues with this approach, feel free to switch to self-hosted mode (instead of IIS Express, run using your project's namespace).

To start we need to add the following to our project.json or install via nuget (version at time of writing is 1.0.0-rc1-update2):


Now to our Startup class to start registering dependencies and wiring up services. For this tutorial we are going to use the AddIdentityServer builder extension, as opposed to the AddDeveloperIdentityServer you may see in other tutorials. We are doing this so that we can talk through some of the concepts this extension will initially hide from you and because we will eventually replace services quickstart uses when we move away from in-memory stores.

In your ConfigureServices method add the following to register the minimum required dependencies:

    .AddInMemoryClients(new List<Client>())
    .AddInMemoryScopes(new List<Scope>())
    .AddInMemoryUsers(new List<InMemoryUser>())

And then in your Configure method add the following to add the IdentityServer middleware to the HTTP pipeline:


What we have done here is registered IdentityServer in our DI container using AddIdentityServer, set all tokens/grants to be stored in memory using AddInMemoryStores, used a demo signing certificate with SetTemporarySigningCredential, and again used in-memory stores for our clients, scopes and users. We will add actual clients, scopes and users shortly.

We can actually run IdentityServer already, it might have no UI, not support any scopes and have no users, but you can already start using it! Check out the OpenID Connect Discovery Document at /.well-known/openid-configuration.

OpenID Connect Discovery Document

The OpenID Connect Discovery Document is available on every OpenID Connect provider at this well known endpoint (as per the spec). This document contains information such as the location of various endpoints (e.g. the token endpoint and the end session endpoint), the grant types the provider supports, the scopes it can provide, and so on. By having this standardised document, we open up the possibility of automatic integration.

You can read more on the OpenID Connect Discovery Document in the OpenID Connect Discovery 1.0 specification.

Signing Certificate

A signing certificate is a dedicated certificate used to sign tokens, allowing for client applications to verify that the contents of the token have not been altered in transit. This involves a private key used to sign the token and a public key to verify the signature. This public key is accessible to client applications via the jwks_uri in the OpenID Connect discovery document.

When you go to create and use your own signing certificate, feel free to use a self-signed certificate. This certificate does not need to be issued by a trusted certificate authority.

Now that we have IdentityServer up and running let's add some data to it.

Clients, Scopes and Users

First we need to have a store of Client applications that are allowed to use IdentityServer, as well the Scopes that these clients can use and the Users that allowed to authenticate on them.

We are currently using the InMemory stores and these stores accept a collection of their respective entities, which we can now populate using some static methods.


IdentityServer needs to know what client applications are allowed to use it. I like to think of this as a whitelist, your Access Control List. Each client application is then configured to only be allowed to do certain things, for instance they can only ask for tokens to be returned to certain URLs, or they can only request certain information. They have scoped access.

internal class Clients {
	public static IEnumerable<Client> Get() {
		return new List<Client> {
            new Client {
                ClientId = "oauthClient",
                ClientName = "Example Client Credentials Client Application",
                AllowedGrantTypes = GrantTypes.ClientCredentials,
                ClientSecrets = new List<Secret> {
                    new Secret("superSecretPassword".Sha256())},
                AllowedScopes = new List<string> {"customAPI"}

Here we are adding a client that uses the Client Credentials OAuth grant type. This grant type requires a client Id and client secret to authorize access, with the secret being hashed using an extension method provided by Identity Server (we never store any passwords in plain text after all). The allowed scopes is a list of scopes that this client is allowed to request. Here our scope is customAPI, which we will initialize now.


Scopes represent what you are allowed to do. They represent the scoped access I mentioned before. Scopes come in two flavors: Identity and Resource. An identity scope allows certain claims to be returned, whilst a resource scope allows access to a protected resource (typically an API).

internal class Scopes {
    public static IEnumerable<Scope> Get() {
        return new List<Scope> {
            new Scope {
                Name = "customAPI",
                DisplayName = "Custom API",
                Description = "Custom API scope",
                Type = ScopeType.Resource,
                Claims = new List<ScopeClaim> {
                    new ScopeClaim(JwtClaimTypes.Role)
                ScopeSecrets =  new List<Secret> {
                    new Secret("scopeSecret".Sha256())

The first five scopes are some standard OpenID Connect defines identity scopes we wish IdentityServer to support. A quick tip, the openid scope is always required when using OpenID Connect flows. You can find more information about these in the OpenID Connect Specification.

We are also creating a custom scope that corresponds to a resource we wish to protect. By setting claims within the scope like this we are ensuring that these claim types will be added to any tokens that have this scope (if the user has a value for that type, of course). In this case we are unsuring that a users role claims will be added to any tokens with this scope. The scope secret will be used later during token introspection.


In the place of a fully fledged User Store such as ASP.NET Identity, we can use InMemoryUsers:

internal class Users {
    public static List<InMemoryUser> Get() {
        return new List<InMemoryUser> {
            new InMemoryUser {
                Subject = "5BE86359-073C-434B-AD2D-A3932222DABE",
                Username = "scott",
                Password = "password",
                Claims = new List<Claim> {
                    new Claim(JwtClaimTypes.Email, ""),
                    new Claim(JwtClaimTypes.Role, "Badmin")

A users subject (or sub) claim is their unique identifier. This should be something unique to your identity provider, not something like an email address. I point this out due to a recent vulnerability with Azure AD.

We now need to update our DI container with this information (instead of the previous empty collections):


If you run this and visit the discovery document once again, you’ll now see the scopes_supported and claims_supported sections populated.

OAuth Functionality

To test our implementation we can grab an access token from Identity Server using our OAuth client from earlier. This will be using the Client Credentials flow so our request will look like this:

POST /connect/token
Content-Type: application/x-www-form-urlencoded

This will return our access token as a JWT:

"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjkzNTIwYzlkNDUwYTNmOWY3MjRhNmQxYmJlZDNhODg5IiwidHlwIjoiSldUIn0.eyJuYmYiOjE0NzM1MjAxNzEsImV4cCI6MTQ3MzUyMzc3MSwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAiLCJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo0NDM1MC9yZXNvdXJjZXMiLCJjbGllbnRfaWQiOiJvYXV0aENsaWVudCIsInNjb3BlIjoiY3VzdG9tQVBJIn0.nz0ynAjVAkdM7tsXeT1H1JYcH97w0bYgMYWauK-1qGFrpAe2IlIIqkPvn9nOU9Ct7yp2wJlnEXH_SnSeKAEthTcYueHB1lsq4UuBlefdAw3ZFYQ4vt-dztYZ07OkVChzmGoLJCAKwz0_ZxbprDjUe6sUXDCX0V3Qngwhqc8beSzJCtNcIZXqJh1M5ZFUj9VHjsJN4zZdsnjL51Ub15rNq_sR3U-kENvpaApasIgN4OgxqwEZqjt_FzYXIbu5c2OxVBPZGaOWqTN_5lXwZsV5mZdYrK2GHugr2FO1r8zJRZhNPfuWV28BXhdJdozaR7uNyFrCM_C8BKtPvZMjZse58w",
"expires_in": 3600,
"token_type": "Bearer"

If we take this access token over to we can see that it contains the following claims:

"alg": "RS256",
"kid": "93520c9d450a3f9f724a6d1bbed3a889",
"typ": "JWT"
"nbf": 1473520171,
"exp": 1473523771,
"iss": "https://localhost:44350",
"aud": "https://localhost:44350/resources",
"client_id": "oauthClient",
"scope": "customAPI"

We can now use the token introspection endpoint of IdentityServer to validate the token, as if we were an OAuth resource receiving it from an external party. If successful, we’ll receive the claims in that token echoed back to us. Note that the access token validation endpoint from IdentityServer 3 is no longer available in IdentityServer 4.

It is here that the scope secret we created earlier comes into use, by using Basic Authentication where the username is the scope Id and the password a scope secret.

POST /connect/introspect
Authorization: Basic Y3VzdG9tQVBJOnNjb3BlU2VjcmV0
Content-Type: application/x-www-form-urlencoded


"nbf": 1473520171,
"exp": 1473523771,
"iss": "https://localhost:44350",
"aud": "https://localhost:44350/resources",
"client_id": "oauthClient",
"active": true,
"scope": "customAPI"

If you’d like to do this process programmatically and authorize access to a .NET Core resource in this way, check out the IdentityServer4.AcessTokenValidation library.

Resource Owner Grant Type

The IdentityServer documentation also has a guide on how to use the Resource Owner grant type. Do not be fooled by the fact that this grant type include a username and password, it is still only authorization and not authentication. In fact there are multiple disclaimers in the article stating that this grant type should only be used for legacy applications. See The problem with OAuth for Authentication by John Bradley for a good investigation into everything wrong with the Resource Owner grant type.

User Interface

Up until now we’ve been working without a UI, lets change this by pulling in the Quickstart UI from GitHub that uses ASP.NET Core MVC.

To download this either copy all folders in the repo into your project, or use the following powershell command (again, whilst within you project folder):

iex ((New-Object System.Net.WebClient).DownloadString(''))

Now we need to add ASP.NET MVC Core to our project. To do this, first add the following packages to your project:


And then add to your services (ConfigureServices):


And finally add to the end of your HTTP pipeline (Configure):


Now when we run the project, we get a splash screen. Hooray! But now that we have a UI, we can now start authenticating users.

IdentityServer 4 Quickstart UI Splash Screen

IdentityServer 4 Quickstart UI Splash Screen

OpenID Connect

To demonstrate authentication using OpenID Connect we’ll need to create ourselves a client web application and add a corresponding client within IdentityServer.

First we’ll need to add a new client within IdentityServer:

new Client {
    ClientId = "openIdConnectClient",
    ClientName = "Example Implicit Client Application",
    AllowedGrantTypes = GrantTypes.Implicit,
    AllowedScopes = new List<string>
    RedirectUris = new List<string> {"https://localhost:44330/signin-oidc"},
    PostLogoutRedirectUris = new List<string> {"https://localhost:44330"}

Where the redirect and post logout redirect uris are the url of our upcoming application. The redirect uri requires the path /signin-oidc and this path will be automatically created and handled by an upcoming piece of middleware.

Here we are using the OpenID Connect implicit grant type. This grant type allows us to request identity and access tokens via the browser. I would call this the simplest grant type to get started with.

Client Application

Now we need to create the client application. For this we’ll need another ASP.NET Core website, this time using the Web Application VS template but again, with no authentication.

To add OpenID Connect authentication to a ASP.NET Core site we need to add the following two packages to our site:


And then in our HTTP pipeline:

app.UseCookieAuthentication(new CookieAuthenticationOptions {
    AuthenticationScheme = "cookie"

Here we are telling our application to use cookie authentication, for signing in users. Whilst we may be using IdentityServer to log in users, every application still needs to issue its own cookie (to its own domain).

Now we need to add OpenID Connect authentication.

app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions {
    ClientId = "openIdConnectClient",
    Authority = "https://localhost:44350/",
    SignInScheme = "cookie"

Here we are telling our app to use our OpenID Connect Provider (IdentityServer), the client id we wish to sign in with and the authentication type to login with upon successful authentication (our previously defined cookie middleware).

Now all that’s left is to make a page require authentication to access. Let’s add the authorize attribute to the Contact action, because people contacting us is the last thing we want.

public IActionResult Contact() { ... }

Now when we run this application and select the Contact page, we’ll receive a 401 unauthorized. This in turn will be intercepted by our OpenID Connect middleware, which will 302 redirect us to our Identity Server authentication endpoint along with the necessary parameters.

IdentityServer 4 Quickstart UI Login Screen

IdentityServer 4 Quickstart UI Login Screen

Upon successful login, IdentityServer will then ask our consent for the client application to access certain information or resources on your behalf (these correspond to the identity and resource scopes the client has requested). This consent request can be disabled on a client by client basis. By default the OpenID Connect middleware for ASP.NET Core will request the openid and profile scopes.

IdentityServer 4 Quickstart UI Consent Screen

IdentityServer 4 Quickstart UI Consent Screen

And that’s all that’s required for wiring up a simple OpenID Connect Client using the implicit grant type.

Entity Framework Core

Currently we are using in memory stores, which as we noted before is for demo purposes or, at most, very lightweight implementations. Ideally we’d want to move our various stores into a persistent database that won't be wiped on every deploy or require a code change to add a new entry.

IdentityServer has an Entity Framework (EF) Core package that we can use to implement client, scope and persisted grant stores using any EF Core relational database provider.

The Identity Server Entity Framework Core package has been integration tested using the In-Memory, SQLite (in-memory) and SQL Server database providers. If you find any issues with other providers or wish to write tests against other database providers, feel free to open up an issue on the GitHub issue tracker or submit a pull request).

For this article we will be using SQL server and either SQL Express or Local DB, so we’ll require the following nuget packages:


Persisted grant store

The persisted grant store contains all information regarding given consent (so we don't keep asking for consent on every request), reference tokens (stored jwt’s where only a key corresponding to the jwt is given to the requester, making them easily revocable), and much more. Without a persistent store for this, tokens will be invalidated on every redeploy of IdentityServer.

First lets new up a couple of variables:

const string connectionString = 
    @"Data Source=(LocalDb)\MSSQLLocalDB;database=Test.IdentityServer4.EntityFramework;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

We can then add support for the persisted grant store by replacing AddInMemoryStores with:

.AddOperationalStore(builder => 
    builder.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)))

Where our migrations assembly is our project hosting IdentityServer. This is necessary to target DbContexts not located in your hosting project (in this case it is in the nuget package) and allows us to run EF migrations. Otherwise we’ll be met with an exception with a message such as:

Your target project 'Project.Host' doesn't match your migrations assembly 'Project.BusinessLogic'. Either change your target project or change your migrations assembly. Change your migrations assembly by using DbContextOptionsBuilder. E.g. options.UseSqlServer(connection, b => b.MigrationsAssembly("Project.Host")). By default, the migrations assembly is the assembly containing the DbContext.
Change your target project to the migrations project by using the Package Manager Console's Default project drop-down list, or by executing "dotnet ef" from the directory containing the migrations project.

Client and Scope stores

To add persistent storage for our scope and client stores we need something similar, replacing AddInMemoryClients and AddInMemoryScopes with:

.AddConfigurationStore(builder => 
    builder.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)))

These registrations also include a CORS policy service that reads from our Client tables.

Running EF Migrations

To run EF migration we need to add the Microsoft.EntityFrameworkCore.Tools package both as a project dependency and also to the "tools" section of your project.json (you can create this section if you don’t already have one. Package version at time of writing is 1.0.0-preview2-final). Then we can run:

dotnet ef migrations add InitialIdentityServerMigration -c PersistedGrantDbContext
dotnet ef migrations add InitialIdentityServerMigration -c ConfigurationDbContext

ASP.NET Core Identity

To add a persistent store for our users, Identity Server 4 offers integration for the ASP.NET Core Identity (aka ASP.NET Identity 3) library. We’ll do this using the ASP.NET Core Identity Entity Framework library and the base IdentityUser entities, again using SQL server:


Currently we need to create ourselves a custom implementation of IdentityDbContext in order to override the constructor to take a non-generic version of DbContextOptions. This is because IdentityDbContext only has a constructor accepting the generic DbContextOptions which, when we are registering multiple DbContexts, results in an Invalid Operation Exception. I’ve opened an issue on this, so hopefully we can skip this step soon.

public class ApplicationDbContext : IdentityDbContext {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

We then need to add a registration for the ASP.NET Identity DbContext to our ConfigureServices method.

services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();

And then in our IdentityServerBuilder replace AddInMemoryUsers with:


And finally in out Configure method (before UseIdentityServer or else we get cookie issues):


Again we need to run migrations. This can be done with:

dotnet ef migrations add InitialIdentityServerMigration -c ApplicationDbContext

That’s all that’s needed to wire up ASP.NET Core Identity with IdentityServer 4, but unfortunately our Quickstart UI we downloaded earlier is no longer going to work (if we try running it we’ll face an InvalidOperationException with message: Unable to resolve service for type 'IdentityServer4.Services.InMemory.InMemoryUserLoginService' while attempting to activate 'IdentityServer4.Quickstart.UI.Controllers.AccountController').

However, we can modify our existing AccountsController from the Quickstart UI to work for ASP.NET Core Identity by replacing some code.

First we need to change the constructor to accept the ASP.NET Core Identity UserManager, instead of the existing InMemoryUserLoginService. Our constructor should now look like this:

private readonly UserManager<IdentityUser> _userManager;
private readonly IIdentityServerInteractionService _interaction;

public AccountController( UserManager<IdentityUser> userManager, IIdentityServerInteractionService interaction) { _userManager = userManager; _interaction = interaction; }

By removing the InMemoryUserLoginService we have no broken two methods: Login (post) and ExternalCallback. We can replace the Login method entirely with the following:

public async Task<IActionResult> Login(LoginInputModel model)
    if (ModelState.IsValid)
        var identityUser = await _userManager.FindByNameAsync(model.Username);
        if (identityUser != null && await _userManager.CheckPasswordAsync(identityUser, model.Password))
            await HttpContext.Authentication.SignInAsync(identityUser.Id, identityUser.UserName);
            if (_interaction.IsValidReturnUrl(model.ReturnUrl))
                return Redirect(model.ReturnUrl);
            return Redirect("~/");
        ModelState.AddModelError("", "Invalid username or password.");

var vm = new LoginViewModel(HttpContext, model); return View(vm); }

And with the ExternalCallback callback method, we need to replace the find and provision logic with the following:

public async Task<IActionResult> ExternalCallback(string returnUrl)
    var tempUser = await HttpContext.Authentication.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
    if (tempUser == null)
        throw new Exception("External authentication error");

var claims = tempUser.Claims.ToList();
var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject); if (userIdClaim == null) { userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier); } if (userIdClaim == null) { throw new Exception("Unknown userid"); } claims.Remove(userIdClaim);
var provider = userIdClaim.Issuer; var userId = userIdClaim.Value; var user = await _userManager.FindByLoginAsync(provider, userId); if (user == null) { user = new IdentityUser { UserName = Guid.NewGuid().ToString() }; await _userManager.CreateAsync(user); await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, userId, provider)); }
var additionalClaims = new List<Claim>(); var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); if (sid != null) { additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); }
await HttpContext.Authentication .SignInAsync(user.Id, user.UserName, provider, additionalClaims.ToArray()); await HttpContext.Authentication .SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (_interaction.IsValidReturnUrl(returnUrl)) { return Redirect(returnUrl); } return Redirect("~/"); }

Job done!


Share this article: