IdentityServer4 is the latest iteration of the IdentityServer OSS project, a popular OpenID Connect and OAuth framework for ASP.NET Core. In this article, you are going to see how IdentityServer4 works, and how to create a working implementation, taking you from zero to hero.

IdentityServer 3 vs IdentityServer 4

After IdentityServer4 was initially released, IdentityServer3 was soon switched into 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 consider migrating to IdentityServer4 as soon as possible.

There are a fair few differences between IdentityServer3 and IdentityServer4. Click here to read 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.

Implementing IdentityServer4 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.300 and Visual Studio 2019 (I developed using Rider, but I had Visual Studio installed).

You’re going to start building your IdentityServer4 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 (article currently written for IdentityServer4 version 4.0.0):

dotnet add package IdentityServer4

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 IdentityServer4:

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

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

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

With the above code, you have registered IdentityServer4 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. 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

The OpenID Connect Discovery Document (affectionately known as the “disco doc”) is available on every OpenID Connect provider at the /.well-known/openid-configuration endpoint (as per the spec). This document contains information such as the location of various endpoints (for example, the token endpoint and the end session endpoint), the grant types the provider supports, the scopes it can authorize, and so on. By having this standardized document, you 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 Credentials

Your signing credentials are dedicated keys used to sign tokens, allowing for client applications to verify that the contents of the token have not been altered in transit. This involves private keys to sign tokens and public keys to verify their signatures. 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 use an X509 certificate, but there’s typically no need to have it issued by a Global CA. You’re only interested in the private key here.

In fact, with the latest versions of IdentityServer4, you are no longer constrained to just RSA keys, but also ECC keys.

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 Resources that these 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.

Clients

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 the Client Credentials OAuth 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.

Resources & Scopes

Scopes represent what a client application is allowed to do. They represent the scoped access I mentioned before. In IdentityServer4, 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())}, 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") }; } }

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 can find more information about these in the OpenID Connect specification.

ApiResources and ApiScopes

An API resource models a single API that IdentityServer is protecting (a “protected resource” from the OAuth specification). 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.

By setting the UserClaims property, you 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, you are ensuring that a user’s role claims will be added to any tokens authorized to use this scope.

Scope vs. Resource OpenID Connect and OAuth scopes being modeled as resources is the most significant conceptual change between IdentityServer 3 and IdentityServer 4. In the latest iteration, a resource can have many scopes, and a scope can belong to many resources, but a scope does not necessarily need to belong to a resource. The offline_access scope used to request refresh tokens is now supported by default, with authorization to use this scope controlled by the Client property AllowOfflineAccess . To read more about API resources and scopes in IdentityServer4, I recommend checking out the IdentityServer4 documentation.

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();

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 Identity Server 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": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkQxNDlENEM2MzdDRjVCMTBFRTIwNDVGRjA2RTFDNzZCIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE1OTMxMDI2MjQsImV4cCI6MTU5MzEwNjIyNCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6ImFwaTEiLCJjbGllbnRfaWQiOiJvYXV0aENsaWVudCIsImp0aSI6IjRENzM1RUMxMkZEMjRDNTBFQTFCRjcwRjlCODQ5QjA1IiwiaWF0IjoxNTkzMTAyNjI0LCJzY29wZSI6WyJhcGkxLnJlYWQiXX0.0xott5jEBg_KDn_62iKDLJvD5CWpT_4BhyY4clXoRjUgCaJmnhQuyF0jEUNDPylPXQIz-BzsRdraLoXmdm4fF6HyLUcZ587dXfM_OUPup4RpHTCV5g1WuMU1PleEOu1FF7FepNAZ52XFK5JQl2NY2PikvzXNIeirieT7KBBTyQH2L9Rxe-_ZS4q7WFq4eIYIIt9ZhCGv7QP_B_4LcpkFiDZIOqU3ZxhvafMl44DeHkFj8cIunmDszJxghxPCqIi9KX37RLwrtKa0sO5r8lnq2itV7LZBPdGQPFYmRnIr9Nnys4ikUkp5qGIqyWWwsdsRtzKEBAxmssYAHAHlBetL1w", "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": "D149D4C637CF5B10EE2045FF06E1C76B", "typ": "at+jwt" } // payload { "nbf": 1593102624, "exp": 1593106224, "iss": "https://localhost:5000", "aud": "api1", "client_id": "oauthClient", "jti": "4D735EC12FD24C50EA1BF70F9B849B05", "iat": 1593102624, "scope": [ "api1.read" ] }

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 IdentityServer4.AccessTokenValidation

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

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

Where the authority is the URL of your IdentityServer4, 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 eyJhbGciOiJSUzI1NiIsImtpZCI6IkQxNDlENEM2MzdDRjVCMTBFRTIwNDVGRjA2RTFDNzZCIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE1OTMxMDI2MjQsImV4cCI6MTU5MzEwNjIyNCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6ImFwaTEiLCJjbGllbnRfaWQiOiJvYXV0aENsaWVudCIsImp0aSI6IjRENzM1RUMxMkZEMjRDNTBFQTFCRjcwRjlCODQ5QjA1IiwiaWF0IjoxNTkzMTAyNjI0LCJzY29wZSI6WyJhcGkxLnJlYWQiXX0.0xott5jEBg_KDn_62iKDLJvD5CWpT_4BhyY4clXoRjUgCaJmnhQuyF0jEUNDPylPXQIz-BzsRdraLoXmdm4fF6HyLUcZ587dXfM_OUPup4RpHTCV5g1WuMU1PleEOu1FF7FepNAZ52XFK5JQl2NY2PikvzXNIeirieT7KBBTyQH2L9Rxe-_ZS4q7WFq4eIYIIt9ZhCGv7QP_B_4LcpkFiDZIOqU3ZxhvafMl44DeHkFj8cIunmDszJxghxPCqIi9KX37RLwrtKa0sO5r8lnq2itV7LZBPdGQPFYmRnIr9Nnys4ikUkp5qGIqyWWwsdsRtzKEBAxmssYAHAHlBetL1w

Adding a User Interface

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'))

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:

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 developer splash screen

OpenID Connect

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.

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 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!

With 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.

Resource Owner Password Credentials (ROPC) Grant Type At some point, you’ll be asked why the login page cannot be hosted within the client application, or maybe you’ll have someone on a UX team scream at you for asking them to use a web browser to authenticate the user. Sure, this could be achieved using the ROPC/password grant type; however, this is a security anti-pattern, and this grant type is only included in the OAuth 2.0 specification to help legacy applications. That’s applications considered legacy in 2012. For a full write-up of everything wrong with the Resource Owner grant type, check out my article Why the Resource Owner Password Credentials Grant Type is not Authentication nor Suitable for Modern Applications.

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

Persisted Grant 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, some tokens will be invalidated on every restart of IdentityServer and in progress authorization requests will fail. This means that you would not be able to reliably host more than one IdentityServer instance at a time (no load balancing).

First, let’s 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;

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)))

Where your migration assembly is the project hosting IdentityServer. 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.

Client and Scope Stores

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.

Running 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

InitializeDbTestData method in this To programmatically create clients & resources using the config you previously used, check out themethod in this article’s GitHub repository

ASP.NET Core Identity

To add a persistent store for users, IdentityServer 4 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

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 random DB context, 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 cookie scheme 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 ASP.NET Core Identity with IdentityServer4, 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:

// validate username/password against in-memory store if (_users.ValidateCredentials(model.Username, model.Password)) { var user = _users.FindByUsername(model.Username); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId)); // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; }; // issue authentication cookie with subject ID and username var isuser = new IdentityServerUser(user.SubjectId) { DisplayName = user.Username }; await HttpContext.SignInAsync(isuser, props); // TODO: existing return URL handling... } // find user by username var user = await _signInManager.UserManager.FindByNameAsync(model.Username); // validate username/password using ASP.NET Identity if (user != null && (await _signInManager.CheckPasswordSignInAsync(user, model.Password, true)) == SignInResult.Success) { await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId)); // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; }; // issue authentication cookie with subject ID and username var isuser = new IdentityServerUser(user.Id) { DisplayName = user.UserName }; await HttpContext.SignInAsync(isuser, props); // TODO: existing return URL handling... }

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.

FindUserFromExternalProvider Before After

var user = _users.FindByExternalProvider(provider, providerUserId); var user = await _userManager.FindByLoginAsync(provider, providerUserId);

AutoProvisionUser Before After

// create test user from claims var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); // create dummy internal account (you can do something more complex) var user = new IdentityUser(Guid.NewGuid().ToString()); await _userManager.CreateAsync(user); // add external user ID to new account await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerUserId, provider));

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.

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.