Getting Started with IdentityServer 4

Identity Server Last Updated: 23 April 2017

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

IdentityServer

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 initial time of writing IdentityServer 4 was in RC5 and IdentityServer 3 was at v2.5.3 with another major release (v3.0.0) planned for the future. This article has since been updated to IdentityServer 4 v1.5.

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. IdentityServer 4 now supports .NET Core 1.1, leaving behind .NET Core 1.0 due to breaking changes between the two versions.

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 .NET Core 1.1 and Visual Studio 2017.

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 install the following nuget package (article currently written for 1.5.0):

IdentityServer4

Now to our Startup class to start registering dependencies and wiring up services.

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

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

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

app.UseIdentityServer();

What we have done here is registered IdentityServer in our DI container using AddIdentityServer, used a demo signing certificate with AddTemporarySigningCredential, and used in-memory stores for our clients, resources and users. By using AddIdentityServer we are also causing all generated tokens/grants to be stored in memory. We will add actual clients, resources 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, Resources and Users

First we need to have a store of Client applications that are allowed to use IdentityServer, as well the Resources 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.

Clients

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.read"}
            }
        };
    }
}

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.read, which we will initialize now in the form of an API resource.

Resources & Scopes

Scopes represent what you are allowed to do. They represent the scoped access I mentioned before. In IdentityServer 4 scopes are modelled as resources, which come in two flavors: Identity and API. An identity resource allows you to model a scope that will return a certain set of claims, whilst an API resource scope allows you to model access to a protected resource (typically an API).

internal class Resources {
    public static IEnumerable<IdentityResource> GetIdentityResources() {
        return new List<IdentityResource> {
            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 List<ApiResource> { new ApiResource { Name = "customAPI", DisplayName = "Custom API", Description = "Custom API Access", UserClaims = new List<string> {"role"}, ApiSecrets = new List<Secret> {new Secret("scopeSecret".Sha256())}, Scopes = new List<Scope> { new Scope("customAPI.read"), new Scope("customAPI.write") } } }; } }
IdentityResources

The first three identity resources represent some standard OpenID Connect defined scopes we wish IdentityServer to support. For example he email scope allows the email and email_verified claims to be returned. We are also creating a custom identity resource in the form of role which returns an role claims for authenticated user.

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.

ApiResources

For api resources we are modelling a single API that we wish to protect called customApi. This API has two scopes that can be requested: customAPI.read and customAPI.write.

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 ensuring that a users role claims will be added to any tokens with this scope. The scope secret will be used later during token introspection.

Scope vs Resource

OpenID Connect and OAuth scopes now being modelled as resources, is the biggest conceptual change between IdentityServer 3 and IdentityServer 4.

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.

Users

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

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

services.AddIdentityServer()
    .AddInMemoryClients(Clients.Get())                         
    .AddInMemoryIdentityResources(Resources.GetIdentityResources())
    .AddInMemoryApiResources(Resources.GetApiResources())
    .AddTestUsers(Users.Get())                     
    .AddTemporarySigningCredential();

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
Headers:
Content-Type: application/x-www-form-urlencoded
Body:
grant_type=client_credentials&scope=customAPI.read&client_id=oauthClient&client_secret=superSecretPassword

This will return our access token as a JWT:

"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0M2U4MjljMmI1NzQ4OTk2OTc1M2JhNGY4MjA1OTc5ZGYwZGE5ODhjNjQwY2ZmYTVmMWY0ZWRhMWI2ZTZhYTQiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE0ODE0NTE5MDMsImV4cCI6MTQ4MTQ1NTUwMywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAiLCJhdWQiOlsiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAvcmVzb3VyY2VzIiwiY3VzdG9tQVBJIl0sImNsaWVudF9pZCI6Im9hdXRoQ2xpZW50Iiwic2NvcGUiOlsiY3VzdG9tQVBJLnJlYWQiXX0.D50LeW9265IH695FlykBiWVkKDj-Gjiv-8q-YJl9qV3_jLkTFVeHUaCDuPfe1vd_XVxmx_CwIwmIGPXftKtEcjMiA5WvFB1ToafQ1AqUzRyDgugekWh-i8ODyZRped4SxrlI8OEMcbtTJNzhfDpyeYBiQh7HeQ6URn4eeHq3ePqbJSTPrqsYyG9YpU6azO7XJlNeq_Ml1KZms1lxrkXcETfo7U1h-z66TxpvH4qQRrRcNOY_kejq1x_GD3peWcoKPJ_f4Rbc4B-UvqicslKM44dLNoMDVw_gjKHRCUaaevFlzyS59pwv0UHFAuy4_wyp1uX7ciQOjUPyhl63ZEOX1w",
"expires_in": 3600,
"token_type": "Bearer"

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

"alg": "RS256",
"kid": "143e829c2b57489969753ba4f8205979df0da988c640cffa5f1f4eda1b6e6aa4",
"typ": "JWT"
"nbf": 1481451903,
"exp": 1481455503,
"iss": "https://localhost:44350",
"aud": [ "https://localhost:44350/resources", "customAPI" ],
"client_id": "oauthClient",
"scope": [ "customAPI.read" ]

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
Headers:
Authorization: Basic Y3VzdG9tQVBJOnNjb3BlU2VjcmV0
Content-Type: application/x-www-form-urlencoded
Body:
token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0M2U4MjljMmI1NzQ4OTk2OTc1M2JhNGY4MjA1OTc5ZGYwZGE5ODhjNjQwY2ZmYTVmMWY0ZWRhMWI2ZTZhYTQiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE0ODE0NTE5MDMsImV4cCI6MTQ4MTQ1NTUwMywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAiLCJhdWQiOlsiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAvcmVzb3VyY2VzIiwiY3VzdG9tQVBJIl0sImNsaWVudF9pZCI6Im9hdXRoQ2xpZW50Iiwic2NvcGUiOlsiY3VzdG9tQVBJLnJlYWQiXX0.D50LeW9265IH695FlykBiWVkKDj-Gjiv-8q-YJl9qV3_jLkTFVeHUaCDuPfe1vd_XVxmx_CwIwmIGPXftKtEcjMiA5WvFB1ToafQ1AqUzRyDgugekWh-i8ODyZRped4SxrlI8OEMcbtTJNzhfDpyeYBiQh7HeQ6URn4eeHq3ePqbJSTPrqsYyG9YpU6azO7XJlNeq_Ml1KZms1lxrkXcETfo7U1h-z66TxpvH4qQRrRcNOY_kejq1x_GD3peWcoKPJ_f4Rbc4B-UvqicslKM44dLNoMDVw_gjKHRCUaaevFlzyS59pwv0UHFAuy4_wyp1uX7ciQOjUPyhl63ZEOX1w

Response:

"nbf": 1481451903,
"exp": 1481455503,
"iss": "https://localhost:44350",
"aud": [ "https://localhost:44350/resources", "customAPI" ],
"client_id": "oauthClient",
"scope": [ "customAPI.read" ],
"active": true

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('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.ps1'))

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

Microsoft.AspNetCore.Mvc
Microsoft.AspNetCore.StaticFiles

And then add to your services (ConfigureServices):

services.AddMvc();

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

app.UseStaticFiles();
app.UseMvcWithDefaultRoute();

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>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        IdentityServerConstants.StandardScopes.Email,
        "role",
        "customAPI.write"
    },
    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:

Microsoft.AspNetCore.Authentication.Cookies
Microsoft.AspNetCore.Authentication.OpenIdConnect

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.

[Authorize]
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:

IdentityServer4.EntityFramework
Microsoft.EntityFrameworkCore.SqlServer

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 and we wouldn't be able to host more than one installation at a time (no load balancing).

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 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, AddInMemoryIdentityResources and AddInMemoryApiResources 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 as a Cli Tool in our csproj:

<ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
</ItemGroup>

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:

IdentityServer4.AspNetIdentity
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer

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 AddTestUsers with:

.AddAspNetIdentity<IdentityUser>()

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

app.UseIdentity();

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 properly, as it is still using a TestUserStore.

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 TestUserStore. Our constructor should now look like this:

private readonly UserManager<IdentityUser> _userManager;
private readonly IIdentityServerInteractionService _interaction;
private readonly IEventService _events;
private readonly AccountService _account;

public AccountController( IIdentityServerInteractionService interaction, IClientStore clientStore, IHttpContextAccessor httpContextAccessor, IEventService events, UserManager<IdentityUser> userManager) { _userManager = userManager; _interaction = interaction; _events = events; _account = new AccountService(interaction, httpContextAccessor, clientStore); }

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

[HttpPost]
[ValidateAntiForgeryToken]
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)) { AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; };
await _events.RaiseAsync(new UserLoginSuccessEvent( identityUser.UserName, identityUser.Id, identityUser.UserName)); await HttpContext.Authentication.SignInAsync(identityUser.Id, identityUser.UserName, props);
if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); }
return Redirect("~/"); }
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); }
var vm = await _account.BuildLoginViewModelAsync(model); return View(vm); }

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

[HttpGet]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl) {
    var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
    var tempUser = info?.Principal;
    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 = info.Properties.Items["scheme"]; 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)); }
AuthenticationProperties props = null; var id_token = info.Properties.GetTokenValue("id_token"); if (id_token != null) { props = new AuthenticationProperties(); props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } }); }
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, userId, user.Id, user.UserName)); await HttpContext.Authentication.SignInAsync(user.Id, user.UserName, provider, additionalClaims.ToArray());
await HttpContext.Authentication.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); }
return Redirect("~/"); }

Job done!

Repositories