Authentication is a trickier subject today than it was 10 years ago. There are hundreds of potential strategies for securing APIs and ensuring that a user is, in fact, who they say they are. When building or refactoring APIs, developers have to decide what authentication strategy makes sense for their particular product use case. This can be daunting, as choosing the wrong approach can lead to difficulties down the road including data breaches, trouble building integrations, and user experience limitations that plague product development. At BoltSource, we’ve had great success leveraging the generic and flexible stateless authentication mechanism provided by JSON Web Tokens. Most of our engineers have 5+ years of battle hardened experience using JWTs full-stack in large-scale consumer and enterprise grade systems. In this post, we’re going to share what we’ve learned about building APIs with JWTs.
What is a JWT?
JSON Web Token (JWT) is an open standard (RFC 7519), popularized by the good folks at Auth0, that defines a compact and self-contained way for securely transmitting information between parties with an encoded JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret or a public/private key pair.
In this tutorial we are going to focus on using secrets, but know that public/private key pairs are an especially useful tool here if you plan to have a centralized authentication service that generates JWTs with a private key and want other services to be able to verify JWTs using the paired public key in order to avoid sharing secrets between services.
JWTs are encoded into a URI-friendly string, which can be passed between client and server via the header, body, or URI parameters of a request/response. In its compact form, JSON Web Token strings consist of three sections delimited by dots (
.). Below is a description of each section, in order:
- A header, which is a Base64Url encoded object that typically contains the type (JWT) and the signing algorithm (HMAC, SHA256, RSA, etc)
- A payload, which is a Base64Url encoded object that typically contains a set of claims and additional data about the user (such as the user’s primary key).
- A signature, which is encoded by passing the secret, the encoded header, and the encoded payload into the signing algorithm and then encoding the output as Base64Url.
The output is three Base64-URL strings delimited by dots that can be easily passed between client and server in HTML and HTTP environments, while being more compact than alternatives such as SAML. If you want to see this process in action, check out the JWT.io Debugger where you can generate and inspect JWTs and their data
JWTs are used in the same manner as most other token-based authentication strategies. During the login flow, the client (such as a web application) sends the server a set of credentials. The server then looks up the performs checks against the credentials via business logic. If the credentials are accepted, the server generates a JSON Web Token and sends it back to the client as part of the response. The client, optionally storing the token in a cookie, attaches the token to all subsequent authenticated requests to the server, usually via an Authentication header using the Bearer schema. However, the server implementation can be constructed in such a way that it accepts the JWT being passed in other ways, such as in the request body or in the URI.
Now that we have a good high-level understanding of JWTs, let’s take a look at a basic implementation of JWT-based authentication using NodeJS. To get started, we’ll need to install a few packages:
$ npm install jsonwebtoken express body-parser await-to-js
We’ll be referring to a fake database package here as well, called “database” throughout the tutorial. Feel free to use whatever database you are most comfortable with and simply swap out the database access code.
Now that we’ve installed the necessary packages, let’s take a look at how login can be implemented using JWTs:
Here, we can see that it’s fairly easy to set up a simple login route and begin issuing JWT tokens. Next, let’s write code for a protected endpoint and middleware to verify the JWT before allowing access to the endpoint:
As you can see, it’s incredibly easy to adopt JWTs. There is a very low commitment barrier here, as the implementation is not at all opinionated about things like sessions vs session-less or how the underlying data-model works.
This is a very simple example, and might be all that you need! Our engineers have built countless apps that only require this level of complexity at the level of generating and verifying tokens, when combined with a solid secret rotation strategy. The one weakness of this approach is blast radius if the JWT secret is somehow deciphered. This could happen if you forget to add rate limiting to your login and/or registration endpoints, where a bot could hammer these endpoints on a regular cadence and eventually decipher the signature commonalities into a token secret. There are several defenses against this, such as proper rate limiting, but these can be defeated by someone clever enough (e.g. using distributed lambda functions with a cluster of IPs). Rotating secrets regularly is another possibility here, but this forces all of your users to re-login every time it happens. In the next example, we’ll go over a more complicated example limits the blast radius to individual users while also compounding the complexity of reverse engineering the global secret.
Tenant Secret Implementation
In the previous example, we went over a very simple implementation of JWT that utilized a single JWT secret that is shared to generate and verify all JWTs. In this example, we are going to build on top of that to implement a more complicated secret management paradigm that we’ve come to call “tenant secrets”. Before we dive into the code, let’s discuss the high level architecture for this approach so that you can better understand the goal.
At a high level, the tenant secret implementation works much the same way as the simple secret implementation. The only key difference is that, rather than using a single shared secret to generate all JWTs, the tenant secret approach uses a unique secret for every single user. By having a unique JWT secret for every user, a malicious user can, at most, damage data that they already had access to in the first place. This is much more tenable than having an open data breach.
Implementing this approach is dead simple, and only requires an additional table / collection to store each secret object with a foreign key reference to the owning user. Using a caching system like Redis for lookups can also be helpful from a performance standpoint. We will go into more detail around storage and retrieval of tenant secrets on a later blog post. For now, let’s take a look at how the login mechanism could be implemented, using our fake
Now let’s take a look at how we’d modify our existing middleware to support a route:
As you can see, it’s easy to customize the way that you issue and verify JWTs to fit any sort of security requirements.
In our next blog post, I’ll be going into more detail about how to implement a working API complete with tenant-secret JWT authentication, PostgreSQL and Redis integration, and deployment on Kubernetes using Google Kubernetes Engine and Docker. Stay tuned!