Introduction

I got many questions how the ASP.NET Core ApiBoilerplate protects the API endpoints and which URL validates the token. If you are new to ASP.NET Core and are using the template to get started, chances are you might ask the same question to yourself. I hope this post clears some confusion about it.

I'll try to make this post as generic as possible so that you can still apply the same concept even if you are not using the ApiBoilerplate template.

Getting Started

In ASP.NET Core or even in traditional Web APIs, we would normally decorate our Controllers or Actions with the [Authorize] attribute to secure our Api endpoints from anonymous, unauthorized or unauthenticated users:

Controller Level Restriction

[Authorize]
public class PersonController: ControllerBase  
{
   //rest of the code here...
}

Action Level Restriction

[HttpGet]
[Authorize]
public async Task<IEnumerable<PersonResponse>> Get()  
{
   //rest of the code here...
}

The [Authorize] attribute provides filters for users and roles and it’s fairly easy to implement it if you are using membership/identity provider. You can even customize it's behavior by extending the [AuthorizeAttribute] class based on your custom needs. There are two ways in which we can implement authorization in ASP.NET Core. These include role-based authorization and policy-based authorization. From the example above, it doesn't specify any arguments for the [Authorize] attribute, which means that it only checks if the user is authenticated. This is sufficient enough for the purpose of this demo.

There are many possible options that we can use to secure our APIs, a few of the most popular are using OAuth 2.0 Access TokensApi Keys or JSON Web Tokens(JWT). Here's an article explaining their differences: API Keys vs OAuth Tokens vs JSON Web Tokens

In this demo we are going to use JWT as access token which contains a JSON data payload that is signed and serialized.

Why JWT?

Because it's simple and a great technology for authenticating APIs and server-to-server authorization. I'm not going to cover the details about JWT as there are already tons of guides about this, and I personally recommend this one: Introduction to JSON Web Tokens

Please keep in mind that a JWT guarantees data ownership but not encryption; the JSON data you store into a JWT can be seen by anyone that intercepts the token, as it’s just serialized, not encrypted. In other words, using JWT doesn't make our API invulnerable. For this reason, an extra layer of protection that always goes hand in hand with JWT is to secure all our network traffic with an HTTPS connection.

What IdentityServer4 Is?

IdentityServer4 an OpenID Connect and OAuth 2.0 framework that provides a set of services and middleware for ASP.NET Core apps. I won't be covering all features in this post, and I would recommend you to head over to the official documentation page to see what features it provides.

IdentityServer4 supports multiple protocol flows or grant types such as Authorization CodeClient CredentialsRefresh TokenImplicit and etc. In this post we are going to take a look at the Client Credentials flow.

Client Credentials Flow

Client Credentials Flow is a process in which client apps use client_idclient_secret and sometimes a scope in exchange for an access_token to access a protected resource.

This flow is the recommended way to secure APIs easily without a particular user connected, mostly this approach is better in server-to-server scenarios, when interconnected internal applications within a system need to authenticate without Login UI to present username and password.

For more information about IdentityServer 4 supported grant types, see: Grant Types

But What the Heck is OAuth and OpenID Connect (OIDC)?

Many folks that I know virtually in the forums and in person were asking about the distinctions between the two terms: OAuth and OpenID. I thought I'd give a brief overview about them in this post to help you better understand each terms.

In general, OpenID is about authentication (proving who you are (a.k.a identity)). Authentication is a process in which a user provides credentials, typically in a form of username and password that are then compared to those stored in an database, application or resource. OAuth on the other hand is about authorization (to grant access to files/resource/data without having to deal with the original authentication). Authorization refers to the process that determines what a user is allowed to do after they have been authenticated. The other thing called OpenID Connect does both.

We will use .NETCore 3.1 and IdentityServer4 framework to provide most of the security features that an application requires based OAuth 2.0 and OIDC protocol implementation. This enables a third-party app to obtain a limited access to an HTTP service and APIs within your server premise. Instead of using resource owner's credentials to directly access a protected resource from our APIs, the client obtains an access token. In OAuth 2.0, access tokens are normally a string denoting a specific scope, lifetime and other access attributes.

Always remember that:

  • OAuth 2.0 is an authorization protocol that specifies how tokens are transferred.There is no defined structure for the token required by the spec, which means you can generate a string and implement tokens however you want.
  • JWT defines a token format.
  • OAuth 2.0 can use JWT as a token format. This is why we will use JWT in concert with OAuth to obtain an access token.

Just to give you a quick overview, here's a glossary of OAuth terms:

  • Resource Owner (a.k.a the User) - An entity capable of granting access to a protected resource.
  • Resource Server (a.k.a your ASP.NET Core APIs) - The server hosting the protected resource, capable of accepting and responding to protected resource requests using access tokens.
  • Client - An application (desktop, web, service or mobile app) making protected resource requests on behalf of the resource owner and with its authorization.
  • Token Server - The service issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.

We're not going to cover Resource Owner in this example as we won't be doing with user authentication that allow users to input their account credentials. Instead, we will focus on API to API authentication and authorization using client credentials grant type.

To make it clearer, here's a simple diagram describing the high-level flow on how each component interact with each other.

Building a Token Server with IdentityServer4

Let's build a simple Token Server using IdentityServer4 that authorizes internal/external client apps for accessing a certain Resource Server. You can think of it as a system that generates a simple data structure containing Authorization and/or Authentication information. In this post, we'll only cover the OAuth aspect of IdentityServer4 to generate an access_token in a form of JWT for authorizing access. This would be the "keys to the house", so to speak, letting you through the doorway and into the residence of a protected resource, usually an ASP.NET Core Web APIs. A good practice is to host this server as a separate service and not implement it within your web application specific apps.

For this particular demo, I'm going to use the following tools and frameworks:

  • Visual Studio 2019
  • .NET Core 3.1
  • ASP.NET Core 3.1
  • IdentityServer4
  • IdentityServer4.AccessTokenValidation
  • ApiBoilerPlate.AspNetCore

To begin, let's go ahead and fire up Visual Studio 2019. Create a new ASP.NET Core Web Application project with an Empty project template and make sure Authentication option is unchecked.

Install the latest version of IdentityServer4 Nuget Package:

PM> Install-Package IdentityServer4 -Version 3.1.1  

Note: The latest version as of this time of writing is 3.1.1.

For the sole purpose of this demo, we'll just use an In-Memory data store to keep the list of clients and resources. In real/production applications, you should store these data in a persistent data store such as a database. IndentityServer4 has persistent layer support baked for Entity Framework Core and you can use it to generate the schema needed for storing clients, scopes and grants.

Now, let's a create a couple of static internal classes that house some test data for clients and resources.

Configuring API Resources

Here's the code for our test API resources:

using IdentityServer4.Models;  
using System.Collections.Generic;

namespace IdentityServer4Demo.TokenServer.Data  
{
    internal static class ResourceManager
    {
        public static IEnumerable<ApiResource> Apis =>
            new List<ApiResource>
            {
                new ApiResource {
                    Name = "app.api.whatever",
                    DisplayName = "Whatever Apis",
                    ApiSecrets = { new Secret("a75a559d-1dab-4c65-9bc0-f8e590cb388d".Sha256()) },
                    Scopes = new List<Scope> {
                        new Scope("app.api.whatever.read"),
                        new Scope("app.api.whatever.write"),
                        new Scope("app.api.whatever.full")
                    }
                },
                new ApiResource("app.api.weather","Weather Apis")
            };
    }
}

The ApiResource object is a class that lives within IdentityServer4.Models namespace and is built-in to IdentityServer4. This enable us to mock API Resources (a.k.a ASP.NET Core / Web APIs) that we wish to protect. From the code example above, we are modelling two APIs that we want to protect: The app.api.whatever and app.api.weather.

Notice that each ApiResource object are defined differently. The first item uses the default constructor with no parameters. You would use this approach if you wanted to configure multiple scopes per API. In this example, we've defined three scopes: app.api.whatever.readapp.api.whatever.write and app.api.whatever.full.

The second item uses the constructor parameter for convenience. You would typically use this for simpler scenarios where you only require one scope per API. In this case, the app.api.weather name will automatically become a scope for the resource.

Note: A couple of things to keep in mind here is that each scopes should be unique, and that is why it's recommended to define your scopes based on each API unique name. You can also define the ApiResource in JSON configuration file (appsettings.json) and then pass the configuration section to the AddInMemoryApiResource method.

The next thing that we need to do is to setup some test clients that we want to authorize access to our configured ApiResources.

Configuring Clients

Here's the code for our test clients:

using IdentityServer4.Models;  
using System.Collections.Generic;

namespace IdentityServer4Demo.TokenServer.Data  
{
    internal static class ClientManager
    {
        public static IEnumerable<Client> Clients =>
            new List<Client>
            {
                    new Client
                    {
                         ClientName = "Client Application1",
                         ClientId = "t8agr5xKt4$3",
                         AllowedGrantTypes = GrantTypes.ClientCredentials,
                         ClientSecrets = { new Secret("eb300de4-add9-42f4-a3ac-abd3c60f1919".Sha256()) },
                         AllowedScopes = new List<string> { "app.api.whatever.read", "app.api.whatever.write" }
                    },
                    new Client
                    {
                         ClientName = "Client Application2",
                         ClientId = "3X=nNv?Sgu$S",
                         AllowedGrantTypes = GrantTypes.ClientCredentials,
                         ClientSecrets = { new Secret("1554db43-3015-47a8-a748-55bd76b6af48".Sha256()) },
                         AllowedScopes = { "app.api.weather" }
                    }
            };
    }
}

The code above is nothing but a simple static method that returns a List of test clients. Each client has a unique ClientNameClientIdClientSecrets and AllowedScopes, but all have the same AllowedGrantTypes set to GrantTypes.ClientCredentials.

A few things to keep in mind:

  • The OAuth Client Credentials grant type requires ClientId and ClientSecrets to authorize access.
  • The ClientId in this example uses a random string that is hashed using the Sha256() extension method built-in to IdentityServer4. You are free to use whatever format for secrets based on your requirements.
  • The ClientSecrets in this example uses a UUID that is also hashed using the Sha256() extension method. Again, you are free to use whatever format for secrets based on your requirements.
  • Client applications will use the ClientId and ClientSecrets to request a Token from IdentityServer4.
  • The AllowedScopes attribute defines the scopes that a certain client is allowed to access. In this case our configured API resources. Scopes can be used to restrict access to a resource based on read/write permissions.
  • In IdentityServer4 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, while an API resource scope allows you to model access to a protected resource/API. We won't be covering identity resource in this post.
  • The values we've set are just examples, you would want to change those values to whatever to you want.

Configuring IdentityServer4

Now that we have in-memory test clients and API resources configured, next is to enable IdentityServer4. The good thing about IdentityServer4 is it provides various in-memory configurations for us to easily configure IdentityServer from our configured in-memory objects.

Add the following code at ConfigureServices method of Startup.cs file:

public void ConfigureServices(IServiceCollection services)  
{
    services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryApiResources(Data.ResourceManager.Apis)
            .AddInMemoryClients(Data.ClientManager.Clients);

}

The code above registers IdentityServer and it's dependencies in DI Container.

Since a signing certificate is required for signing and validating tokens, we've used the AddDeveloperSigningCredential() provided by IdentityServer4 to automatically generate a signing credential for us to test OAuth functionality during development and prototyping. In real applications, you should consider using AddSigningCredential() instead and provide an asymmetric key pair and signing algorithm to sign and validate tokens.

The AddInMemoryApiResources()and AddInMemoryClients() middlewares register our in-memory stores for our configured clients and API resources example.

Finally, add the following code at Configure method of Startup.cs file:

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

That simple! We now have a simple Token Server for generating JWTs.

Requesting a Token

IdentityServer4 provides an OIDC discovery endpoint, which can be used to retrieve metadata about the authorization server including the Token Endpoint. The discovery endpoint is available via /.well-known/openid-configuration relative to the base address of your Token Server. For example, if we run the application locally and perform a GET request to the following endpoint:

https://localhost:44354/.well-known/openid-configuration  

We will then be presented with the following JSON schema below:

{
    "issuer": "https://localhost:44354",
    "jwks_uri": "https://localhost:44354/.well-known/openid-configuration/jwks",
    "authorization_endpoint": "https://localhost:44354/connect/authorize",
    "token_endpoint": "https://localhost:44354/connect/token",
    "userinfo_endpoint": "https://localhost:44354/connect/userinfo",
    "end_session_endpoint": "https://localhost:44354/connect/endsession",
    "check_session_iframe": "https://localhost:44354/connect/checksession",
    "revocation_endpoint": "https://localhost:44354/connect/revocation",
    "introspection_endpoint": "https://localhost:44354/connect/introspect",
    "device_authorization_endpoint": "https://localhost:44354/connect/deviceauthorization",
    "frontchannel_logout_supported": true,
    "frontchannel_logout_session_supported": true,
    "backchannel_logout_supported": true,
    "backchannel_logout_session_supported": true,
    "scopes_supported": [
        "app.api.whatever.read",
        "app.api.whatever.write",
        "app.api.whatever.full",
        "app.api.weather",
        "offline_access"
    ],
    "claims_supported": [],
    "grant_types_supported": [
        "authorization_code",
        "client_credentials",
        "refresh_token",
        "implicit",
        "urn:ietf:params:oauth:grant-type:device_code"
    ],
    "response_types_supported": [
        "code",
        "token",
        "id_token",
        "id_token token",
        "code id_token",
        "code token",
        "code id_token token"
    ],
    "response_modes_supported": [
        "form_post",
        "query",
        "fragment"
    ],
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "subject_types_supported": [
        "public"
    ],
    "code_challenge_methods_supported": [
        "plain",
        "S256"
    ],
    "request_parameter_supported": true
}

We can see that the Token Endpoint can be accessed at: https://<YOUR-TOKENSERVER-DOMAIN>/connect/token endpoint. Also notice the app.api.whatever.readapp.api.whatever.writeapp.api.whatever.full and app.api.weather scopes that we have configured earlier are added under the scopes_supported attribute.

Now, let's do a quick test for requesting a JWT using the Clients we've configured by issuing a POST Http request in POSTMAN:

POST /connect/token HTTP/1.1  
Host: localhost:44354  
Content-Type: application/x-www-form-urlencoded  
client_id=t8agr5xKt4$3&  
client_secret=eb300de4-add9-42f4-a3ac-abd3c60f1919&  
grant_type=client_credentials&  
scope=app.api.whatever.read app.api.whatever.write  

We should be presented with the following Http response in JSON format:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlJMUGVHQVhlQkluZFdsdVFwclBBdEEiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODE5MjM5MzQsImV4cCI6MTU4MTkyNzUzNCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTQiLCJhdWQiOiJhcHAuYXBpLndoYXRldmVyIiwiY2xpZW50X2lkIjoidDhhZ3I1eEt0NCQzIiwic2NvcGUiOlsiYXBwLmFwaS53aGF0ZXZlci5yZWFkIiwiYXBwLmFwaS53aGF0ZXZlci53cml0ZSJdfQ.lWQflS4xUe8hPcPHPjaTenOu7XkSWtsLHY4IGqLxu4AUtM0Ki8XSS2vEaLBfp5rIxgvjSMKbwv2SMJJCOPeB8Ck0L62ohldmAvs2fhiYYNNg4_Oz3ljVfbQz6zdP8xAVc6LKXXVM3Ed8GO_yRAgFDOOCfpiimj81h4QPd8yCrpWvHDihxsvwtCVGBQVQRMEv85fYhWoJ8qeX4sj2sW-dcuLzj-DFsPfqcX-BggXw5O4JVpmQ8QEUNCX1NCkLe8wYhu4GCAwTLK-umhirCSNzBhmAuIV3sXoPWa6VsYK4qJn8OVEOG0vDr8Jd394uauc6NYyxcZsf1qgxPa8-LRm_xA",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "app.api.whatever.read app.api.whatever.write"
}

The value in access_token attribute is the JWT. To verify the content and signature inside JWT, we can use an online tool called jwt.io to decode the value. Here's the decoded metadata:

HEADER:ALGORITHM & TOKEN TYPE

{
  "alg": "RS256",
  "kid": "RLPeGAXeBIndWluQprPAtA",
  "typ": "at+jwt"
}

The result above contains a public key Id (kid) and other attributes that will be used by client applications to verify the JWT signature. These attributes are accessible to client applications via the .well-known/openid-configuration/jwks endpoint in the OpenID Connect discovery document:

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "RLPeGAXeBIndWluQprPAtA",
            "e": "AQAB",
            "n": "vkz4dwnAFRZ0KmoI4OXFgd53217md67N-egnTKlJQIF4buNjdpLuGzmivTS7_bkaJa4EnRk9O4LA2E59b_q-hDKV34XPpl5FEnr8SOmeNU2BhFDwKxVodqbw4ovkUH3pH5UOcCOIH-_jYBLxwFK2tsJitn_rfDx1bd_W0OnaFqrrgghYMZqf7EYRxCvqrWl6TURtY_zdvWJOWIWiwI7b8D39AxELGbWPpj9oP7sBFmeImqiPnJsnP3626aiB5FMBJeFKF0lwP86x9ZCSFDVMMQcJsrs-Fr6grWzGq-wR7FChfFhZulQ0vH610x323A491yEpYvyZRXjhEqgtOQRthQ",
            "alg": "RS256"
        }
    ]
}

The claims above are part of the signing credentials generated by AddDeveloperSigningCredential() call. The generated developer signing credentials will be persisted in a file called tempkey.rsa. By default, the file is created at the root directory of our application. The tempkey.rsa stores the required 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 a private key used to sign the token and a public key to verify the signature.

Note: You don't have to check-in the tempkey.rsa file into your source control / git repository, it will be re-created if it is not present.

PAYLOAD:DATA

{
  "nbf": 1581923934,
  "exp": 1581927534,
  "iss": "https://localhost:44354",
  "aud": "app.api.whatever",
  "client_id": "t8agr5xKt4$3",
  "scope": [
    "app.api.whatever.read",
    "app.api.whatever.write"
  ]
}

The claims above are the basic metadata for a JWT.

  • nbf stands for not before.
  • exp stands for expiry.
  • iss stands for issuer.
  • aud stands for audience. The resource name in which a client is needed to access.
  • client_id the client id of the client application requesting the token.
  • scope the scope in which a client is allowed to access.

Validating a Token

Since we don't have an ASP.NET Core API application that we can use to test yet, we can take advantage of the token introspection endpoint of IdentityServer4 to validate the token. This simulates as if we were an OAuth resource receiving it from an external client.

The token introspection is available at https://localhost:5001/connect/introspect endpoint. In POSTMAN, we can use Basic Auth and set the ApiResource name as the username and ApiSecret as the password. These values will then be encoded to Base64 string. Finally, we can set the JWT as the token parameter. Here's an example POST Http request:

POST /connect/introspect HTTP/1.1  
Host: localhost:5001  
Content-Type: application/x-www-form-urlencoded  
Authorization: Basic YXBwLmFwaS53aGF0ZXZlcjphNzVhNTU5ZC0xZGFiLTRjNjUtOWJjMC1mOGU1OTBjYjM4OGQ=  
token=eyJhbGciOiJSUzI1NiIsImtpZCI6IlJMUGVHQVhlQkluZFdsdVFwclBBdEEiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODE5MjQ5MTMsImV4cCI6MTU4MTkyODUxMywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImF1ZCI6ImFwcC5hcGkud2hhdGV2ZXIiLCJjbGllbnRfaWQiOiJ0OGFncjV4S3Q0JDMiLCJzY29wZSI6WyJhcHAuYXBpLndoYXRldmVyLnJlYWQiLCJhcHAuYXBpLndoYXRldmVyLndyaXRlIl19.TPhU1v8xeOf7WEu1ApS4Vc5Is_s5ciEYXOrRxk7fEl-5CwAh00psuMtmZ57R6avOmg1kLrcHZQUSNS7UbbIisTcJm6GAdtpuKWvbCV4svagaWKfqntmCJenfYXCJ1ETrTvFYLIW5uSX2sSU8Ve2W8LQOmwlNREvf3QUXFiyODK9S-NO1q2hdVB_cVtVC5EdxSDYbvwJHq-qoz01vL_jdAJl-XBBOqSOGr2htwqsukv6IpvrhPaJzZ6s8oWZ5VOUos1LLc6AeFHmdaTkHqjM34jdmVV2kZ4SycfbM4I6crxTzkK0ou1BCG9e79fsliDrf3OrLmb6rEKSuNZk14s4X_Q  

If successful, we’ll receive the following claims in a given token echoed back to us:

{
    "nbf": 1581924913,
    "exp": 1581928513,
    "iss": "https://localhost:5001",
    "aud": "app.api.whatever",
    "client_id": "t8agr5xKt4$3",
    "active": true,
    "scope": "app.api.whatever.read app.api.whatever.write"
}

Creating A Simple API Resource

Let's create an ASP.NET Core API that will act as an audience for our Token Server. Add a new project to our exiting solution. Create a new ASP.NET Core Web Application project and select API project template.

Visual Studio scaffolds all the necessary files and dependencies to help you get started building RESTful APIs in ASP.NET Core. The generated template includes a WeatherForecastController to simulate a simple GET Http request using a static data as shown in the code below:

namespace IdentityServer4Demo.WeatherApi.Controllers  
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", 
            "Cool", "Mild", "Warm", "Balmy", 
            "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

Notice that the Controller and Action method isn't protected by default. This means that we should be able to access the /WeatherForecast endpoint by invoking a GET Http request:

Let's run the application to ensure that our API is working as we expected. You should be presented with the following output in JSON format:

[
    {
        "date": "2020-02-18T10:23:51.0316366-06:00",
        "temperatureC": 31,
        "temperatureF": 87,
        "summary": "Mild"
    },
    {
        "date": "2020-02-19T10:23:51.0316788-06:00",
        "temperatureC": 36,
        "temperatureF": 96,
        "summary": "Warm"
    },
    {
        "date": "2020-02-20T10:23:51.0316796-06:00",
        "temperatureC": 22,
        "temperatureF": 71,
        "summary": "Cool"
    },
    {
        "date": "2020-02-21T10:23:51.0316802-06:00",
        "temperatureC": -5,
        "temperatureF": 24,
        "summary": "Cool"
    },
    {
        "date": "2020-02-22T10:23:51.0316805-06:00",
        "temperatureC": -18,
        "temperatureF": 0,
        "summary": "Bracing"
    }
]

Great!

Protecting the API with JWT

Now let's secure the WeatherForecast endpoints. First, install IdentityServer4.AccessTokenValidation:

PM> Install-Package IdentityServer4.AccessTokenValidation -Version 3.0.1  

IdentityServer4.AccessTokenValidation is an ASP.NET Core authentication handler to validate JWT and reference tokens from IdentityServer4. Now, let's setup JWT Authentication Handler with IdentityServer4 by adding the following code at ConfigureServices method of Startup.cs file:

public void ConfigureServices(IServiceCollection services)  
{
    services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = "https://localhost:44354";
                options.ApiName = "app.api.weather";
            });

    services.AddControllers();
}

The code above adds Authentication support using "Bearer" as the default scheme. It then configures IdentityServer Authentication handler. The Authority is the base Url to where your IdentityServer is hosted, in this example our Token Server sits at https://localhost:44354. The ApiName should be registered in your IdentityServer as an Audience. The RequireHttpsMetadata property is turned off by default and you should turn it on when you deploy the app in production.

When our APIs are decorated with the [Authorize] attribute, then the requesting clients should provide the access token generated from IdentityServer and pass it as a Bearer Authorization Header before they can be granted access to our API endpoints.

Now let's go back the WeatherForecast Controller and then decorate it with the [Authorize] attribute:

[ApiController]
[Authorize]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase  
{
    //...removed for brevity
}

Finally, add the UseAuthentication() middleware to the pipeline between UseRouting() and UseAuthorization() calls:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{
    //..removed for brevity

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

    //..removed for brevity
}

That simple! We now have a simple API Resource that is protected by JWT Bearer Authentication scheme.

Testing the API Endpoint

Since this demo is composed of two projects and our ASP.NET Core API application relies on our Token Server application, we need to make the Token Server accessible when testing the API Resource. Typically, we will host or deploy both projects in a web server to be able to connect between them. Luckily, one of the cool features since Visual Studio 2017 is to enable multiple start-up projects. This means that we could run both applications simultaneously within Visual Studio. All you need to do is:

  1. Right click on the Solution
  2. Select Set Startup Projects...
  3. Select Multiple Startup Projects radio button
  4. Select Start as the action for both ASP.NET Core Api and Token Server project
  5. Click Apply and then OK
  6. Now build and run the application.

You will see that our /WeatherForecast GET endpoint is now returning an Http 401 Error, and means we are now Unauthorize to access that endpoint:

In order for us to be authorized, we need a JWT bearer from our Token Server. Let's use POSTMAN once again to request a JWT, and this time we will use the Client Application2 credential since our WeatherForecast application use the app.api.weather as the ApiName which we already have configured in our Token Server earlier. Here's the sample POST Http request for getting a JWT:

POST /connect/token HTTP/1.1  
Host: localhost:44354  
Content-Type: application/x-www-form-urlencoded  
client_id=3X=nNv?Sgu$S&  
client_secret=1554db43-3015-47a8-a748-55bd76b6af48&  
grant_type=client_credentials&  
scope=app.api.weather  

The response should give us the JWT contained in access_token attribute:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlJMUGVHQVhlQkluZFdsdVFwclBBdEEiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODE5NjE1ODgsImV4cCI6MTU4MTk2NTE4OCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTQiLCJhdWQiOiJhcHAuYXBpLndlYXRoZXIiLCJjbGllbnRfaWQiOiIzWD1uTnY_U2d1JFMiLCJzY29wZSI6WyJhcHAuYXBpLndlYXRoZXIiXX0.tlL_-h8cXxogjOgqvU6tnQwGo9hjAHZS36MIi-M80Q4EqdGSobx1UJM0erd14YolYjtFpAHOr6HajUEzZBePnnkUhWt3zE1Jom6NKMmMB0wWOX8fiEoDtSetrbJNtQ24gOTRRA4mAJobNQtqERYmq0CUCwwcCQ3x7w0igiynZbYTAZPGz_yBDy0XK8Q_SIikD7x9M6OWizvOzcYEobzeVa5Sdt7nE5T4-0vpEllf2-yrF63rxthiZ4WYuT8qFXcFHV1MbzB2skmr66uGyX4Uqs-QZeIt-h0rSQSY6FMprlQf6xwXAk0L8H7SIIWiYFf6u2SHe8CUb1NJsFjZTbed2w",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "app.api.weather"
}

Copy the value of access_token and then create a new tab in POSTMAN to test out our /WeatherForecast endpoint. Here's the sample GET Http request with the Bearer Authorization header:

GET /weatherforecast HTTP/1.1  
Host: localhost:44380  
Content-Type: application/json  
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IlJMUGVHQVhlQkluZFdsdVFwclBBdEEiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODE5NjE1ODgsImV4cCI6MTU4MTk2NTE4OCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTQiLCJhdWQiOiJhcHAuYXBpLndlYXRoZXIiLCJjbGllbnRfaWQiOiIzWD1uTnY_U2d1JFMiLCJzY29wZSI6WyJhcHAuYXBpLndlYXRoZXIiXX0.tlL_-h8cXxogjOgqvU6tnQwGo9hjAHZS36MIi-M80Q4EqdGSobx1UJM0erd14YolYjtFpAHOr6HajUEzZBePnnkUhWt3zE1Jom6NKMmMB0wWOX8fiEoDtSetrbJNtQ24gOTRRA4mAJobNQtqERYmq0CUCwwcCQ3x7w0igiynZbYTAZPGz_yBDy0XK8Q_SIikD7x9M6OWizvOzcYEobzeVa5Sdt7nE5T4-0vpEllf2-yrF63rxthiZ4WYuT8qFXcFHV1MbzB2skmr66uGyX4Uqs-QZeIt-h0rSQSY6FMprlQf6xwXAk0L8H7SIIWiYFf6u2SHe8CUb1NJsFjZTbed2w  

Now we should be able to see a Http Status 200 back with the response from /weatherforecast GET call:

ApiBoilerPlate AccessTokenValidation Integration

If you are reading this post and haven't tried using ApiBoilerPlate for building your ASP.NET Core APIs, then head over to the official repository here: https://github.com/proudmonkey/ApiBoilerPlate.

The template uses IdentityServer4 AccessTokenValidation as well to authenticate and validate access tokens. You can find the code that configures IdentityServer Authentication under Installers/RegisterIdentityServerAuthentication.cs file. Here’s the code snippet:

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)  
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = config["ApiResourceBaseUrls:AuthServer"];
        options.RequireHttpsMetadata = false;
        options.ApiName = "api.boilerplate.core";
});

The template automatically handles requesting a token for you. At line 61 of Infrastructure/Installers/RegisterApiResources.cs class, you can find that the OIDC Discover Endpoint is injected as a Singleton:

services.AddSingleton<IDiscoveryCache>(r =>  
{
    var factory = r.GetRequiredService<IHttpClientFactory>();
    return new DiscoveryCache(config["ApiResourceBaseUrls:AuthServer"], () => factory.CreateClient());
});

This means that everytime you instantiate the HttpClient object, the discovery endpoint is binded automatically, allowing you to call the token endpoint directly without configuring them all over again. The DiscoveryCache object pulls the base Url of the Token Server configured in appsettings.json:

"ApiResourceBaseUrls": {
    "AuthServer": "https://localhost:5000"
}

Keep in mind that you need to change the value of AuthServer attribute to where your Token Server is hosted.

Requesting Client Credentials Token

You can see how a token request is being configured at Services/AuthServerConnect.cs class:

namespace ApiBoilerPlate.Services  
{
    public class AuthServerConnect : IAuthServerConnect
    {
        private readonly HttpClient _httpClient;
        private readonly IDiscoveryCache _discoveryCache;
        private readonly ILogger<AuthServerConnect> _logger;
        private readonly IConfiguration _config;

        public AuthServerConnect(HttpClient httpClient, IConfiguration config, IDiscoveryCache discoveryCache, ILogger<AuthServerConnect> logger)
        {
            _httpClient = httpClient;
            _config = config;
            _discoveryCache = discoveryCache;
            _logger = logger;
        }
        public async Task<string> RequestClientCredentialsTokenAsync()
        {

            var endPointDiscovery = await _discoveryCache.GetAsync();
            if (endPointDiscovery.IsError)
            {
                _logger.Log(LogLevel.Error, $"ErrorType: {endPointDiscovery.ErrorType} Error: {endPointDiscovery.Error}");
                throw new HttpRequestException("Something went wrong while connecting to the AuthServer Token Endpoint.");
            }

            var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            {
                Address = endPointDiscovery.TokenEndpoint,
                ClientId = _config["Self:Id"],
                ClientSecret = _config["Self:Secret"],
                Scope = "SampleApiResource"
            });

            if (tokenResponse.IsError)
            {
                _logger.Log(LogLevel.Error, $"ErrorType: {tokenResponse.ErrorType} Error: {tokenResponse.Error}");
                throw new HttpRequestException("Something went wrong while requesting Token to the AuthServer.");
            }

            return tokenResponse.AccessToken;
        }
    }
}

The code snippet above request an access token from IndentityServer Token endpoint via the DiscoveryCache by passing the registered ClientIdClientSecret and Scope in your Token Server. The ClientId and ClientSecret are also contained appsettings.json file:

"Self": {
    "Id": "api.boilerplate.core",
    "Secret": "0a2e472b-f263-43fd-8372-3b13f5acf222"
}

Tip: In production or real application, you should consider storing the secrets and keys in a more secured location like database or cloud vault.

The RequestClientCredentialsTokenAsync() method will then be called each time you issue an Http requests via HttpClient. This process is encapsulated in a custom bearer token DelegatingHandler class called ProtectedApiBearerTokenHandler. Here’s the code snippet:

namespace ApiBoilerPlate.Infrastructure.Handlers  
{
    public class ProtectedApiBearerTokenHandler : DelegatingHandler
    {
        private readonly IAuthServerConnect _authServerConnect;

        public ProtectedApiBearerTokenHandler(IAuthServerConnect authServerConnect)
        {
            _authServerConnect = authServerConnect;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // request the access token
            var accessToken = await _authServerConnect.RequestClientCredentialsTokenAsync();

            // set the bearer token to the outgoing request as Authentication Header
            request.SetBearerToken(accessToken);

            // Proceed calling the inner handler, that will actually send the requestto our protected api
            return await base.SendAsync(request, cancellationToken);
        }
    }
}

We can then register the ProtectedApiBearerTokenHandler as a Transient service in ConfigureServices method in Startup.cs:

services.AddTransient<ProtectedApiBearerTokenHandler>();  

That's it! I hope you'll find this post helpful!

Source Code

The source code for this demo is available in my GitHub repository here: https://github.com/proudmonkey/IdentityServer4Demo