A New Way to Create Time Restricted Endpoints in .NET
Transcript
It's often a common requirement to have restricted endpoints such as a file download available for a limited amount of time. AWS, for example, offers the ability to have a pre-signed URL for a file in S3. But what if we aren't using S3, or what if we just want to restrict a specific endpoint?
In this video, I'm going to show you how to have a time restricted endpoint in .NET using PASETO. Before we go into the code, let's take a quick look at what PASETO is.
PASETO stands for Platform Agnostic Security Tokens. It's been designed from the ground up to prove that JSON data can be trusted. As such, many of the use cases overlap with JWTs. PASETO tokens are designed for the web, so you can put them in URLs or in headers, and there are even already drafts to get this into OpenID Connect.
One of the major differences between PASETO and JWTs is that PASETO tokens are always authenticated, meaning there is always a cryptographic key associated with them. JWTs however have the option not to do any signing at all, leaving them open to forgery.
If we look at an example of a PASETO token, we can see that it's made up of three to four parts, with each section being base64 URL encoded and joined by the period character in the same way that a JWT would be. The four parts of a token are: version, purpose, payload, and an optional footer.
PASETO tokens are versioned so that as cryptographic standards evolve and older methods become broken or obsolete, the standard itself remains relevant and we can continually upgrade our applications without breaking anything. The cipher suites that are used are defined by the version. Each version provides a cipher suite for each of the available purposes, reducing the chance of misconfiguration by developers. When implementing PASETO, you should always try to use the latest version of the specification where it's available.
The payload section is essentially a collection of claims that are then passed through the cipher suite defined by the version and the purpose selection. There are a number of predefined special claims such as issued at, expiry, and not before. We can also add in our custom claims such as user details, exactly like we would in JWTs.
Lastly, we have the footer section, which is not passed through any cipher suites and is entirely optional. Typically, this will be used for something like the key ID so we know which key was used to secure the payload. This allows us to rotate keys pretty easily.
Before I go into how to use PASETO in.
.NET, I noticed that 76% of you aren't subscribed to the channel. So now we have a basic understanding of PASETO tokens, let's take a look at how to use them in ASP.NET Core.
The first thing that we need to go and do is install the Paseto.Core package written by David De Smet. Once installed, we can start to create keys. Which key you create depends on the purpose of your token. As I'm going to be doing a file download scenario, I don't want anyone else to see the secret information inside the token, so I'm just going to use a local purpose meaning that I will need to generate a symmetric key.
To generate a symmetric key, we need to first create an instance of PasetoBuilder. Then we need to call Use and pass in the version specification that we're going to use alongside the purpose, and then call GenerateSymmetricKey. This key can then be stored in an external store such as AWS Parameter Store or Secrets Manager for use between different instances.
Now we have the key generated, we can start to generate our first token. For our download request endpoint, we need to start with the PasetoBuilder again, passing in the same version and purpose that we used to generate the key, but now we can start adding in some additional data. Once we set the key we wish to use for the encoding, this API is actually pretty straightforward and easy to use. The information you add is up to you. In this example, I've set up things like the issuer, the audience, expiration time, and a few more custom claims.
Once the token has been generated, we will return a redirect result pointing the user to the download endpoint and passing in the token via a query string parameter called token. This replicates the same functionality as you would expect from something like an S3 pre-signed URL, but without exposing any details about the bucket where the file was stored.
For our download endpoint, we first just need to check that the token was passed to us. Assuming that there is, we then use the PasetoBuilder again with the expected version, purpose, and key before we call Decode. Here I've passed in a series of token validation parameters so we can verify things like expiry, audience, and subject, something which I found to be off by default.
Once the token has been decoded, we have complete access to all of the claims contained in the token. If for whatever reason the token cannot be decoded successfully, you also have access to this on the result.
So let's run this from Postman and see it all in action. Once we request a download, we get redirected to a download endpoint which just returns us the contents of the payload for now. If we take a look at the Postman console, you can see that we have completed the redirect and included the token as part of the URL. If we try to hit the download endpoint without the token, we are rejected by the server as expected.
I would be remiss to introduce a new package without talking about its performance characteristics. Here I've written a quick benchmark to cover the encoding and decoding of the tokens based on the examples we have above. As you can see, whilst the library is quick enough considering it's doing cryptographic operations, it does allocate a ton of memory. I suspect there are going to be some quick wins in the library, so if you have some spare time go check out the project and see if you can give them a hand. Who knows, you could learn a performance trick from this video.
If you enjoyed this video, consider subscribing to the YouTube channel for more content like this.