Securing C#/.NET WebAPI with public-private-key-signed JWTs signed by NodeJS

Kevin Le
6 min readApr 27, 2018

--

This article was cross-posted on dev.to

In this article, I will show how to implement and secure a C#/.NET (hereinafter I will only say C#) WebAPI. To secure the WebAPI, we will use JWT. The JWT is signed by a NodeJS backend using Private Key. The WebAPI will verify the JWT using the Public Key.

I’d like to be clear so let me clarify some the terminologies that I prefer to use. When I say client, I mean a client application such as mobile app, a web application, Postman, etc. On the other had, a user is a human who uses those clients. When a client sends a login request to the server, it's actually doing it on behalf of user who enters his/her name on the mobile app and tab Submit button.

So with that, the client first makes the request to /login endpoint of the NodeJS server. This NodeJS server is the Authorization Server. Its job is to issue JWT if the login is correct. Assume it is, once the client obtains the JWT, the client can store this JWT in memory, or in local storage or cookie or elsewhere. Now the client wants to access the resources provided by the C# WebAPI. So when it sends a request, it includes a JWT in the Authorizationattribute of the request header. The C# WebAPI is the Resource Server or Provider. Its job is to provide resources. But it only does so if it can verify the JWT.

In a sequence diagram:

The Authorization Server (NodeJS) and the Resource Provider (C# WebAPI) can run on 2 totally different servers or clouds. Instead of using public private key to sign and verify the JWT like in his article, we could also have used a shared secret that is known by both the Authorization Server (NodeJS) and the Resource Provider (C# WebAPI). However, the shared secret approach is not as effective as the public private key approach for the following reasons.

  1. There are 2 potential points of failure instead of 1. Either the Authorization Server or the Resource Provider could compromise the shared secret. On the other hand, the private key can still be compromised, but there’s only one entity that knows about the private key.
  2. If there are multiple Resource Providers, sharing 1 secret only increases the number of potential points of failure.
  3. Having a different secret for each Resource Provider is an option, but in some cases we don’t have control of the Resource Provider, then we have to deal with the problem of distributing of the shared secrets.

Anyway, let’s generate public and private keys.

Generate Public Private Key

On a Windows computer,

$ ssh-keygen -t rsa -b 4096 -f jwtRS256.key
$ openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub

Credit: https://gist.github.com/ygotthilf/baa58da5c3dd1f69fae9

On a Mac,

$ openssl genrsa -out jwtRS256.key 4096
$ openssl rsa -in jwtRS256.key -pubout -out jwtRS256.key.pub

Credit: https://gist.github.com/h-sakano/84dc4bd8371be4f0e8dddc9388974348#file-file0-sh

The file jwtRS256.key.pub is the public key and will be served as a static file. This will be shown later. The file jwtRS256.key is the private key and we will use it to sign the JWT.

Sign the JWT in NodeJS

We will write a NodeJS server code that has an endpoint called /login and accepts a POST request. The body of the POST request contains the userid and password in JSON format.

Run npm init and install the necessary packages:

$ npm init -y
$ npm i --save express path body-parser

Create a public and a private folder and move the public jwtRS256.key.pub and private key jwtRS256.key files to those folders respectively.

Create a file called server.js with the content show in the screenshot below.

At this point the file structure and the server.js file should look like:

(Can’t copy and paste, don’t worry, this code will be completed and available then. Just read on)

We have not really done anything yet. But you can see the place holders. If the correct userid and password are entered, we will generate a signed JWT and return with a status code 200. Otherwise, we return with status of 401. The logic to check for userid and password is up to you.

If you run the NodeJS server locally at this point, you can use Postman or your browser to go address http://localhost:8080/jwtRS256.key.pub, the public key is readily available.

Now we install the jsonwebtoken package, which is the essence of signing the JWT and also fs.

npm i --save jsonwebtoken
npm i --save fs

Now the complete code:

There are only 3 lines that are more important than the rest. The first line is reading the private key (const privateKey = ...). The second line is assigning 'RS256' to algorithm. The third line is the one where the token is signed (const token = jwt.sign(...))

Now launch Postman, and make a POST request like in the figure below, you will get a JWT in the response.

Verify the JWT in C# WebAPI

As you see, a JWT is returned in the response. Where to store this JWT depends on which kind of client app you are developing, mobile, web application or Electron desktop, etc.

What I will show next is how to secure a C# WebAPI resource.

So in Visual Studio 2017 or 2015, just use the WebAPI Project template to create a new solution.

You will see a file called ValuesController.js with the following code generated for you.

public class ValuesController : ApiController
{
// GET api/values
public async Task<IEnumerable<string>> Get()
{
await Task.Delay(10);
return new string[] { "value1", "value2" };
}
...
}

Right now, this endpoint GET api/values is unprotected. Let's go ahead and secure this endpoint.

Modify this file by adding one single line

public class ValuesController : ApiController
{
// GET api/values
[JwtAuthorization]
public async Task<IEnumerable<string>> Get()
{
await Task.Delay(10);
return new string[] { "value1", "value2" };
}
...
}

JwtAuthorization is a class that we will write. It subclasses from AuthorizationFilterAttribute. Before I'll show it, we have to install a Nuget package called BouncyCastle.

Then let’s write a class that reads the public key. Remember the public key is a static file served at address http://localhost:8080/jwtRS256.key.pub

Since the public only has to be read once, I just create singleton for it.

Now we get to the most important part which is verifying the JWT. As I mentioned, this will be done in the JwtAuthorization class which overrides the OnAuthorization(HttpActionContext actionContext) of the base class AuthorizationFilterAttribute

Now go to Postman, and make a Post request to where your WebAPI is running, pass in the JWT that you got above (using the bearer scheme) in the Authorization attribute, you will the response with status 200.

Points of interests

  1. Instead of the following code
...
var pr = new PemReader(new StringReader(publicKey));
var asymmetricKeyParameter = (AsymmetricKeyParameter)pr.ReadObject();
...

I have seen

...
var keyBytes = Convert.FromBase64String(publicKey);
var asymmetricKeyParameter = PublicKeyFactory.CreateKey(keyBytes);
...

The problem is with the latter, the following FormatException was thrown

The format of s is invalid. s contains a non-base-64 character, more than two padding characters, or a non-white space-character among the padding characters.

2. The JwtAuthorizationAttribute filter runs asynchronously because of the singleton reading the public key is also asynchronously. To ensure the filter always runs before the controller method, I artificially introduced a delay of 10 ms. However as I said, the public key only has to be read once, and after that, it's available in memory. Therefore if every request gets penalized 10 ms, that does not seem fair. So I'm looking for a better solution.

Finally, if you want source code, I’m still tidying it up. In the mean time, you could help motivating me by giving this article a like and sharing it.

--

--

Kevin Le
Kevin Le

Written by Kevin Le

Driven by passion and patience. Read my shorter posts https://dev.to/codeprototype (possibly duplicated from here but not always)