IdentityServer4, ASP.NET Core API and a client with username/password

This is an end-to-end guide on how to quickly setup IdentityServer4, use it in your ASP.NET Core API for authentication, and finally login to your API from a client by asking a user for her/his username and password. It is divided in three parts that describe respectively the configuration of each one of the following three systems:

  • IdentityServer4
    Contains instructions on how to setup and configure a token service based on IdentityServer4, that follows the quick-start guides, keeping only the absolutely minimum requirements for this tutorial
  • ASP.NET Core API
    An API configured to use IdentityServer4 as a middleware that adds the spec compliant OpenID Connect and OAuth 2.0 endpoints
  • Client (API Consumer)
    For this post, just a Console Application that consumes a protected resource from the API

These systems interact with each other in a way outside the complete control of a user creating a triangle of communication that prevents man-in-the-middle attacks. To get a general idea about how the information flows between the three systems, study the following sequence flow:

A user is launching the Console Application which immediately requests a protected resource from the API. Since it’s getting a 401 as a response, the Console Application then asks for user’s credentials and with that, it requests an access token from the Identity Server. Finally, the Console Application uses the access token to request -again- the protected resource so the API responds with the protected resource, having first validate the access token with the Identity Server.

IdentityServer4

The Identity Server has three major entities that we have to setup for this tutorial to work, the ApiResource, the Client and a TestUser. All of them will need a minimum configuration, but before we start it is useful to have the following in mind:

  • The Console Application will play the role of the Client. It uses a ClientId & a Secret plus the username and the password of a User to get the token.
  • The ASP.NET Core API will of course be the ApiResource. It uses an ApiName & Secret plus the access token, to get Claims back.
  • A Client must have an ApiResource in their AllowedScopes list in order for the Idenity Server to allow access

Setting up the project

There is an easy way to create a new project for the IdentityServer4! All we need is to create the new project based on IdentityServer4 templates, and in order to do that, we must install the templates and create a new project by following these steps:

  1. Open PowerShell and navigate to a directory that you want your project to be created
  2. Type dotnet new -i IdentityServer4.Templates and hit enter
  3. Type dotnet new is4empty -n IdentityServer and hit enter again

And that’s it! You can now double click the IdentityServer.csproj created, and use Microsoft Visual Studio (or Visual Studio Code) to explore the project. Since there aren’t many files you will easily notice a Config.cs file (it keeps all the initial hard-coded configuration) with 3 IEnumerables: IdentityResources, ApiResources and Clients. For this tutorial we only care about ApiResources and Clients plus, we will also add a fourth method that returns TestUsers (all methods in Config.cs are called from StartUp.cs during service configuration, and we will follow this “pattern” for the TestUsers method).

Setting up the ApiResource

A word of notice before we start: The models of the Identity Server do not always include just properties as someone would expect by a namespace IdentityServer4.Models included in an assembly named IdentityServer4.Storage. Some of those models have constructors with a bit of logic in them (e.g. ApiResource.cs), so before you use them check them. And yes, “some storage models having some constructors with some arguments that you must use instead of the properties” is not cool at all, I spend quite some time searching which models have constructors and with what arguments.

Adding an ApiResource is quite straight forward (that is, if you read the notice above): Just use the constructor to pass the name and assign a Secret to the ApiSecrets property:

Behind the scenes, that constructor adds the “ApiName” as an AllowedScope for that ApiResource

Setting up the Client

Clients can be a little more complex though! Besides the ClientId and a Secret, we have to set the AllowedGrantTypes and the AllowedScopes properties:

GrantTypes.ResourceOwnerPassword simply means allow a client to send username and password to the token service and get an access token back that represents that user. Also note that we added the API name as an allowed scope of the client ...AllowedScopes = { "ApiName" }... (simply as string, no type safety here). Apparently, the Clients don’t need to add themselves in their own AllowedScope, that is why there is no construction here.

There are two types of access tokens, JWT and Reference. Default is JWT, a self-contained access token which is essentially a protected data structure with claims and an expiration. Read more about pros and cons of each solution here.

Setting up a TestUser

Setting up a user store is probably a blog post on its own (and it might be in the future), so for this tutorial we will only add test users to the Identity Server. To do so, we will need to use the .AddTestUsers() extension method and pass a list of test users, which can be returned by a static method in the Config.cs file like so:

Sort of self-explanatory I think! Keep in mind this is the bare minimum to pass the authentication. In a real life scenario you will need claims that you can add to the Claims property of the TestUser like this: Claims = { new Claim(JwtClaimTypes.Role, "SomeRole") }. Finally, as the rest of the static methods in the Config.cs file, make the call to add the users in the ConfigureServices method of the StartUp.cs:

Important!
ApiSecrets, ClientSecrets and TestUser.Password should be real secrets, e.g. random strings!

ASP.NET Core API

Enabling authentication capabilities to your API requires the addition and configuration of the Authentication middleware. After that, we can add the AuthorizeFilter either while configuring the container or use the [Authorize] decoration attribute that is just a comfortable wrapper of the AuthorizeFilter.
Three straight forward steps are needed to complete this:

Add the necessary nuget packages

Just right click on your Dependencies and add the following two nuget packages:

Add the necessary services to the container

First of all, I like Authentication by default. I find it unsafe to have to remember to add the [Authorize] attribute to each Action. I prefer to explicitly allow access for the actions that I am absolutely certain that need to be available for everyone with the [AllowAnonymous] decoration attribute. Having stated this, we will need to add the AuthorizeFilter to all actions using the FilterCollection:

In a role-based or claims-based environment you can still have authentication by default and decorate your actions with the [Authorize] attribute. MVC will then add a second AuthorizeFilter to its filter pipeline, with a drawback of course, the one of double execution. To minimise (or vanish) this negative effect either don’t use authentication by default, or choose JWT access tokens because once an API has learned about the key material, it can validate self-contained tokens without needing to communicate with the issuer.

Immediately after the AddMvcCore add the authorization service to the services collection with AddAuthorization(). You can also use the AuthorizationOptions to configure Claims-based or Policy-based authorization.

Add AddApiExplorer() at the end if you are using Swagger. Read more here…

After this basic setup, you have to configure the authentication. You do that with the AddAuthentication method. Since we will be using a OAuth 2.0 Bearer Token, we also need to pass that to the method. Following this, we add the identity server authentication configuration with the .AddIdentityServerAuthentication() method, in which we have to set the URL of the IdentityServer, the ApiName and of course the secret:

It’s a good idea to use caching and avoid asking the IdentityServer each and every time. Do that with the EnableCaching and CacheDuration options.

Configure the HTTP request pipeline

Easiest step! Just add the AuthenticationMiddleware in the pipeline but be careful where! It needs to be before all other middlewares, that is first in the pipeline:

Client (API Consumer)

The Authorization: <type> <credentials> pattern was introduced by the W3C in HTTP 1.0, and this is what we need to follow regardless the client. That means, that although for this tutorial though it will just be a .NET Core Console Application, the core mechanism of how a client gets authenticated with a username and password remains the same.

The complete code, the one that also handles the 401 response described in the sequence diagram, can be found on my github account.

Add the necessary nuget packages

Just right click on your Dependencies and add the following nuget package:

Get the token from the Identity Server

There are quite a few things to setup in order to make the call and get a token back. Besides the address of the token service (which is in the form of https://localhost:5000/connect/token, we will need Client’s and User credentials, and the GrandType, which for us is “password”:

The Password grant type (GrantType = "password") is used by first-party clients to exchange a user’s credentials for an access token and thus, it should not be used by third-party clients. It simply means, exchange the user’s username and password with an access token.

Add the authorization header

After a successful authentication we should have the AccessToken in the response. We can now use that and add it as a AuthenticationHeader in the DefaultRequestHeaders:

Code Sample

The entire sample, constisting of a setup of the IdentityServer4, a ASP.NET Core API and a Console Application acting as a first-party client, can be found on my GitHub account under the repository https://github.com/georgekosmidis/IdentityServer4Tests