Securing a .NET Core API with JWTs

Securing a .NET Core API with JWTs

Code Examples / api-integration

This tutorial explains how to secure endpoints with JSON Web Tokens (JWTs) in a .NET Core API. This can be managed using the .NET Core Security Framework.

Prerequisites

Curity Identity Server

You will need an installation of the Curity Identity Server with the basic setup completed. Achieve this by following the Getting Started Guides. Alternatively, if you have a system up and running with your own configuration, you can use that as well.

This tutorial will require some configuration information about your Curity Identity Server. You’ll need the Issuer value that the server uses. With this, the Security Framework can locate OpenID Metadata to find the JWKS endpoint. You can see your issuer URL in the Admin UI by clicking on Profiles -> Token Service -> Info.

You must also know the audience of the Access Token. By default, this is the client id.

If you need to configure a new client, follow the tutorial here: Configure Client

In this tutorial, the following values will be used:

Parameter NameValue in tutorial
Issuerhttps://idsvr.example.com/oauth/v2/oauth-anonymous
Audiencedemo-client

Note on Opaque Tokens

It’s a common practice for an Authorization Server to issue opaque access tokens instead of JWTs. This is also the default behavior of the Curity Identity Server. Using opaque tokens in the outside world is more secure as no data can be read from the token (as is the case with JWTs).

However, it’s more convenient for an API to use JWTs as access tokens, as you’ll usually want your service to have access to all the data carried in a JWT. That’s why at Curity, we recommend using the Phantom token or the Split token approach, where an API gateway is responsible for exchanging the opaque token for a JWT, which is then sent in requests to API endpoints.

This tutorial assumes you either use JWTs as access tokens for your API or one of the mentioned approaches. Therefore, the microservice always deals with a JWT — never an opaque token.

.NET

Ensure that you have the .NET Core 5.0 SDK installed and an IDE such as Visual Studio Code.

Create an API

This tutorial is based on the built-in WeatherForecast example project. But if you already have an API, you can use that instead.

Generate this project and add the necessary dependencies from the command line:

dotnet new webapi -o weather
cd weather
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

In this tutorial, TLS is disabled to avoid certificate problems. Remove the HTTPS URL from the applicationUrl property in Properties/launchSettings.json.

"applicationUrl": "http://localhost:5000",

Settings

The properties from Curity Identity Server will be stored in the appsettings.json file. Add the following:

"Authorization": {
  "Issuer": "https://idsvr.example.com/oauth/v2/oauth-anonymous",
  "Audience": "demo-client"
}

Enable JWT Authorization

In this tutorial, all configurations of the authentication and authorization, along with its policies, will be managed in the same file. In more complex scenarios, it is possible to use separate files.

Open the Startup.cs file.

Add the JwtBearer dependency:

using Microsoft.AspNetCore.Authentication.JwtBearer;

Add this to the ConfigureServices() method:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = Configuration["Authorization:Issuer"];
        options.Audience = Configuration["Authorization:Audience"];
    });

services.AddAuthorization(options =>
{
    options.AddPolicy("lowRisk", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(claim => 
                claim.Type == "risk" && Int32.Parse(claim.Value) < 50
            )
        )
    );
    options.AddPolicy("developer", policy =>
        policy.RequireClaim("title", "junior developer", "senior developer")
        .RequireClaim ("department", "development")
    );
});

This will set up the Authentication service and also define the authorization policies.

The lowRisk policy verifies that the risk claim has a value below 50.

The developer policy requires that the JWT contain a department claim set to development and a title claim set to either junior developer or senior developer.

Since there is no HTTPS listener, remove the following from the Configure method:

app.UseHttpsRedirection();

Then add the following to register the Authentication middleware for the app:

app.UseAuthentication();

NOTE: It is important to add this before app.UseAuthorization(); as the order matters.

Assign Policies to Endpoints

At this point, the policies are set up but not yet assigned to the endpoints. This is accomplished in the Controller. The example below contains multiple endpoints to demonstrate the options for authorization as well as some helper methods.

Open Controllers/WeatherForecastController.cs and add the following dependencies:

using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;

Next, add two helper methods. These enable the controller to read the JWT claims. They are not required for the authorization since the middleware covers that.

private String GetSubject()
{
    return GetClaim(ClaimTypes.NameIdentifier);
}
private String GetClaim(String type) 
{
    Claim c = User.Claims.FirstOrDefault(c => c.Type == type);
    return c?.Value;
}

Disable Claims Mapping

It is possible to disable claims mapping, making it possible to call GetClaim("sub") instead of GetSubject(). This is done by clearing the JwtSecurityTokenHandler.DefaultInboundClaimTypeMap before authentication is enabled for the app.

Lastly, add the new endpoints:

[HttpGet("authenticated")]
[Authorize]
public IActionResult Authenticated()
{
    return Ok(new {data = "Some data from secured endpoint.", user = GetSubject()});
}

[HttpGet("developer")]
[Authorize( Policy = "developer")]
public IActionResult Developer()
{
    return Ok(new {data = "Some data for developers", user = GetSubject(), title = GetClaim("title")});
}

[HttpGet("lowrisk")]
[Authorize( Policy = "lowRisk")]
public IActionResult LowRisk()
{
    return Ok(new {data = "Your risk score is low", user = GetSubject(), risk = GetClaim("risk")});
}

The new endpoints all have different authorization policies. The original one does not have [Authorize] annotation, meaning it will be publicly available. The authenticated endpoint has the [Authorize] annotation without any policy set, meaning it will require a valid JWT from the issuer but no particular claims. The other ones refer to the policies set up earlier.

Try It Out

At this point, everything should be ready to go. Either run it from the IDE or the command line.

dotnet run

Once it is running, the service will serve the following endpoints with these policies:

PolicyEndpoint
publichttp://localhost:5000/WeatherForecast
valid JWThttp://localhost:5000/WeatherForecast/authenticated
lowRiskhttp://localhost:5000/WeatherForecast/lowrisk
developerhttp://localhost:5000/WeatherForecast/developer

You can access them using cURL. All endpoints but the public one require a JWT in the Authorization header’s request. Without a valid JWT, you can expect an HTTP 401 response. Requests with a valid JWT that do not meet requirements should instead receive HTTP 403.

curl -i http://localhost:5000/WeatherForecast/authenticated -H "Authorization: Bearer eyJ0e...aOCg"

To obtain a valid JWT, you can use the online tool OAuth.tools, a powerful tool to explore OAuth and OpenID Connect. You can easily add your Curity Identity Server configuration and use any flow to generate a valid access token.

Conclusion

Protecting endpoints with JWTs is easy in .NET Core. The middleware supporting it is already there, and it is easy to set up policies.

The policies in this tutorial are pretty easy to express in a few lines of code. In case of more complex procedures, look at Requirements and Authorization handlers.

Swagger was enabled by default when the project was generated. This tutorial does not cover how to set it up with the new endpoints.

Further information on API authorization is available in Scope Best Practices and Claims Best Practices.

You can check out the complete code for this tutorial here: https://github.com/curityio/dotnet-api-jwt-validation.

Let’s Stay in Touch!

Get the latest on identity management, API Security and authentication straight to your inbox.

Keep up with our latest articles and how-tos using RSS feeds