Securing a .NET 4.6.x API with OpenID Connect

isaacadams

Isaac Adams

Posted on September 8, 2021

Securing a .NET 4.6.x API with OpenID Connect

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.

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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,
}
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

the expected response should look like the following

{
  "access_token": "xxx.xxx.xxx",
  "expires_in": 3600,
  "token_type": "Bearer"
}
Enter fullscreen mode Exit fullscreen mode

Using the access_token

Lets setup a controller in our legacy app

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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

GET /legacy/api/test/auth HTTP/1.1
Host: localhost
Authorization: Bearer xxx.xxx.xxx
Enter fullscreen mode Exit fullscreen mode
  • 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 ✅
💖 💪 🙅 🚩
isaacadams
Isaac Adams

Posted on September 8, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related