Securing a .NET API with JWTs
On this page
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.
Note
The example uses the Curity Identity Server, but you can run the code against any standards-based authorization server.
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 Name | Value in tutorial |
---|---|
Issuer | https://idsvr.example.com/oauth/v2/oauth-anonymous |
Audience | demo-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 tokenor 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 weathercd weatherdotnet 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:
Policy | Endpoint |
---|---|
public | http://localhost:5000/WeatherForecast |
valid JWT | http://localhost:5000/WeatherForecast/authenticated |
lowRisk | http://localhost:5000/WeatherForecast/lowrisk |
developer | http://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.
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial