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")
}
}
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
});
}
}
}
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,
}
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
the expected response should look like the following
{
"access_token": "xxx.xxx.xxx",
"expires_in": 3600,
"token_type": "Bearer"
}
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");
}
}
}
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
- If the response is
401 Unauthorized
❌, something is not configured properly 😢 - If the response is
200 OK
🚀 and the content isAuthorized: Hello World
, you are in business ✅
Top comments (0)