Anatomy of a JSON Web Token
Ryan Chenkie: [0:00] If you're using JSON Web Tokens to secure your API instead of something like cookies and sessions, then it's a good idea to understand how they work. Over at jwt.io, we get a token when the page first loads, and then we also get the decoding of that token.
[0:16] Perhaps, you are a little bit familiar with JSON Web Tokens and you've seen how parts like the header, the payload, and the signature map over to these parts in the encoding. Let's dive into what this jumble of letters and numbers actually means.
[0:31] A JSON Web Token is really just a way to transmit information between two parties. One party might be your frontend React application and another party might be your API. The real value of JSON Web Tokens is they include a security feature. That is that you can be sure that the information that was transmitted in the token wasn't tampered with along the way.
[0:53] As long as you've got a JSON Web Token that passes validation, you can be sure that the information sent in the payload wasn't messed with at all. If a JWT validates, it means that the information that is received is the information that was sent. That's really important when it comes to communicating whether or not a party has access to a resource.
[1:15] Why don't we see if we can reconstruct this JSON Web Token manually? That's going to give us a way to explore what it's all about. Over in my code editor, I've copied over the header and the payload that we get from jwt.io. These are both pretty simple. They are just JSON objects. Now, let's see if we can get them looking like this in coded representation.
[1:37] I've got this function here called encode(). Encode is going to take an object, and it's going to JSON.stringify() and run that through BTOA. BTOA is a method on Window. It stands for "binary to ASCII." This is the browser's built-in way of translating something to a Base64 representation.
[2:01] We won't get too much into Base64 right now, but it's basically a way of taking some kind of binary data and making a string representation out of it. The reason you might want to do this is because it becomes easier to transmit that information.
[2:15] Let's try encoding that header portion that we've got. We can say here, "Let's encode that header". Then let's come back over to the browser and we'll go to this little app we've got going. Up here is this Base64 encoded header. This should match up directly with the header we've got over here, and sure enough, it does.
[2:36] Now the payload is just the same. We can do the same kind of encoding on it here, so we'll encode the payload. We save that and take a look. We've got something now that looks pretty much like we have over in jwt.io, but you'll notice that we've got a little bit of extra stuff here at the end. We've got this double equals.
[2:56] This is what in Base64 is called padding. Sometimes you'll get an extra bit of padding at the end. Now, we'll want to remove this so that we can match up with what's at jwt.io. Also, so we can make our Base64 encoding URL safe. It's common to see a JSON Web Token transmitted in the URL bar, and if we've got certain characters in there it doesn't play so well with the URL bar.
[3:22] I've got this function called MakeURLSafe. This function is going to run the encoding through a string.replace, and it's going to look for a plus, a slash, or an equal sign and then replace those characters with what we are telling it to use as replacements up here.
[3:37] To make a Base64 encoding URL safe, we commonly just replace the plus sign with a dash, the slash with an underscore, and the equal sign with nothing. In the encode() function, let's return MakeURLsafe. If we save this now, the padding gets removed.
[3:54] The header and the payload are fairly straightforward. Those now match up with what's over here, but the signature portion is a little bit more involved. We can get some hints about it if we take a look down here at the verify signature portion.
[4:07] What this is saying is that we want to take our Base64 encoded header and put that together with the Base64 encoded payload, and then take that value along with some secret key that we define and run that through a hashing algorithm.
[4:21] The hashing algorithm used in this example is HMAC-SHA256. This isn't the only hashing algorithm that's used for JSON Web Tokens. There's plenty of others as well. In fact, HMAC-SHA256 is probably a less secure hashing algorithm. There are safer ways to deal with JSON Web Tokens.
[4:38] You'll notice that the secret that I've got plugged in here is just secret123 and this is something that you would never want to use in production. Instead, you'd want to have some long unguessable string, but again, just for demonstration purposes this is what we'll use for now.
[4:52] Now, to produce this signature over here in our code, we're going to need some help from CryptoJS. There are two good reasons to use a library like CryptoJS. The first is that it is a long, complex process to create our own cryptography. Also, we wouldn't want to ever create our own crypto library.
[5:11] This is a problem that has been solved already by people that are way smarter than you or I. It's really for the best that we use battle-tested code that's written by people that know what they're doing. Cryptography really isn't something to mess around with.
[5:23] Let's go to the top and we are going to import HMAC-SHA256 from CryptoJS/HmacSHA256. Then we're going to want another import that will be used alongside this. We'll want to import Base64 from CrytpoJS/enc for encode Base64. Now, let's put in a new function that's going to be used to sign the token. Maybe, we'll call this MakeSignature().
[5:53] The MakeSignature() function is going to take the header and then the payload. Those are two things that we need. Then we're also going to need the secret key.
[6:04] Following along with what we see over at jwt.io, the first part that we're going to want here is the hash itself. What's involved with that is running some things through that HMAC-SHA256() function. This function is going to take a string and then it's going to take a secret, and it's going to produce a hash for us.
[6:24] Now, HMAC-SHA256 is somewhat different from straight-up SHA256 in terms of the hashing algorithm. It's a more complex algorithm and is ultimately a safer one to use.
[6:34] For the string to pass in, let's do this. Let's interpolate out the encoding of the header, so we'll say, "Encode that header." Then tack together the payload separated by a dot. We'll encode the payload, so that's going to be the result string that goes into the hashing algorithm. Along with that is going to go the secret key.
[6:56] Now, the result of this is going to be something that's not immediately usable. What we're going to need to do is stringify it. Down here, let's say, "const stringified = base64," that comes from CryptoJS and we will call it stringify() method. Into there, we'll pass in hashed. Finally, since we want to make this URL safe, we will return MakeURLSafe on stringified.
[7:22] Let's see if we can get that signature portion out on the screen here. Right down here, we can say, "Make signature". We'll pass in the header, the payload, and that JWT secret that we defined above. We'll save this. Now, let's check it out over in the browser. Here is our signature portion. It ends in URv4. We can verify with our encoding here. We get the same results.
[7:48] This means that the last step of the process is actually pretty easy. If we want to get the complete JSON Web Token, we can just give ourselves a function that is going to take a header, a payload, and a signature. Then we will simply return all of that tagged together with dots.
[8:06] We can say header goes there. We'll put a dot. We'll say the payload goes there and then finally the signature goes last.
[8:14] Let's print that final portion out here. We'll say, "Get JWT," and into get JWT we'll pass the encoding of the header. We'll pass the encoding of the payload and we will make the signature, which itself receives the header, the payload, and the JWT secrets.
[8:37] Let's see if we get a good result. We'll give that a save and come back over. Here's the complete token and if we were to do a diff with what we get here at jwt.io, they should be identical. Now, just to be really clear, once again, doing this JSON Web Token construction in the browser is just for demonstration purposes.
[8:57] We would never actually want to be signing tokens like this in our client applications. That's a task that's reserved for the server. That's because the server is the only place that can properly handle secrets, like the secrets that we have to pass in to do the signing portion of the JWT. The server's also the only place that can properly verify a token.
[9:16] The verification step relies on taking that shared secret that we've used and basically undoing the work that we've demonstrated here to prove that the signature portion is valid. Once we know it's valid we can just trust that JSON Web Token and know that it hasn't been tampered with.
[9:31] The reason we know that it hasn't been tampered with is that any attempts to change something up over here, like if we were to change the issued at time to maybe be earlier, any of those attempts are going to throw off this signature portion and change it completely. That's because as we've seen, the signature portion itself relies on the payload to do the signature.
[9:55] There are some other considerations that we've got to make when working with JSON Web Tokens and we'll cover those in other lessons, but hopefully this gives you a good sense of how these different parts work under the hood.