Dynamic Key Validation with JWT in ASP.NET Core
Often, the very first question one must address when designing a modern web service or API is the one centered around authentication and authorization. How can you make sure that your clients are who they say they are, and how can you verify what roles and permissions any given client has?
Obviously, one can handle that by writing your own authentication/authorization process. Unfortunately, that can take significant time and bloat your scope. Better to use some existing infrastructure, offload the job of authentication/authorization onto a dedicated service, and just interface with that service to make sure that any given client is properly authenticated. So how does that service provide you with those guarantees?
One of the more popular methods of handling authentication/authorization today centers around the use of JWT (Json Web Tokens). The full scope of how the various components (your service, your client, your authentication service, etc.) all interact is a topic that would easily take several lengthy posts to describe, so for our purposes today, we're going to focus on one particular issue: Dynamic JWT Validation.
Any given JWT (that you're likely to see in this kind of scenario) is typically signed by the Private Key of the issuer. In a nutshell, that means that the headers and the payloads of the token are concatenated together, Base-64 encoded, and then hashed. The resulting hash is then encrypted by the Private Key. Being encrypted by the Private Key, it can only be decrypted by the corresponding Public Key. Token validation is the process whereby the Public Key is used to decrypt the hash. Then the validator concatenates the header and payloads, encodes them, and hashes them itself. We know that the key is valid if the generated hash matches the decrypted hash.
In ASP.NET Core, JWT authentication is a first-class middleware supported approach. The entire process of validating a token leverages baked-in core functionality. However, the framework makes one crucial assumption: That you know, before looking at the JWT, what the corresponding public key to use to validate it is. This may not always be the case. Consider a scenario whereby your API can be communicated to by many external systems. Each of these external systems has been approved for this purpose, and your application has access to a database that contains the public keys of each of these systems. These systems connect to yours and supply JWT that are signed by their private keys, so that you can be confident that the communication is actually on behalf of an approved system.
We can digress for a moment, and see how this would be handled if there were only one Public/Private Keypair in play. We start by creating a project:
We set it up to use the baked in ASP.NET Core middleware. The result will look something like the following:
Obviously for your particular scenario, you may want to use different values for the various options. The exact pattern of them is not critical for our purposes today. The important one is the `IssuerSigningKey` value. Note that this is not dynamic; the framework assumes you're using the same key for each request. This will not work for our dynamic scenario. How, then, can we change the key used for each request?
The ASP.NET Core middleware is designed to be robust and extensible. If we examine the system, we see that the `TokenValidationParameters` includes a collection of `SecurityTokenValidators`. These validators perform the actual token validation. Crucially, we can manipulate that process by supplying our own validator. First, we need to implement a custom validator. Here's what one might look like:
Then, we need to configure the middleware to use our validator. This turns out to be relatively trivial:
There is one final gotcha, though. The middleware, as part of its validation, may assume that there are other steps that you may wish to undertake. As part of your custom validation, you may want to override any number of these Events, governing what the middleware will do when it determines that a TokenValidated, AuthenticationFailed, or Challenge is issued. In our case, we want to disable the challenge, as we have sufficient information to determine that we don't need it for this scenario. Here's an example of how to achieve that; bear in mind that literally any custom logic can be applied here to match your particular use case.
And that's it! Your system is now handling JWT with dynamic or arbitrary public keys, and you have hooks in place for customizing exactly how you can get the middleware to work _with_ you instead of _against_ you. All of the source code from this example is publicly available on GitHub. If it helped you in any way, pay it forward; software development is and always must be a team sport. If you've got questions or concerns about what I've discussed here, drop me a line.