Okay I learned some pretty awesome stuff about JSON Web Tokens (JWTs) and handling access tokens today.
Access tokens come in one of two broad varieties:
transparent. Opaque tokens are random sequences of alphanumeric characters with no embedded data - they simply act as a pointer (identifier) for some information to be looked up. Opaque access tokens are not JWTs, and are outside the scope of this post.
Transparent access tokens on the other hand, do indeed have data embedded directly in them. In fact, they might look like random strings of characters, but they’re actually base64 encoded JSON objects (tip: you may notice that all JWTs start with an “ey” … that’s because “ey” is the start of any base64 encoded string that begins with the
One of the great powers of JWT formatted access tokens is that they allow servers to grant authorization to users without having to reach out to an authorization server or database each time a validation is needed - which means performance gains on both the server and the network.
Wow! Sounds too good to be true, right??? Well it is true because, as mentioned earlier, all the data needed for the authorization is embedded and readable in the token itself. On the server side, any transparent access token (JWT) received contains a signature, which is then verified (to make sure it was signed using a protected
secret kept on the server - if so, we can trust that this token was generated directly on the server at some previous point).
This figure shows that a token can be validated on the resource server without needing to reach out to another server to perform a lookup.
Because JWTs are often set to expire (for security reasons, they should not remain valid forever) the verification process must also check the token expiration. The IETF specification includes a number of “regiseterd claim names” which are basically reserved JSON property names to be included in a JSON object in any JWT. One of these properties is
exp which identifies the expiration time of the JWT, specified as the number of seconds since 1970-01-01T00:00:00Z.
By having this date on hand, both the client and the server are able to quickly process if the token is still valid. Versus an
expires_in: 1hr field, (which I’ve seen before) which, when used alone, doesn’t give complete enough information for a stateless evaluation.
Next, I learned that the
jsonwebtoken package available for install via NPM contains all the utilies needed to set and verify expiration. The following code snippet will set the tokens
exp field to be set for 60 seconds from the moment it is signed.
When sent back to the server, the
.verify() method will evaluate that the token has a valid signature and is within the expiration limit. If either is not true, it will throw an error, so you’ll want to wrap this line in a try/catch.
The output of the
.verify() function is the decoded JWT in JSON format, so in the snippet above, the
decodedToken variable we set can then be used to read or do some work directly with the decoded JWT.
.decode() is also available if we want to work with the JWT itself, but aren’t looking to validate it. These values
I’d be remiss not to mention this very handy site that offers a great UI for validating, encoding, and decoding JWTs: https://jwt.io/#debugger-io.
Okay - now that we have JWT access tokens down, what about refresh tokens? A refresh token is a special token that allow a client application to have a new access token issued without the work of requiring a new round of login and credentials checking every time an access token expires. Where do they come from? They are generated by the server at the intial time of sign-in, and sent back to the client with the access token. Because they are longer-lived, the security for refresh tokens should be higher than that for access tokens. I.e. They should only leave the backend when being sent to the authorization server, and the backend itself should be secure. Users and admins have more ability to invalidate refresh tokens than they do Access Tokens. Some reasons that may happen are as follows:
In practice, the authorization service typically makes an endpoint available at
/auth/refresh for the client to send a refresh token and have a new access token given in return.
Tokens have a lifecycle and managing them can potentially add a lot of overhead to your app. When you think about it, there’s a lot to do each time you want to make a call to your token-protected API:
That’s a lot to do. Luckily, there are some cool tricks and tools that can come in handy:
Tokens are very sensitive pieces of data that must be stored somewhere.
Web developers have can leverage several different storage mechanisms provided by the browser (e.g. cookies or web storage like Local Storage and Session Storage). The contents can be viewed in the developer tools on the “Application” tab.
The Local Storage option is nice with convenient features features (unlike Session Storage, data persists beyond a single session or when the browser is closed), but is left vulnerable to XSS attacks.
Now that we’ve covered storage, let discuss management of token lifecycles. Token expiration checking and refresh would be an annoying pain to implement in every single location where we want to make a call to our backend. Luckily axios has a feature called “interceptors” which intercept a request or response before it’s handled by
.catch(). Usage might look like this:
We will use this feature to intercept all requests/responses to our backend API. Luckily we don’t have to apply interceptors universally throughout our app. Instead, we can create a generic axio
instance to communicate with our backend and apply our interceptors to that instance, then import it wherever we need to make calls to our backend. and not just our allow us to centralize this logic and apply it to all outbound/inbound requests/responses.
This is great. Using either of the options above, we can be sure that we have our access token being sent to the server with each request.
Now, what about refreshing the token when it expires? In theory, we could could create some logic that checks for expiration client-side, but we’ll choose not to do that since it’d rely on system clocks being synced. The alternative we’ll choose is to send the original request as-is, but listen for any error responses that suggest and expired token. If we find one, then we’ll use an axios response intercepter to make two more calls: one to refresh the access token, and another to replay the first call using the new acess token.
Does it work? I hope! I have put this all together into a proof of concept application (React/Mongo/Express/Node) that you can find on my GitHub.