Getting Started with IdentityServer4 and Duende IdentityServer

Scott Brady
Scott Brady
Identity Server ・ Updated August 2021 28 August 2021

IdentityServer is the popular OSS OpenID Connect and OAuth framework for ASP.NET Core. It allows your ASP.NET Core website to act as an OpenID Provider and OAuth authorization server, offering both Single Sign-On (SSO) and API protection out of the box. IdentityServer takes care of the protocol support, but user authentication is up to you, the developer.

In this tutorial, you’re going to see an IdentityServer implementation added to a basic web project, with examples showing how to configure client applications, API scopes, and user authentication. By the end of this tutorial, you’ll have a working IdentityServer implementation, using ASP.NET Identity, with an example ASP.NET Core client application and API (protected resource).

This article has samples for both IdentityServer4 and Duende IdentityServer (v5), targeting .NET Core 3.1 onwards. You can toggle between IdentityServer4 and Duende IdentityServer code as you progress through the article.

IdentityServer 3 vs IdentityServer 4

After the release of IdentityServer4 (aka. IDS4), IdentityServer3 entered maintenance mode, with only security fixes being released. However, in 2019, Microsoft dropped support for the OWIN libraries (Katana 3) that IdentityServer3 relied upon, and as a result, free IdentityServer3 support has ended.

If you are still using IdentityServer3 (or even IdentityServer2), ), I highly recommend that you migrate to the latest version of IdentityServer as soon as possible.

There are a fair few differences between IdentityServer3 and IdentityServer4. If you want to learn more, .

IdentityServer3 and IdentityServer4 are mostly compatible. They typically both implement the same specifications; they are both OpenID Providers; however, their internals are almost completely different. Luckily, when you integrate using OpenID Connect or OAuth, in the case of IdentityServer, you are not integrating to an implementation, but rather integrating using the OpenID Connect or OAuth specifications. In my experience, IdentityServer is one of the least opinionated implementations of these specifications out there.

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

The IdentityServer IUserService that was used to integrate your user store is also gone, replaced with a new user store abstraction in the form of IProfileService and IResourceOwnerPasswordValidator. You must now implement user authentication yourself (and that’s a good thing).

In my experience, many people are still using IdentityServer3 (we’ve even had customers still using IdentityServer2). My recommendation is to seriously consider migrating from these older versions to IdentityServer4 as soon as you can. But, I admit, rewriting your authentication system, rather than building new features, can be hard to sell to stakeholders. If you’re struggling to convince them, feel free to contact us on IdentityServer.com, and we’ll see if we can help.

IdentityServer4 vs Duende IdentityServer

As of 2021, IdentityServer switched to a commercial license and is now know as Duende IdentityServer (kind of IdentityServer v5). Duende IdentityServer is still OSS, but the license now requires most organizations to purchase a license from Duende.

IdentityServer4’s will continue to be supported for the lifetime of .NET Core 3.1, which ends December 2022. However, for newer versions of .NET, you should Duende IdentityServer instead.

At the time of writing (September 2021), the IdentityServer4 and Duende IdentityServer codebases are largely the same; but that will change over time, especially as .NET releases more LTS versions. Currently, new Duende features include key management, dynamic loading of external identity providers, OAuth resource indicators, and access to a BFF client library.

Implementing IdentityServer on ASP.NET Core and .NET Core

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 .NET Core 3.1 and JetBrains Rider (Visual Studio is fine).

You’re going to start building your IdentityServer as an empty web app, without any MVC or Razor dependencies, or any authentication. You can create this using dotnet new.

dotnet new web

You can achieve the same in Visual Studio, by creating an “ASP.NET Core Web Application” project using the “Empty” template. Just make sure you enable HTTPS and use no authentication.

The rest of this tutorial will assume that you are using https://localhost:5000 for your IdentityServer solution. This can be configured in your launchSettings.json file (“Properties” in Visual Studio).

Now, let’s add IdentityServer by installing it from NuGet. When creating this tutorial, I used IdentityServer4 version 4.1.2 and Duende IdentityServer version 5.2.2.

dotnet add package IdentityServer4
dotnet add package Duende.IdentityServer

Next, head over to your Startup class, where you can start registering dependencies and wiring up your pipeline.

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

services.AddIdentityServer()
    .AddInMemoryClients(new List<Client>())
    .AddInMemoryIdentityResources(new List<IdentityResource>())
    .AddInMemoryApiResources(new List<ApiResource>())
    .AddInMemoryApiScopes(new List<ApiScope>())
    .AddTestUsers(new List<TestUser>())
    .AddDeveloperSigningCredential();
services.AddIdentityServer(
  options => options.KeyManagement.Enabled = true)
    .AddInMemoryClients(new List<Client>())
    .AddInMemoryIdentityResources(new List<IdentityResource>())
    .AddInMemoryApiResources(new List<ApiResource>())
    .AddInMemoryApiScopes(new List<ApiScope>())
    .AddTestUsers(new List<TestUser>());

And then, update your Configure method to look something like the following to allow IdentityServer to start handling OAuth and OpenID Connect requests:

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();
    app.UseIdentityServer();
}

With the above code, you have registered IdentityServer in your DI container using AddIdentityServer, used a demo signing certificate with AddDeveloperSigningCredential, and used in-memory, volatile stores for your clients, resources, and users. You don’t have to call AddDeveloperSigningCredential when using Duende IdentityServer, since IdentityServer will manage signing keys for you.

By using AddIdentityServer, you are also causing all generated temporary data to be stored in memory. You will add actual clients, resources, and users shortly.

UseIdentityServer allows IdentityServer to start handling routing for OAuth and OpenID Connect endpoints, such as the authorization and token endpoints.

With this setup, you 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

IdentityServer’s discovery document (affectionately known as the “disco doc”) is available on the path /.well-known/openid-configuration, as per the OpenID Connect discovery specification. This document contains metadata information such as the:

  • location of various endpoints (for example, the authorization endpoint and the token endpoint)
  • location of it’s public keys (a JSON Web Key Set (JWKS))
  • grant types the provider supports
  • scopes it can authorize.

By having this standardized document, you enable the possibility of automatic integration and automatic key rollover.

IdentityServer Signing Credentials

Your signing credentials are private keys used to sign tokens. This allows for client applications and protected resources to verify that the contents of the token have not been altered in transit and that the token was created by IdentityServer.

IdentityServer uses the private key to create signatures, while other applications use the corresponding public key to verify the signature. These public keys are accessible to client applications via the jwks_uri in the OpenID Connect discovery document.

When you go to create and use your own signing credentials, do so using a tool such as OpenSSL or the New-SelfSignedCertificate PowerShell command. You can store the keys in an X.509 certificate, but there shouldn’t be any need to have the certificate issued by a Global CA. IdentityServer is only interested in the private key here and verifiers are typically only concerned with the public key.

In fact, with the newest versions of IdentityServer, you are no longer constrained to just RSA keys. You can now use ECDSA in IdentityServer.

IdentityServer Clients, Resources and Users

Now that you have IdentityServer up and running, let’s add some data to it.

First, you need to have a store of client applications that are allowed to use IdentityServer, as well as the protected resources that those clients can use, and the users that can authenticate in your system.

You are currently using the in-memory stores whose registrations accept a collection of their respective entities, which you can now populate in code.

IdentityServer client applications

IdentityServer needs to know what client applications are allowed to use it. I like to think of this as a list of applications that are allowed to use your system; your Access Control List (ACL). 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 specific URLs, or they can only request certain information about the user. They have scoped access.

internal class Clients
{
    public static IEnumerable<Client> Get()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "oauthClient",
                ClientName = "Example client application using client credentials",
                AllowedGrantTypes = GrantTypes.ClientCredentials,
                ClientSecrets = new List<Secret> {new Secret("SuperSecretPassword".Sha256())}, // change me!
                AllowedScopes = new List<string> {"api1.read"}
            }
        };
    }
}

Here you are adding a client that uses OAuth’s client credentials grant type. This grant type requires a client ID and secret to authorize access, with the secret simply being hashed using an extension method provided by IdentityServer. After all, you should never store passwords (shared secrets) in plain text, and your secrets in production should have enough entropy to never be guessable. The allowed scopes are a list of permissions that this client is allowed to request from IdentityServer. In this example, the only permitted scope is api1.read, which you will initialize now in the form of an API resource.

IdentityServer resources & scopes

Scopes represent what a client application is allowed to do. They represent the scoped access I mentioned before. In IdentityServer, scopes are typically modeled as resources, which come in two flavors: identity and API.

An identity resource allows you to model a scope that will permit a client application to view a subset of claims about a user. For example, the profile scope enables the app to see claims about the user such as name and date of birth.

An API resource allows you to model access to an entire protected resource, an API, with individual permissions levels (scopes) that a client application can request access to.

internal class Resources
{
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new[]
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email(),
            new IdentityResource
            {
                Name = "role",
                UserClaims = new List<string> {"role"}
            }
        };
    }

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new[]
        {
            new ApiResource
            {
                Name = "api1",
                DisplayName = "API #1",
                Description = "Allow the application to access API #1 on your behalf",
                Scopes = new List<string> {"api1.read", "api1.write"},
                ApiSecrets = new List<Secret> {new Secret("ScopeSecret".Sha256())}, // change me!
                UserClaims = new List<string> {"role"}
            }
        };
    }
	
	public static IEnumerable<ApiScope> GetApiScopes()
    {
        return new[]
        {
            new ApiScope("api1.read", "Read Access to API #1"),
			new ApiScope("api1.write", "Write Access to API #1")
        };
    }
}

IdentityServer IdentityResources

The first three identity resources represent some standard OpenID Connect scopes you’ll want IdentityServer to support. For example, the email scope allows the email and email_verified claims to be returned. You are also creating a custom identity resource called role which returns any role claims for the authenticated user.

A quick tip, the openid scope is always required when using OpenID Connect flows (where you want to receive an identity token). You’ll see this in action shortly.

You can find more information about these core OpenID Connect identity scopes in the OpenID Connect specification.

IdentityServer ApiResources and ApiScopes

An API resource models a single API that IdentityServer is protecting. In OAuth, this is known as a “protected resource”.

An API scope is an individual authorization level on an API that a client application is allowed to request. For example, an API resource might be adminapi, with the scopes adminapi.read, adminapi.write, and adminapi.createuser. API scopes can be as fine-grained or as generic as you want.

Just remember, scopes represent what the user is authorizing the client application to do on their behalf. That does not mean that the user is allowed to perform an action. OAuth does not provider user-level authorization.

By setting the UserClaims property, you are ensuring that these claim types will be added to any access tokens that have this scope (if the user has a value for that type, of course). In this case, you are ensuring that a user’s role claims will be added to any access tokens authorized to use this scope.

IdentityServer test users

In the place of a fully-fledged user store such as ASP.NET Identity, you can use the TestUsers class from IdentityServer:

internal class Users
{
    public static List<TestUser> Get()
    {
        return new List<TestUser> {
            new TestUser {
                SubjectId = "5BE86359-073C-434B-AD2D-A3932222DABE",
                Username = "scott",
                Password = "password",
                Claims = new List<Claim> {
                    new Claim(JwtClaimTypes.Email, "[email protected]"),
                    new Claim(JwtClaimTypes.Role, "admin")
                }
            }
        };
    }
}

A user’s subject (or sub) claim is their unique identifier. This should be something unique to your identity provider that will never change, as opposed to volatile data such as an email address.

With this test data, you can now update your DI container to look like the following:

services.AddIdentityServer()
    .AddInMemoryClients(Clients.Get())
    .AddInMemoryIdentityResources(Resources.GetIdentityResources())
    .AddInMemoryApiResources(Resources.GetApiResources())
    .AddInMemoryApiScopes(Resources.GetApiScopes())
    .AddTestUsers(Users.Get())
    .AddDeveloperSigningCredential();
services.AddIdentityServer(
  options => options.KeyManagement.Enabled = true)
    .AddInMemoryClients(Clients.Get())
    .AddInMemoryIdentityResources(Resources.GetIdentityResources())
    .AddInMemoryApiResources(Resources.GetApiResources())
    .AddInMemoryApiScopes(Resources.GetApiScopes())
    .AddTestUsers(Users.Get())

If you rerun IdentityServer and visit the discovery document, you’ll now see that the scopes_supported and claims_supported sections are populated based on your identity resources and API scopes.

OAuth Functionality

To test your implementation, you can grab an access token from IdentityServer using the OAuth client you have configured. This will be using the client credentials flow, meaning your request will look like this:

POST /connect/token

Headers:
Content-Type: application/x-www-form-urlencoded

Body:
grant_type=client_credentials&scope=api1.read&client_id=oauthClient&client_secret=SuperSecretPassword

Which returns a token response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3MzdCRDU2MUU2MTExM0UzMTVFRDQ3QUM1MzBCRkI5IiwidHlwIjoiYXQrand0In0.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo1MDAwIiwibmJmIjoxNjMwMTY4OTY3LCJpYXQiOjE2MzAxNjg5NjcsImV4cCI6MTYzMDE3MjU2NywiYXVkIjoiYXBpMSIsInNjb3BlIjpbImFwaTEucmVhZCJdLCJjbGllbnRfaWQiOiJvYXV0aENsaWVudCIsImp0aSI6IkY0MzZGREY5OUU4Q0Y2QTREMkM0QzY5M0FCQTdDMEY4In0.HG-1bNAv5OQf_Vn8vI_-G5rK1ueQhsN_VSx_M78rU_n85hc1BH7Nt6p3MKeGPDy6YYed9krpGSgRwwB2GC-pdWM-o5X7JsVregZh1nCyOFBYvMYYQuDY2Pi3r49sXC6PrFvEvR4OzJ0phxuwFWvwG_IFVmYUaMalbpOdiLOgDl5R68gcwrQ-4NPYAdX_9tmLBLJZYFl5gpbhI5nwYDxGBIWpPk63oNIIYd9sVgMj7THdG_-tH5oV_eCT8MmsMK-_rOQ_o5GcmzwdMWvqn9enhf0995Rhf28ANBEEseEiWlKw_dCaRZ9OQE7Fp0GlCOtGmlpN6JN2UrKsKAS_m6dzAg",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": "api1.read"
}

If you take this access token over to jwt.ms, you can see that it contains the following claims:

// header
{
  "alg": "RS256",
  "kid": "0737BD561E61113E315ED47AC530BFB9",
  "typ": "at+jwt"
}

// payload
{
  "iss": "https://localhost:5000",
  "nbf": 1630168967,
  "iat": 1630168967,
  "exp": 1630172567,
  "aud": "api1",
  "scope": [
    "api1.read"
  ],
  "client_id": "oauthClient",
  "jti": "F436FDF99E8CF6A4D2C4C693ABA7C0F8"
}

Protecting an API

You can now use this access token to access API, protected by your implementation of IdentityServer.

You can quickly spin up an API using the .NET CLI, using the webapi template:

dotnet new webapi

To protect the API, you can either use the JWT authentication handler from Microsoft or the IdentityServer specific implementation. I prefer the IdentityServer specific version because it sets some useful default options and can support reference tokens & token introspection if you decide to move away from JWTs.

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

You’ll then want to add the following to your API’s ConfigureServices method:

services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Audience = "api1";
        options.Authority = "https://localhost:5000";
    });

Where the authority is the URL of your IdentityServer, and the audience name is the name of the API resource that represents it. This authentication handler will automatically fetch the discovery document from IdentityServer on first use.

You’ll also need to add the authentication middleware, by updating your Configure method to look something like this:

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
}

You can then require an access token by using the AuthorizeAttribute on one of your endpoints.

To use the access token, simply attach it to an HTTP request, using the Authorization header. For example:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3MzdCRDU2MUU2MTExM0UzMTVFRDQ3QUM1MzBCRkI5IiwidHlwIjoiYXQrand0In0.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo1MDAwIiwibmJmIjoxNjMwMTY4OTY3LCJpYXQiOjE2MzAxNjg5NjcsImV4cCI6MTYzMDE3MjU2NywiYXVkIjoiYXBpMSIsInNjb3BlIjpbImFwaTEucmVhZCJdLCJjbGllbnRfaWQiOiJvYXV0aENsaWVudCIsImp0aSI6IkY0MzZGREY5OUU4Q0Y2QTREMkM0QzY5M0FCQTdDMEY4In0.HG-1bNAv5OQf_Vn8vI_-G5rK1ueQhsN_VSx_M78rU_n85hc1BH7Nt6p3MKeGPDy6YYed9krpGSgRwwB2GC-pdWM-o5X7JsVregZh1nCyOFBYvMYYQuDY2Pi3r49sXC6PrFvEvR4OzJ0phxuwFWvwG_IFVmYUaMalbpOdiLOgDl5R68gcwrQ-4NPYAdX_9tmLBLJZYFl5gpbhI5nwYDxGBIWpPk63oNIIYd9sVgMj7THdG_-tH5oV_eCT8MmsMK-_rOQ_o5GcmzwdMWvqn9enhf0995Rhf28ANBEEseEiWlKw_dCaRZ9OQE7Fp0GlCOtGmlpN6JN2UrKsKAS_m6dzAg

Adding a user interface for user authentication

Up until now, IdentityServer has been running without a UI. Let’s change this by pulling in the Quickstart UI from GitHub that uses ASP.NET Core MVC.

To download the QuickStart UI, either copy all folders in the repo into your project, or use the PowerShell or curl command from the repo’s readme (again, while within your project folder). For example:

iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/main/getmain.ps1'))
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/DuendeSoftware/IdentityServer.Quickstart.UI/main/getmain.ps1'))

To take advantage of these new controllers and views, you’ll need to add the following to your ConfigureServices method:

services.AddControllersWithViews();

And then update your Configure method to look something like the following, by adding the middleware for static files, authorization, and endpoints.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();
    app.UseRouting();
	
    app.UseIdentityServer();
    app.UseAuthorization();

    app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
}

Now when you run the project, assuming you are running in developer mode, you should get a splash screen. Hooray! Now that you have a UI, you can now start authenticating users.

The IdentityServer4 QuickStart UI splash screen showing a welcome message and some links to help pages The IdentityServer4 QuickStart UI splash screen showing a welcome message and some links to help pages

Integrating an ASP.NET Core MVC app with IdentityServer

To demonstrate authentication using OpenID Connect, you’ll need to create another web application and configure it as a client application within IdentityServer.

Let’s start by adding a new client entry within IdentityServer:

new Client
{
    ClientId = "oidcClient",
    ClientName = "Example Client Application",
    ClientSecrets = new List<Secret> {new Secret("SuperSecretPassword".Sha256())}, // change me!
    
    AllowedGrantTypes = GrantTypes.Code,
    RedirectUris = new List<string> {"https://localhost:5002/signin-oidc"},
    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        IdentityServerConstants.StandardScopes.Email,
        "role",
        "api1.read"
    },

    RequirePkce = true,
    AllowPlainTextPkce = false
}

This configuration adds a new client application that uses the recommended flow for server-side web applications: the authorization code flow with Proof-Key for Code Exchange (PKCE). For this client, you have also set a redirect URI. Because this flow takes place via the browser, IdentityServer must know an allowed list of URLs to send the user back to, once user authentication and client authorization is complete; what URLs it can return the authorization result to.

An ASP.NET Core MVC client application

Now you can create the client application itself. For this, you’ll need another ASP.NET Core website, this time using the mvc template, and again, with no authentication.

dotnet new mvc

Before adding a remote authentication scheme such as OpenID Connect, you’ll need to add a local authentication scheme, a cookie, which you can add to your ConfigureServices method:

services.AddAuthentication(options =>
    {
        options.DefaultScheme = "cookie";
    })
    .AddCookie("cookie");

This tells your application to use cookie authentication for everything (the DefaultScheme). So, if you call sign in, sign out, challenge, etc. then this is the scheme that will be used. This local cookie is necessary because even though you’ll be using IdentityServer to authenticate the user and create a Single Sign-On (SSO) session, every individual client application will maintain its own, shorter-lived session.

You can now update your authentication configuration to use OpenID Connect to find out who the user is.

First, install the OpenID Connect authentication NuGet package:

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

And then you can update your authentication configuration to look like the following:

services.AddAuthentication(options =>
    {
        options.DefaultScheme = "cookie";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("cookie")
    .AddOpenIdConnect("oidc", options =>
    {
	    options.Authority = "https://localhost:5000";
        options.ClientId = "oidcClient";
        options.ClientSecret = "SuperSecretPassword";
    
        options.ResponseType = "code";
        options.UsePkce = true;
        options.ResponseMode = "query";
    
        // options.CallbackPath = "/signin-oidc"; // default redirect URI
        
        // options.Scope.Add("oidc"); // default scope
        // options.Scope.Add("profile"); // default scope
        options.Scope.Add("api1.read");
        options.SaveTokens = true;
    });

By default, the ASP.NET Core OpenID Connect handler will use the implicit flow with the form post response mode. The implicit flow is in the process of being deprecated, and the form post response is becoming unreliable thanks to 3rd party cookies policies being rolled out by browsers. As a result, you have updated these to use the authorization code flow, PKCE, and the query string response mode.

UsePkce is a relatively new setting in ASP.NET Core, check out my other articles if you need to implement PKCE in .NET Core 2.1 or .NET Framework web applications.

I’ve also shown some of the default settings used by the OpenID Connect authentication handler. By default, the redirect URL will use the /signin-oidc path. This is fine; however, you’ll need to add a unique callback path for each authentication handler you have in your application. ASP.NET Core also adds the oidc and profile scopes you can clear and/or add extra scopes if required. SaveTokens causes the identity and access tokens to be saved, accessible using code such as HttpConect.GetTokenAsync("access_token").

To have your callback path work, you’ll need to again update your Configure method to call the ASP.NET Core authentication middleware:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();
    app.UseRouting();
	
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
}

And again, add the AuthorizationAttribute to a controller action to authenticate the user using a cookie or challenge the user via oidc.

[Authorize]
public IActionResult Privacy() => View();

The next time that you run the application and select the Privacy page, you’ll receive a 401 unauthorized. This, in turn, will be handled by your DefaultChallengeScheme (your OpenID Connect authentication handler), which will 302 redirect you to your IdentityServer authorization endpoint.

The IdentityServer4 QuickStart UI login screen, showing local authentication using a username and password

The IdentityServer QuickStart login screen

Upon successful login, you’ll be redirected back to your client application’s redirect URI, be logged in using your local cookie, and then redirected back to the page you were trying to access (the privacy page). That’s all that’s required for wiring up a simple OpenID Connect client application in ASP.NET Core!

As of IdentityServer4 v4, the OAuth consent page is no longer enabled by default. If you are dealing with 3rd party client applications or protected resources, I recommend that you re-enable this for your apps.

IdentityServer with Entity Framework Core

Currently, you are using in-memory stores which, as I noted before, are only suitable for demo purposes or, at most, very lightweight implementations. Ideally, you’d want to move your 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 you can use to implement client, resource, scope, and persisted grant stores using any EF Core relational database provider.

For this tutorial you will be using SQL Server (SQL Express or Local DB will do), so you’ll need the following nuget packages:

dotnet add package IdentityServer4.EntityFramework
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Duende.IdentityServer.EntityFramework
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

If you don’t want to use IdentityServer’s Entity Framework library, then you’ll need to create your own IdentityServer storage layer.

Storing persisted grants using the operational store

The persisted grant store maintains temporary data such as consent, reference tokens, refresh tokens, device codes, authorization codes, and more. Without a persistent store for this data, you will not be able to reliably load balance IdentityServer.

To use IdentityServer’s default Entity Framework store, you’ll first need to get a connection string and set the migrations assembly:

// using local db (assumes Visual Studio has been installed)
const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=Test.IdentityServer.EntityFramework;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

You can then add support for the persisted grant store by updating your call to AddIdentityServer with:

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

In this case, you are setting the assembly to store your EF migrations as the current assembly. This is necessary to target DbContexts not located in your hosting project (in this case, it is in a nuget package) and allows us to run EF migrations. Otherwise, you’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.

Storing IdentityServer clients and scopes using the configuration store

To add persistent storage for your scope and client stores, you’ll need something similar, this time replacing the calls to AddInMemoryClients, AddInMemoryIdentityResources, AddInMemoryApiScopes, and AddInMemoryApiResources:

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

These registrations also add a CORS policy service that initializes itself from data in your client records. I recommend using this CORS service. Do not just allow all origins.

Running the IdentityServer EF migrations

To run EF migrations, you’ll need the EF Core tooling installed, and the Microsoft.EntityFrameworkCore.Design package installed in your project:

dotnet add package Microsoft.EntityFrameworkCore.Design

Once you have this, you can create your migrations using:

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

You can then create your databases by calling dotnet ef database update on each of the DB contexts:

dotnet ef database update -c PersistedGrantDbContext
dotnet ef database update -c ConfigurationDbContext
To programmatically create clients & resources using the config you previously used, check out the InitializeDbTestData method in this article’s GitHub repository.

IdentityServer with ASP.NET Core Identity

To add a persistent store for users, IdentityServer offers out of the box integration for ASP.NET Core Identity (aka ASP.NET Identity 3). ASP.NET Identity includes the basic features you’d need to implement a production-ready user authentication system, including password hashing, password reset, and lockout functionality.

This tutorial will use the Entity Framework Core implementation of the ASP.NET user and roles stores, which means you’ll need the following NuGet packages:

dotnet add package IdentityServer4.AspNetIdentity
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Duende.IdentityServer.AspNetIdentity
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

You’ll then need to create a DbContext that inherits ASP.NET Identity’s IdentityDbContext and override the constructor to use a non-generic version of DbContextOptions. This is because IdentityDbContext only has a constructor accepting the generic DbContextOptions which, when you are registering multiple DbContexts, results in an InvalidOperationException. It would be great if this ceremony was one day made unnecessary.

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

With these libraries installed and your new DbContext, you can now register then need to add a registration for the ASP.NET Identity DbContext to your ConfigureServices method. These registrations should be made before your IdentityServer registrations.

services.AddDbContext<ApplicationDbContext>(builder =>
    builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)));

services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

It’s important to know that calling AddIdentity will change your application’s default cookie scheme to IdentityConstants.ApplicationScheme.

You can then configure IdentityServer’s internal code to use ASP.NET Identity by replacing the call to AddTestUsers with:

AddAspNetIdentity<IdentityUser>()

At a high level, this call does the following:

  • Adds an ASP.NET Identity compatible profile service (how IdentityServer generated user claims)
  • Adds an extended implementation of ASP.NET Identity’s IUserClaimsPrincipalFactory (how ASP.NET Identity transforms a user object into claims)
  • Configures IdentityServer to use ASP.NET Identity’s cookies and tweaks those cookies to be suitable for OpenID Connect

You’ll also need database migrations for this context, using:

dotnet ef migrations add InitialIdentityServerMigration -c ApplicationDbContext
dotnet ef database update -c ApplicationDbContext

That’s all that’s needed to wire up IdentityServer itself to use ASP.NET Core Identity, but unfortunately, the Quickstart UI you downloaded earlier is no longer going to work properly, as it is still using a TestUserStore. However, you can modify the QuickStart UI to work with ASP.NET Core Identity by replacing some code.

This tutorial will use the ASP.NET Identity’s SignInManager during authentication. The SignInManager meets mosts basic use cases, but if you have any complex user authentication requirements, or want greater control over the user experience, then I recommend using ASP.NET Identity’s UserManager instead.

Let’s start with the AccountController. First, you’ll need to change the constructor to accept the ASP.NET Identity SignInManager, instead of the current TestUserStore. Your constructor should now look something like this:

private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly IEventService _events;
private readonly SignInManager<IdentityUser> _signInManager;

public AccountController(
    IIdentityServerInteractionService interaction,
    IClientStore clientStore,
    IAuthenticationSchemeProvider schemeProvider,
    IEventService events,
    SignInManager<IdentityUser> signInManager)
{
    _interaction = interaction;
    _clientStore = clientStore;
    _schemeProvider = schemeProvider;
    _events = events;

    _signInManager = signInManager;
}

By removing the TestUserStore, you should see that you broke a single method: Login. I recommend changing the failing validation block to the following:

You’ll need to do something similar in the ExternalController, for account linking and provisioning when using an external identity provider such as Google or Azure AD.

You should be able to run with that and start tweaking the QuickStart UI to meet your business needs. If you’re stuck, check out the completed sample for this tutorial on GitHub.

User registration and password reset

This tutorial only brought in the basic use of ASP.NET Core Identity. For features like user registration and password reset, there’s nothing IdentityServer specific you’ll need to do. Just follow the regular use of ASP.NET Identity!

If you’re new to ASP.NET Identity, check out my Pluralsight course for a deep dive into the library.

Next Steps

This tutorial has covered the basics of IdentityServer and shown you how to use it to protect an ASP.NET Core web app and API. This should be enough to get you started, but before you go into production, don’t forget to address the points listed in the deployment documentation.

For more advanced use cases, check out some of my other articles to learn how to:

Otherwise, if you don’t want to do this yourself or need features such as SAML integration or FIDO authentication, then head over to identityserver.com to view IdentityServer’s commercial products and services.

Content is licensed under CC BY 4.0. Remember, don't copy and paste code written by strangers on the internet.