Categories
.NET Core Authentication Security

JWT Authentication in .NET Core

I was setting up JWT authentication/authorization in .NET Core the other day and wanted to write a little about the process. I’m using .NET Core 3.1 in this example.

The code for this tutorial can be found here: https://github.com/gaui/jwt-netcore

Initial setup

First create a new .NET Core Web API project with the dotnet CLI:

$ dotnet new webapi -o jwt-netcore

Open up the project in your IDE. I use VSCode and the C# extension.

When we bootstrapped the project a default controller is created for us, WeatherForecastController. We will be securing that controller.

If we run the project with dotnet run or dotnet watch run we get a server started on both http://localhost:5000 and https://localhost:5001.

Removing HTTPS redirection

If we go to http://localhost:5000/WeatherForecast we get redirected to https://localhost:5001/WeatherForecast. We won’t be using HTTPS for this simple example so we can remove the HTTPS redirection. This is done by removing the following line in Startup.cs in the Configure method:

app.UseHttpsRedirection();

Now if we go to http://localhost:5000/WeatherForecast we won’t be redirected and you should get the following JSON response from the server:

[
  {
    "date": "2020-08-21T13:54:24.8996754+00:00",
    "temperatureC": 35,
    "temperatureF": 94,
    "summary": "Warm"
  },
  {
    "date": "2020-08-22T13:54:24.8998331+00:00",
    "temperatureC": 49,
    "temperatureF": 120,
    "summary": "Balmy"
  },
  {
    "date": "2020-08-23T13:54:24.8998358+00:00",
    "temperatureC": -1,
    "temperatureF": 31,
    "summary": "Bracing"
  },
  {
    "date": "2020-08-24T13:54:24.8998361+00:00",
    "temperatureC": 27,
    "temperatureF": 80,
    "summary": "Hot"
  },
  {
    "date": "2020-08-25T13:54:24.8998363+00:00",
    "temperatureC": -2,
    "temperatureF": 29,
    "summary": "Bracing"
  }
]

Great!

Dependencies

Before we go on we need to install some NuGet packages that we will use. Go ahead and install the following dependencies.

Microsoft.AspNetCore.Authentication.JwtBearer

This is a ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token.

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 3.1.7

System.IdentityModel.Tokens.Jwt

Includes classes that provide support for creating, serializing and validating JWTs.

dotnet add package System.IdentityModel.Tokens.Jwt --version 6.7.1

Microsoft.IdentityModel.Tokens

Includes types that provide support for SecurityTokens, Cryptographic operations: Signing, Verifying Signatures, Encryption.

dotnet add package Microsoft.IdentityModel.Tokens --version 6.7.1

Setting up the JWT config

Let’s begin by setting up our JWT config. This configuration will be used to generate JWTs. It will be stored in appsettings.json file.

Append the following JSON object to appsettings.json and make sure to create a unique secret key:

"JwtConfig": {
    "Secret": "YOUR SECRET KEY",
    "ExpirationInMinutes": 1440,
    "Issuer": "NAME OF YOUR APPLICATION",
    "Audience": "NAME OF YOUR APPLICATION"
}

So the file should look something like this:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "JwtConfig": {
    "Secret": "6U&PT7%k9h$%RY8d8agr#c$@dnQ8b6",
    "ExpirationInMinutes": 1440,
    "Issuer": "https://gaui.is",
    "Audience": "https://users.gaui.is"
  }
}

Setting up the JWT middleware

Now that we have set up our JWT config, we can go on configuring and setting up the JWT authentication middleware.

In the Startup.cs file add the following dependencies:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

And add the following to the ConfigureServices method:

var key = Encoding.ASCII.GetBytes(Configuration["JwtConfig:secret"]);

services.AddAuthentication (options => {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  })
  .AddJwtBearer (options => {
    options.TokenValidationParameters = new TokenValidationParameters () {
      ValidateIssuer = true,
      ValidateAudience = true,
      ValidAudience = Configuration["JwtConfig:Audience"],
      ValidIssuer = Configuration["JwtConfig:Issuer"],
      IssuerSigningKey = new SymmetricSecurityKey (key),
      ValidateLifetime = true,
      ClockSkew = TimeSpan.Zero
    };
  });

Then add the following in the Configure method below app.UseRouting():

app.UseAuthentication ();
app.UseAuthorization ();

Let’s break it down.

The package Microsoft.AspNetCore.Authentication.JwtBearer adds an extension method to the AuthenticationBuilder (services.AddAuthentication() method) called AddJwtBearer().

This configures the authentication middleware with the AuthenticationBuilder and finally adds it to the IServiceCollection.

I’m not going great lengths to explain the middleware architecture but here’s a high-level overview:

Now everything is wired up in the request pipeline for authentication and validation of JWTs.

JWT Service

Now we need to create a service that handles generating our JWTs. We don’t need to validate the JWTs in our service, because that is handled by the middleware above.

Let’s go ahead and create JwtService.cs and put the following:

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;

public class JwtService {
  private IConfiguration Configuration { get; }

  public JwtService (IConfiguration config) {
    Configuration = config;
  }

  public string GenerateToken (string email) {
    var tokenHandler = new JwtSecurityTokenHandler ();
    var key = Encoding.ASCII.GetBytes (Configuration["JwtConfig:Secret"]);
    var tokenDescriptor = new SecurityTokenDescriptor {
      Issuer = Configuration["JwtConfig:Issuer"],
      Audience = Configuration["JwtConfig:Audience"],
      Subject = new ClaimsIdentity (new [] {
        new Claim (JwtRegisteredClaimNames.Email, email)
      }),
      Expires = DateTime.UtcNow.AddMinutes (int.Parse(Configuration["JwtConfig:ExpirationInMinutes"])),
      SigningCredentials = new SigningCredentials (new SymmetricSecurityKey (key), SecurityAlgorithms.HmacSha256Signature)
    };

    var token = tokenHandler.CreateToken (tokenDescriptor);

    return tokenHandler.WriteToken (token);

  }
}

Let’s break it down.

This is just a class with one method GenerateToken that takes in a single parameter email. This method handles generating JWTs.

We instantiate the JwtSecurityTokenHandler class which handles creating our JWT and put in our properties for the token, which we defined in our JwtConfig in appsettings.json earlier. That is the Issuer, Audience, Subject, Expiration and our secret key which is used to encrypt and decrypt the token.

Authentication Controller

Now on to the controller. Let’s create AuthController.cs which will be used by the user to get a JWT.

My controller looks like this:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

[ApiController]
[Route ("auth")]
public class AuthController : ControllerBase {
  private IConfiguration Configuration { get; }

  public AuthController (IConfiguration configuration) {
    Configuration = configuration;
  }

  [HttpGet("token")]
  public IActionResult GetToken () {
    var jwt = new JwtService (Configuration);
    var token = jwt.GenerateToken ("my@email.com");
    return Ok(token);
  }
}

This controller only has one GetToken method which resolves to the /auth/token route. This method creates a new JWT and returns it with a 200 OK HTTP response. This simple example doesn’t include any user database or lookup for authentication. Maybe I’ll write how to do that later.

Now if we run our API and make a request to http://localhost:5000/auth/token we should get a JWT.

Let’s see this in action. I’m using cURL to make requests to my API.

$ curl -i http://localhost:5000/auth/token
HTTP/1.1 200 OK
Date: Thu, 20 Aug 2020 17:23:10 GMT
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Im15QGVtYWlsLmNvbSIsIm5iZiI6MTU5Nzk0NDE5MSwiZXhwIjoxNTk4MDMwNTkxLCJpYXQiOjE1OTc5NDQxOTEsImlzcyI6Imh0dHBzOi8vZ2F1aS5pcyIsImF1ZCI6Imh0dHBzOi8vdXNlcnMuZ2F1aS5pcyJ9.pVgAGtn4QS0ltcJaALmG5syX-BIu7P0XBRbX-Z_zTqs

There’s our JWT token! Now if we go to jwt.io and enter our token, we get the following JSON:

{
  "email": "my@email.com",
  "nbf": 1597944191,
  "exp": 1598030591,
  "iat": 1597944191,
  "iss": "https://gaui.is",
  "aud": "https://users.gaui.is"
}

Those timestamps are so called Unix timestamps.

Unix time is a system for describing a point in time. It is the number of seconds that have elapsed since the Unix epoch, minus leap seconds; the Unix epoch is 00:00:00 UTC on 1 January 1970.

So the expiration (exp) timestamp resolves roughly to Fri Aug 21 2020 17:23:11 GMT+0000.

Securing the Controller

Now on to securing the WeatherForecast controller. All you have to do is add the [Authorize] annotation and add a reference to the Microsoft.AspNetCore.Authorization namespace.

So this is the header of my WeatherForecastController:

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase

Now if I use cURL to make a HTTP request to /WeatherForecast route.

$ curl -i http://localhost:5000/WeatherForecast
HTTP/1.1 401 Unauthorized
Date: Thu, 20 Aug 2020 17:35:37 GMT
Server: Kestrel
Content-Length: 0
WWW-Authenticate: Bearer

The API responds with 401 Unauthorized which is exactly what we want.

Now if I add the JWT as a HTTP header with my request on the following format: Authorization: Bearer <JWT>

$ curl -i -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Im15QGVtYWlsLmNvbSIsIm5iZiI6MTU5Nzk0NDE5MSwiZXhwIjoxNTk4MDMwNTkxLCJpYXQiOjE1OTc5NDQxOTEsImlzcyI6Imh0dHBzOi8vZ2F1aS5pcyIsImF1ZCI6Imh0dHBzOi8vdXNlcnMuZ2F1aS5pcyJ9.pVgAGtn4QS0ltcJaALmG5syX-BIu7P0XBRbX-Z_zTqs' http://localhost:5000/WeatherForecast
HTTP/1.1 200 OK
Date: Thu, 20 Aug 2020 17:37:13 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

[{"date":"2020-08-21T17:37:13.7361816+00:00","temperatureC":3,"temperatureF":37,"summary":"Sweltering"},{"date":"2020-08-22T17:37:13.7362082+00:00","temperatureC":19,"temperatureF":66,"summary":"Hot"},{"date":"2020-08-23T17:37:13.7362085+00:00","temperatureC":-6,"temperatureF":22,"summary":"Scorching"},{"date":"2020-08-24T17:37:13.7362087+00:00","temperatureC":30,"temperatureF":85,"summary":"Balmy"},{"date":"2020-08-25T17:37:13.7362089+00:00","temperatureC":-13,"temperatureF":9,"summary":"Bracing"}]

Success! I get a 200 OK HTTP response and the requested data.

What next?

Now when we’ve gotten a simple JWT authentication to work with our WeatherForecast controller, you could continue and implement an authentication mechanism that can connect to a user database, retrieve the user, hashed password and authenticate the user into the system, and give him a JWT that he will use to connect to your API.

Leave a Reply

Your email address will not be published. Required fields are marked *