Securing a .NET 4.6.x API with OpenID Connect
Tags: security, dotnet, csharp, openid
Published: 2021-09-08 21:16:51

## Introduction

OpenID Connect is powerful... and confusing. If you have ever experienced the frustration of adding authentication/authorization to an API built on .NET 4.6.x where the token comes from an OpenID Server, then you have come to the right place. I have accomplished this and can help you implement it in this guide.

Here are the requirements of the system we will be implementing.

- protect legacy API from unauthorized access
- only requests that include a valid bearer token (`access_token` from identity server) will be allowed
- identity server should issue a valid `access_token` to our configured client

## Protecting the API

In this particular instance, I want to protect a legacy application from unauthorized access. That kind of requirement calls for an API resource to be made. API resources are configured on the identity server and are used to protect API(s) from unauthorized access. So, I added the following code to my self hosted identity server which sits on a .NET core application.

```csharp
new ApiResource()
{
    Name = "legacy",
    Description = "protects the legacy API from unauthorized access",
    ApiSecrets = new List<Secret> { new Secret("secret".Sha256()) },
    Scopes = new List<Scope>
    {
        // only interested in a single scope for this purpose
        new Scope("legacy.access", "grants access to use the legacy API")
    }
}
```

Now I need to add authorization to the legacy application. First, add the `IdentityServer3.AccessTokenValidation` nuget package to your .NET 4.6.x web app.

Next, in `Startup.cs`, add the following code.

```csharp
using Owin;
using Microsoft.Owin;
using IdentityServer3.AccessTokenValidation;

[assembly: OwinStartup(typeof(Legacy.Startup))]
namespace Legacy
{
    public class Startup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            appBuilder.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
            {
                Authority = "https://localhost/identityserver", // this will ultimately change per environment and therefore come from configuration
                ClientId = "legacy",
                ClientSecret = "secret", // this should be populated through configuration
                RequiredScopes = new[] { "legacy.access" },
                ValidationMode = ValidationMode.ValidationEndpoint,
                EnableValidationResultCache = true
            });
        }
    }
}
```

This will enable the use of the `[Authorize]` attribute and respond to any request with `401 Unauthorized` unless it has the Bearer Authorization header with a valid `access_token` issued to it from the identity server with the `legacy.access` scope.

Yes, the `ClientId` and `ClientSecret` should match the `Name` and `ApiSecret` you configured in the `ApiResource`.

## Fetching the access_token

The last piece is getting the `access_token` which will be added to the Authorization header for making authorized requests to the legacy app.

In order to see this in action, we will need a client that can make an authenticated request to the identity server for the token.

Add the following configuration to your identity server.

```csharp
new Client()
{
    ClientId = "client",
    ClientSecrets = new[] { new Secret("client-secret".Sha256()) }, // the secret should be populated through configuration
    AllowedGrantTypes = GrantTypes.ClientCredentials,
    AllowedScopes = new[] { "legacy.access" },
    AccessTokenType = AccessTokenType.Jwt,
    Enabled = true,
}
```

By adding `legacy.access`, we are saying that this client can generate an `access_token` that will include the `legacy.access` scope.

Using postman (or whatever tool you use), construct the following request given that your identity server is hosted @ localhost/identityserver

!! NOTE: make sure your request parameters are formed as `application/x-www-form-urlencoded` content type

```http
POST /identityserver/connect/token HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 89

client_id=client&client_secret=client-secret&grant_type=client_credentials&scope=legacy.access
```

the expected response should look like the following

```json
{
  "access_token": "xxx.xxx.xxx",
  "expires_in": 3600,
  "token_type": "Bearer"
}
```

## Using the access_token

Lets setup a controller in our legacy app

```csharp
using System;
using System.Web;
using System.Web.Http;

namespace Legacy
{
    [RoutePrefix("api/test")]
    public class TestController : ApiController
    {
        [HttpGet]
        public IHttpActionResult Test()
        {
            return Ok("Hello World");
        }

        [Authorize]
        [HttpGet, Route("auth")]
        public IHttpActionResult AuthTest()
        {
            return Ok("Authorized: Hello World");
        }
    }
}
```

Make sure your controller is properly registered in your app by hitting the endpoint that doesn't have the `[Authorize]` attribute.

Now, take the `access_token` you generated and place it inside the Authorization header (prefixed with "Bearer") and make a request to the `AuthTest()` endpoint

```http
GET /legacy/api/test/auth HTTP/1.1
Host: localhost
Authorization: Bearer xxx.xxx.xxx
```

- If the response is `401 Unauthorized` ❌, something is not configured properly 😢
- If the response is `200 OK` 🚀 and the content is `Authorized: Hello World`, you are in business ✅