Recently I've been messing about with ASP.NET Core 1.1, and although the tooling absolutely sucks as of now, it has been a really pleasant experience for the most part. Fortunately, the tooling will be upgraded shortly, so it's just the good stuff that will stick.
While designing my API, I quickly realised there would be calls that require some form of user identification, both because I kind of do need to know who the user is, and because when storing the data provided by their request, I would need to be able to identify that that specific data was theirs further down the line.
I came up with the brilliant idea of generating an API key for the user, and they would just pass that with every request where user identification is required. In short, the process would look something like this:
- User sends a request with some data to the service, including their API key
- The service receives the data, grabs the API key and checks the database for validity
- Return a 401 if the key isn't valid, process the data if it is
- Return a 200, and something that identifies the resource that they have just created
Sounds good right? Well yes, except that every single call that requires authentication has to make at least one round trip to the database, even if the data they provided in their requests turns out to be garbage.
Don't get me wrong, that's fine if you're running a small web service with very few users - but as soon as your service needs to service a larger amount of users, this won't scale all too well.
You end up in a situation where you have to check the API key in storage every single time, because if it's not valid, you both can't and shouldn't service the request. This also means that someone can just hammer your API with invalid keys and garbage data, on every single call that requires authentication. That's not a situation I want to be in.
Having realised that I need something that's very inexpensive in terms of CPU time and I/O to verify a user's authenticity, I went on the internet and found out about JSON web tokens. And boy did that look good.
So what are JSON Web Tokens?
As the JWT.io introduction succinctly states:
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA.
Turns out I was not the only one looking for a solution to this problem. I won't bother going into very much detail about the inner workings of the tokens, as the guys over at JWT.io have done a far better job at explaining them than I ever could.
Implementing them in any .NET environment is relatively simple. There are various libraries available to assist you in creating and validating JWTs, including an official library released and supported by Microsoft.
How do we create a JWT?
First off, grab a copy of System.IdentityModel.Tokens.Jwt through NuGet:
There are a couple of classes that we'll be using to handle tokens:
- JwtSecurityToken - Type representing the actual token itself
- JwtSecurityTokenHandler - Implements functionality to create, validate and encode tokens
- JwtRegisteredClaimNames - Contains constants representing the various standard claims in JWTs as specified by the IETF and OpenID
Armed with these, creating a token is actually fairly trivial. While there are no mandatory claims for JWTs, a handful of them are strongly recommended:
- Issuer (iss) - The entity handing out the tokens. From what I gather, most of the time this is the host name of the service handing out the tokens
- Audience (aud) - The intended recipient of the token. Should be a host name as well. In this case, it's the host name that we're granting access to. If we're handing out tokens for super.duper.com, that'll be the audience of the token. If someone tries to use the token for a different host name, validation will fail because the token was not intended for that host
- Subject (sub) - Contains the username or whatever is used to identify the user this token was issued to
- Expiration Time (exp) - The time after which this token is considered to be invalid, and a new token will have to be issued
On top of that, to sign the token, we'll also need SigningCredentials based on a secret passphrase that only we know.
Because the issuer, audience, the signing credentials and the time span the token is considered valid for is fairly constant, I've cobbled them together in an option type:
Armed with our option type, a string that identifies our user, and some claims, we can create a method to generate tokens for us like so:
Now, to instantiate our option type and start handing out tokens, we'll need some credentials. When using HMAC SHA256, we can simply have a server-side secret and create a symmetric key pair based on it. In code, it'd look something along the lines of:
Your key will need to be at least 256 bits in size, otherwise the ctor for
SymmetricSecurityKey will throw an
ArgumentOutOfRangeException. As an aside, I strongly urge you to not hardcode your secret in your application, but to store it in configuration instead. Make it extremely random, keep it secret, and keep it configurable. The last thing you'll want to worry about should your key get compromised is whether all of your services are compiled using the new key.
Either way, we have all the components we need to generate us a sweet token now. Remember those claims I mentioned earlier, the ones every token should have? We'll throw those in the mix, as well as a unique ID in "jti" and a time stamp in "iat". For the ID, we'll simply use a
Guid in digits format. The time stamp will be the UNIX representation of
Sweet! Now we have a token. But it'll be of the
JwtSecurityToken type, and we can't exactly transmit that via URL. To get there, we'll need to take this token and format it in a "JWT Compact Serialization Format". Fortunately for us, the
JwtSecurityTokenHandler makes this easy for us, we'll only need one call:
var serializedToken = handler.WriteToken(token);
When we run this code, we get a URL-friendly token like:
Now if we run that through the jwt.io debugger, we'll see the claims we provided, our username, and all the other data that went into the token. We can also see that after providing the secret we used to encrypt the token, the debugger was successfully able to verify the signature of the token, verifying it is indeed a token issued by our application:
This means we can trust with relative certainty that the information presented in this token is accurate and hasn't been tampered with, without a single database call. In the case at the start of this post, this means we can include the user's API key in the token as well if we want to, and we could use that to store stuff in our database.
Validating a token
So now we've established how to create a token, they're only ever going to be useful if we can validate them when the client sends them back to us.
There are a ton of parameters we can tweak when validating a JWT. You can view them on GitHub if you feel so inclined, but for now, we'll focus on just a handful of relevant ones:
Validating our token becomes a walk in the park:
JwtValidationResult is a simple immutable type to hold the results of the token validation operation, and some other stuff.
If validation passes, we can use the data stored in the token with relative safety. If the client sends us a skewed token, or one that has expired (the validator will actually throw if a token has expired - a questionable design decision in my opinion), we can simply reject their request and have them (re-)authenticate.
JSON Web Tokens appear to provide a solution to a problem many stateless applications face in these modern times. They allow us to authenticate a user once, and trust the token they send with their requests without having to make round trips to our database or other storage for each subsequent request.
It appears to be a very lightweight, inexpensive approach to authentication, which in modern day REST-services is very much welcome. Theoretically, it should work for both small and large services (and even across services), allowing you to implement authentication once, and scale up as you go without having to go back and changing your code.
I will be updating this post with a GitHub repository once I feel I can get the code covered in this post in a place I am comfortable with.
Thank you for reading!