ProudMonkey

Signing JWT with RSA in .NET Core 3.x Made Easy

This space is for rent. Contact me at vmsdurano at gmail dot com.

There are cases that you will be working with external client integration and your API/service will act as a proxy that needs to perform some data processing which requires you to implement a feature based on external client needs. One typical example is to implement a custom JWT (a.k.a token) generation with custom claims that is signed using RSA algorithm.

In this scenario, the external client will give you the structure of JWT, normally with custom claims that they expect and provide you with an RSA private key to sign the token. The token will then be used to construct a Uri that will be sent to users and allowing them to invoke the external client endpoints.

In this post, we are going to take a look at how we can easily integrate this feature within our .NET Core 3.x application.

What RSA Is?

If you are new and haven't worked with RSA signing before, here's a quick definition taken from Wikipedia:

RSA (Rivest–Shamir–Adleman) is one of the first public-key cryptosystems and is widely used for secure data transmission. In such a cryptosystem, the encryption key is public and distinct from the decryption key which is kept secret (private).

Unlike HMAC where it uses a shared secret key across many applications - a.k.a symmetric primitive, RSA uses a pair of private and public keys to validate the token - a.k.a asymmetric primitive. In other words, with RSA, your service will use a private key to sign the JWT, and all other applications may use the public key to verify the token's validity and integrity.

Now Down To The Meat

Assuming that you already have the RSA private key given to you by your external client. Typically the key should be in the following format:

-----BEGIN RSA PRIVATE KEY-----
MII...  
-----END RSA PRIVATE KEY-----

The BEGIN and END lines represent the header and the footer for the key. Within that is the actual key
that represents a base64-encoded text format based from the PKCS #1: RSA Cryptography Specifications, which is just an Abstract Syntax Notation One Sequence of integers that makes up the RSA key.

Setting Basic Configuration

For the simplicity of this demo, let's just use the appsettings.json file store the keys and other required information. Please note that you should consider storing secrets and sensitive information in a vault or secrets manager.

Here's the entry within the appsettings.json file:

 "ExternalClientServer": {
    "ReferralUrl": "https://yourexternalclient.com",
    "Issuer": "<The Registered Issuer Value>",
    "Audience": "<The Registered Audience Value>",
    "ReferralId": "<Some Unique Id Given To You By Your External Client>",
    "RsaPrivateKey": "<The base64-encoded Private Key Given To You By Your External Client>",
    "RsaPublicKey": "<The Matching base64-encoded Public Key>"
}

Most of the values in the appsettings.json should come from your external client. If you are only given the private key, you can generate the public key using the openssl RSA command by following these simple steps:

  1. Copy the private key with header and footer and save it as a .PEM file.
  2. Execute the following command in the location where you store the .PEM file:
openssl rsa -in key.pem -RSAPublicKey_out  

The command above should give you the matching RSA public key as shown in the following:

$ openssl rsa -in key.pem -RSAPublicKey_out
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAn4XOc6lV0LZ5j+dBCRH2eiDj6fGlzMIJ7gmSUBF++xLLLAP/Espq  
uIMpTSRJFgrg29euExYNVA+DKDn45ckAXnWar/1JLQdWfz+8ybdUH8mAt9omZStv  
jfVbqS1/kyBBOymo2LZ3BZCuVRR/kiZ3xuwY06VhgKOcCJR8YQjW5hX+U9Ovl0fL  
lE4C1a32GBGkcNU7GTrS4aBlciAtALmRLbU+0rr+XJECYWb7/SFfYaM0qAa9kw6F  
YCfatXclHm2qLaOo8mwlsAdQPpCVyW7R/RrdLgLLkkmzeJacLgjFTLyb894t0Y9/  
4fHy+L+FAmC+Rceka9ZpCb+/V6IcAZDj+QIDAQAB  
-----END RSA PUBLIC KEY-----
writing RSA key

We will use the public key later to perform token validation.

Tip: Note that the RSA keys base64-encoded text are formatted with carriage return, so be sure to make it one-liner when storing them in appsettings.json, vault or variable. Also don't include the header and footer when storing the RSA keys. Just set the base64-encoded text in the RsaPrivateKey and RsaPublicKey attributes.

I'd like to point out that we will not be implementing an Authentication scheme for this demo as we will not be protecting any resources here, instead your service will act as the proxy to your external client for issuing and signing JWT's. Your external client will be the one to protect their resource and only authorize access to requests with the signed JWT that we are going to implement.

Now that we have our appsettings.json configured, let's create the following simple class that we can use to bind the values from appsettings.json:

public class ExternalClientJsonConfiguration  
{
    public string ReferralUrl { get; set; }
    public string Issuer { get; set; }
    public string Audience { get; set; }
    public string ReferralId { get; set; }
    public string RsaPrivateKey { get; set; }
    public string RsaPublicKey { get; set; }
}

Next is add the following code below within the ConfigureServices() method of Startup.cs file:

services.Configure<ExternalClientJsonConfiguration>(Configuration.GetSection("ExternalClientServer"));  

The preceding binds the configuration that we have defined within appsettings.json under ExternalClientServer node. This allow us to access strongly typed object via IOptions by registering the ExternalClientJsonConfiguration class into the DI service container.

Now let's create the following classes below:

public class JwtCustomClaims  
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

public class JwtResponse  
{
    public string Token { get; set; }
    public long ExpiresAt { get; set; }
}

The code above is nothing but just simple classes that house some properties. The JwtCustomClaims class holds the custom claims that we are going to pass in the JWT. The JwtResponse class holds two main properties that we are going to return for testing purposes.

Next, let's create the following interface below:

public interface IJwtHandler  
{
    JwtResponse CreateToken(JwtCustomClaims claims);
    bool ValidateToken(string token);
}

The interface above holds two main methods: The CreateToken which takes a JwtCustomClaims as a parameter and returns a JwtResponse. The ValidateToken method takes a token (JWT) as the parameter which returns a boolean. Simple as that!

Implementing the JWT Handler

At this point, we already have the basic requirements that we need to implement the JWT handler. In this demo, we are going to use the System.IdentityModel.Tokens.Jwt Nuget package as it provides a rich support for creating, serializing and validating JSON Web Tokens in .NET Core. Let's go head and install that using the following command:

Install-Package System.IdentityModel.Tokens.Jwt -Version 6.6.0  

Here's the class implementation below:

using Microsoft.Extensions.Options;  
using Microsoft.IdentityModel.Tokens;  
using System;  
using System.IdentityModel.Tokens.Jwt;  
using System.Security.Claims;  
using System.Security.Cryptography;

namespace NetCoreJwtRsa  
{
    public static class TypeConverterExtension
    {
        public static byte[] ToByteArray(this string value) =>
               Convert.FromBase64String(value);
    }

    public class JwtHandler: IJwtHandler
    {
        private readonly ExternalClientJsonConfiguration _settings;

        public JwtHandler(IOptions<ExternalClientJsonConfiguration> setting)
        {
            _settings = setting.Value;
        }

        public JwtResponse CreateToken(JwtCustomClaims claims)
        {
            var privateKey = _settings.RsaPrivateKey.ToByteArray();

            using RSA rsa = RSA.Create();
            rsa.ImportRSAPrivateKey(privateKey, out _);

            var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256)
            {
                CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
            };

            var now = DateTime.Now;
            var unixTimeSeconds = new DateTimeOffset(now).ToUnixTimeSeconds();

            var jwt = new JwtSecurityToken(
                audience: _settings.Audience,
                issuer: _settings.Issuer,
                claims: new Claim[] {
                    new Claim(JwtRegisteredClaimNames.Iat, unixTimeSeconds.ToString(), ClaimValueTypes.Integer64),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    new Claim(nameof(claims.FirstName), claims.FirstName),
                    new Claim(nameof(claims.LastName), claims.LastName),
                    new Claim(nameof(claims.Email), claims.Email)
                },
                notBefore: now,
                expires: now.AddMinutes(30),
                signingCredentials: signingCredentials
            );

            string token = new JwtSecurityTokenHandler().WriteToken(jwt);

            return new JwtResponse
            {
                Token = token,
                ExpiresAt = unixTimeSeconds,
            };
        }

        public bool ValidateToken(string token)
        {

            var publicKey = _settings.RsaPublicKey.ToByteArray();

            using RSA rsa = RSA.Create();
            rsa.ImportRSAPublicKey(publicKey, out _);

            var validationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = _settings.Issuer,
                ValidAudience = _settings.Audience,
                IssuerSigningKey = new RsaSecurityKey(rsa)
            };

            try
            {
                var handler = new JwtSecurityTokenHandler();
                handler.ValidateToken(token, validationParameters, out var validatedSecurityToken);
            }
            catch
            {
                return false;
            }

            return true;
        }
    }
}

Let's take a look at what we did in the preceding code above.

First we created an extension method called TypeConverterExtension which converts string to byte[] using the Convert.FromBase64String() method. Nothing fancy.

Next is the JWT handler class that implements the IJwtHandler interface. You will notice that we have injected the ExternalClientJsonConfiguration class as an IOptions in the class constructor to be able to automatically access the values we defined the appsettings.json in a strongly-type manner. This process is called "Constructor Injection".

Within the JwtHandler class, you will see that we have implemented the interface methods called CreateToken and ValidateToken.

Generating Tokens

The CreateToken method is where we implement the the RSA Private Key importing and signing of JWT. Let's start digging into the specific code snippet below:

var privateKey = _settings.RsaPrivateKey.ToByteArray();  
using RSA rsa = RSA.Create();  
rsa.ImportRSAPrivateKey(privateKey, out _);  

The preceding code reads the RSA private key from appsettings.json and translate that to byte array using the ToByteArray() extension method. We then call RSA.Create() and then import the RSA private key byte array format using ImportRSAPrivateKey method that is built-in to .NET Core 3.x. I use the _ (discard) to indicate that the second parameter should be ignored because we don't need the value for the bytesRead.

The next step in the code is setting up the signing credentials in the following manner:

var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256)  
{
    CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
};

The preceding code creates a new instance of SigningCredentials method and passing in the RSA Private Key and security signature algorithm to RsaSha256. Notice that I set the CacheSignatureProviders to false because it will throw the following error below on subsequent requests:

System.ObjectDisposedException: Cannot access a disposed object.  
Object name: 'RSA'.  
   at System.Security.Cryptography.RSAImplementation.RSACng.ThrowIfDisposed()
   at System.Security.Cryptography.RSAImplementation.RSACng.GetDuplicatedKeyHandle()
   at System.Security.Cryptography.RSAImplementation.RSACng.SignHash(Byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
   at Microsoft.IdentityModel.Tokens.AsymmetricAdapter.SignWithRsa(Byte[] bytes)

If you know a hack on how to fix the error above without turning off the CacheSignatureProviders, please let me know by dropping your comments. I would truly appreciate it :)

Let's continue.

The following block of code is the typical implementation of generating JWT in .NET Core:

var now = DateTime.Now;  
var unixTimeSeconds = new DateTimeOffset(now).ToUnixTimeSeconds();

var jwt = new JwtSecurityToken(  
    audience: _settings.Audience,
    issuer: _settings.Issuer,
    claims: new Claim[] {
          new Claim(JwtRegisteredClaimNames.Iat, unixTimeSeconds.ToString(), ClaimValueTypes.Integer64),
          new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
          new Claim(nameof(claims.FirstName), claims.FirstName),
          new Claim(nameof(claims.LastName), claims.LastName),
          new Claim(nameof(claims.Email), claims.Email)
    },
    notBefore: now,
    expires: now.AddMinutes(30),
    signingCredentials: signingCredentials
);

string token = new JwtSecurityTokenHandler().WriteToken(jwt);

return new JwtResponse  
{
    Token = token,
    ExpiresAt = unixTimeSeconds,
};

The preceding code constructs the structure of the JWT using the JwtSecurityToken object and setting in the required metadata such as audience, issuer, expires, notBefore, basic claims, custom claims and the signature of the token expressed in signingCredentials meta.

It then generates a token by invoking a new instance of JwtSecurityTokenHandler().WriteToken() method.

And finally, we return the response containing the JWT and expiry.

Validating Tokens

The ValidateToken method on the other hand seems to be very straight-forward. Let's start with the following code block:

var publicKey = _settings.RsaPublicKey.ToByteArray();

using RSA rsa = RSA.Create();  
rsa.ImportRSAPublicKey(publicKey, out _);  

As you can see, the implementation is somewhat similar to importing the RSA private key, except that for validation, it uses the RSA public key and uses the ImportRSAPublicKey method which is also built-in to .NET Core.

The next code block is the typical implementation for setting token validation parameters for JWTs:

var validationParameters = new TokenValidationParameters  
{
    ValidateIssuer = true,
    ValidateAudience = true,
    ValidateLifetime = true,
    ValidateIssuerSigningKey = true,
    ValidIssuer = _settings.Issuer,
    ValidAudience = _settings.Audience,
    IssuerSigningKey = new RsaSecurityKey(rsa)
};

The preceding code configures the validation parameters including the issuer, audience and the signature.

Finally, we use the ValidateToken method of JwtSecurityTokenHandler object to validate the JWT based on the validation parameters we configured in the previous code:

try  
{
    var handler = new JwtSecurityTokenHandler();
    handler.ValidateToken(token, validationParameters, out var validatedSecurityToken);
}
catch  
{
    return false;
}

return true;

If the call to ValidateToken method fails, we simply return false, otherwise we return true. If you like, you can use the value of validatedSecurityToken variable to look into the validated JWT.

Now that we have our JWT handler all setup, the last thing we need to do is to register the contract mapping between our IJwtHandler interface and the concrete implementation. Let's go ahead and add the following code in the ConfigureServices method of Startup.cs file:

services.AddTransient<IJwtHandler, JwtHandler>();  

That's it!

Testing Token Creation and Validation

To test the JwtHandler methods, let's create a simple API Controllers containing two endpoints. Here's the code block:

using Microsoft.AspNetCore.Mvc;  
using System.IdentityModel.Tokens.Jwt;  
using System.Linq;  
using static Microsoft.AspNetCore.Http.StatusCodes;

namespace NetCoreJwtRsa.Controllers  
{
    [Route("api/v1/[controller]")]
    public class TestsController : ControllerBase
    {
        private readonly IJwtHandler _jwtHandler;
        public TestsController(IJwtHandler jwtHandler)
        {
            _jwtHandler = jwtHandler;
        }

        [HttpPost]
        [Route("token")]
        [ProducesResponseType(typeof(string), Status200OK)]
        public IActionResult GenerateJwtAsync()
        {

            var claims = new JwtCustomClaims
            {
                FirstName = "Vynn",
                LastName = "Durano",
                Email = "whatever@email.com"
            };

            var jwt = _jwtHandler.CreateToken(claims);

            return Ok(jwt);
        }

        [HttpPost]
        [Route("token/validate")]
        [ProducesResponseType(typeof(string), Status200OK)]
        public IActionResult ValidateJwtAsync([FromBody] string token)
        {

            if (_jwtHandler.ValidateToken(token))
            {
                var handler = new JwtSecurityTokenHandler();
                var jwtToken = handler.ReadToken(token) as JwtSecurityToken;

                var claims = new JwtCustomClaims
                {
                    FirstName = jwtToken.Claims.First(claim => claim.Type == "FirstName").Value,
                    LastName = jwtToken.Claims.First(claim => claim.Type == "LastName").Value,
                    Email = jwtToken.Claims.First(claim => claim.Type == "Email").Value
                };

                return Ok(claims);
            }

            return BadRequest("Token is invalid.");
        }
    }
}

Nothing fancy there, we basically just expose the following endpoints for us to test:

  • api/v1/tests/token
  • api/v1/tests/token/validate

Because we registered our IJwtHandler interface and it's mapping into the DI service container, injecting the interface into the Controller's constructor will enable us to access the available methods and it will automatically resolved them at run time.

Sample Requests and Results

Here are the sample requests and the corresponding test results that I ran in POSTMAN.

Http Request for generating JWT:

POST /api/v1/tests/token HTTP/1.1  
Host: localhost:5001  
Content-Type: application/json  

Output:

{
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1OTE3NDg5MjUsImp0aSI6IjllNzQ4OTg4LTA1MzYtNDc3MS1iNjMwLTFjMzVhNzZlZmRjNCIsIkZpcnN0TmFtZSI6IlZ5bm4iLCJMYXN0TmFtZSI6IkR1cmFubyIsIkVtYWlsIjoid2hhdGV2ZXJAZW1haWwuY29tIiwibmJmIjoxNTkxNzQ4OTI1LCJleHAiOjE1OTE3NTA3MjUsImlzcyI6Ik1lIiwiYXVkIjoiWW91In0.eC9aMvtNCcag3nRztBSaJjiKBTH_GfIUgfMdbREGRlNPlcOebHlt194bavtPWxqNSL2SAmF3eM4BJqyurxuohkdZa6fpm5s8ZllieAdgK3R2qepozUgYCRXEv_KqkwmwHm6QzdYyDyiJ_x2xhF8CkPf6Kt8jRUInJdB_QIaB7IUBspnTGv829IWU3Ki7di0akJpXyfAsT7TDytGj11LcFc7iLNUh4Z2lkwiRhYAwzj7fvi66BUVbZEMqrLzDS7_D9XzBiVeJBkpjmPY4lrPfuowtRFXFXKg8d7nyuKAWS5IyLdPaz2-36Bl1uKLGIW3KJjYfgnuTdCoTORfNZYZzTQ",
    "expiresAt": 1591748502
}

Pasting the JWT to jwt.io should result to something like this:

HEADER  
{
  "alg": "RS256",
  "typ": "JWT"
}

PAYLOAD  
{
  "iat": 1591748925,
  "jti": "9e748988-0536-4771-b630-1c35a76efdc4",
  "FirstName": "Vynn",
  "LastName": "Durano",
  "Email": "whatever@email.com",
  "nbf": 1591748925,
  "exp": 1591750725,
  "iss": "Me",
  "aud": "You"
}

And you can use the Public Key to validate the signature via jwt.io as well.

Http Request for validating JWT:

POST /api/v1/tests/token/validate HTTP/1.1  
Host: localhost:5001  
Content-Type: application/json  
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1OTE3NDg5MjUsImp0aSI6IjllNzQ4OTg4LTA1MzYtNDc3MS1iNjMwLTFjMzVhNzZlZmRjNCIsIkZpcnN0TmFtZSI6IlZ5bm4iLCJMYXN0TmFtZSI6IkR1cmFubyIsIkVtYWlsIjoid2hhdGV2ZXJAZW1haWwuY29tIiwibmJmIjoxNTkxNzQ4OTI1LCJleHAiOjE1OTE3NTA3MjUsImlzcyI6Ik1lIiwiYXVkIjoiWW91In0.eC9aMvtNCcag3nRztBSaJjiKBTH_GfIUgfMdbREGRlNPlcOebHlt194bavtPWxqNSL2SAmF3eM4BJqyurxuohkdZa6fpm5s8ZllieAdgK3R2qepozUgYCRXEv_KqkwmwHm6QzdYyDyiJ_x2xhF8CkPf6Kt8jRUInJdB_QIaB7IUBspnTGv829IWU3Ki7di0akJpXyfAsT7TDytGj11LcFc7iLNUh4Z2lkwiRhYAwzj7fvi66BUVbZEMqrLzDS7_D9XzBiVeJBkpjmPY4lrPfuowtRFXFXKg8d7nyuKAWS5IyLdPaz2-36Bl1uKLGIW3KJjYfgnuTdCoTORfNZYZzTQ"

Output:

{
    "firstName": "Vynn",
    "lastName": "Durano",
    "email": "whatever@email.com"
}

Now that we are able to generate JWTs, generating links should be very straight-forward. For example, in the JwtHandler class you could add a new method like this:

public string GenerateLink(string token) =>  
   $"{_settings.ReferralUrl}/{_settings.ReferralId}/foo?token={token}";

That's it! Thanks for reading and I hope you find this post useful. Source code is available here: https://github.com/proudmonkey/NetCoreJwtRsaDemo

Buy Me A Coffee