Module 4 | General Web » Security
Students should be able to:
We previously talked about client-side security at a high-level. Now we're going to dive a little deeper and talk about one of the strategies for making our applications slightly more secure through the use of JSON Web Tokens.
One of the vulnerable scenarios we talked about when we discussed security was the following:
"someone discovers a server-side endpoint used for sharing private photographs"
This is the exact type of vulnerability we can protect ourselves against with JSON Web Tokens. There are many other types of security threats, and JWTs are not going to solve all of them, but they are a single, simple step we can take towards ensuring our applications are more secure.
Here is what a standard JWT looks like:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
At first glance, it appears to be a giant string of gobbly-gook. Does this look familiar at all? When have you seen something similar?
With a partner, discuss the following questions:
Not only do JWTs look like API keys, they also behave in a very similar manner, with just slight differences. As a definition, both JWTs and API keys can be described as:
Let's dig into that definition a little further as we talk about the similarities and differences between JWTs and API keys.
Encoding is the process of converting data into a format that allows for human readability and efficient transmission/storage. Base64 encoding is a convenient way to manage data in a simple, readable text format rather than as binary data. Base64Url encoding is similar to Base64, but it avoids using common reserved URL characters (e.g. / and -)
Because these strings are Base64Url-Encoded, JWTs and API Keys can be passed through with a request to a server three different ways:
When a client requests a JWT, they generally provide some sort of identifying information about themselves (e.g. a username, app ID, email address, admin role, etc.) The server generates this token based on that information and signs it using a secret key. The server then sends that newly generated token back to the client so it can be used in future requests.
When the client sends this token back to the server when making a new request, the server first decodes the token, verifies all the information is correct, then gives permission for the client to proceed with their request.
The main difference between API Keys and JWTs is the type of data they contain when they are decoded. JWTs can store more custom information and metadata about the identity of a user than an API key. This allows for more granular control over permissions and access to your API.
API Keys are still useful if you simply want to provide general quick-start access to your API. (Generally an API Key gives you all or nothing: if you have a key, you have access to all the endpoints in that API.) They're also useful for keeping track of analytics data like the amount of requests made per user. (Many free, open-source APIs will limit the amount of requests you can make at set intervals.)
We mentioned JWTs differ from API keys in that they can store more custom information and metadata about the identity of a user. Let's head over to JWT.io and take a look at the debugger, which will allow us to get a closer look at the guts of a JWT.
When a JWT is decoded, we can see that it's composed of three distinct parts: a header, a payload (also known as claims), and a signature. Each of these parts are encoded JSON objects that represent different types of information.
The header tells us the type of token we have (in our case, a JWT), and the hashing algorithm used to encrypt the final JWT string. Think about a master lock and how it's opened. The master lock would be our "type" of lock, and the pattern to open it (3 turns to the right, 1 turn to the left, 1 turn to the right) is our "algorithm".
The second part of the JWT is made up of claims. JWT claims are represented again as a JSON Object that contains security information about the message you’re transmitting. The properties on this object ensure the authenticity of the claim, and contain information such as the issuer (identifies the application making the call), issued-at time (when was this token issued?, expiration (when will this token expire/no longer be valid?), and any additional contextual information.
The signature is a super key piece of the JWT. The signature is what's used to verify the sender of the token and ensure that the message hasn't been changed anywhere along the way.
Let's try securing a single server-side endpoint with a JWT. We'll set up a simple express server and utilize the jsonwebtoken npm library for generating and verifying a token that can be used to access our protected endpoint.
secretKey
using app.set
and set the value to a random string (Note: You would not normally store an app secret in your server like this. Because this would be considered sensitive data, you would store it in an environment variable instead.)POST
request that accepts an email
and an appName
.checkAuth
to be used as middleware.checkAuth
function should:
403
and an error message that says 'You must be authorized to hit this endpoint.'403
and message that says 'Invalid token'.appName
part of the token contains a valid application (you can determine the list of approved applications), and then call the express next()
helper to complete the request handler.appName
in the token is invalid, then respond with a 403
and message that says 'Invalid application'.PATCH
request so that only an admin can edit the status of a train line./authenticate
endpoint's payload email address ends in @turing.io
, it should also contain an admin property set to true
. The admin should have special permissions and be allowed to use any DELETE endpoint, which has yet to be created.