Whether you’re writing a public API or an internal microservice, getting authentication right can make or break your API. Let’s take a look at a JSON Web Token-based authentication system.
We’ll begin with basic authentication & JWT concepts, followed by a detailed walkthrough of designing an authentication service with plenty of code examples.
Before we begin, some definitions are in order:
- Credential: a fact that describes an identity
- Authentication: Validation of a credential to identify an entity
- Authorization: Verification that an entity is allowed to access a resource or perform an action
What are JSON Web Tokens?
JSON Web Tokens (JWT - pronounced “jot”) are a compact and self-contained way for securely transmitting information and represent claims between parties as a JSON object.
This is an encoded JSON Web Token:
JSON Web Tokens such as the one shown is a string consisting of three components, each component delimited by a
. (period) character.
Base64Url decoding a JSON Web Token gives us the following:
JSON Web Tokens consists of the following three components: the Header, Payload, and Signature. A token is constructed as follows:
You generate a claim of arbitrary JSON data (the Payload), which in our case contains all the required information about a user for the purposes of authentication. A Header typically defines the signing algorithm
algand type of token
You decorate it with some metadata, such as when the claim expires, who the audience is, etc. These are known as claims, defined in the JWT IETF Draft.
The data (both Header and Payload) is then cryptographically signed with a Hash-based Message Authentication Code (HMAC) secret. This Signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message was’t changed in the way.
The Header, Payload, and Signature are then
Base64encoded and concatenated together with periods to delimit the fields, which results in the token we see in the first example.
JWTs can also be signed using a secret (with HMAC algorithm) OR a public/private key pair using RSA.
For authentication purposes, a JWT serves as the credential/identity object that clients must show to gatekeepers to verify that you’re allowed access protected resources you want to access. It can be signed by a trusted party, and verified by gatekeepers.
JSON Web Tokens work across different programming languages. You should be able to find clients you can use to sign and verify tokens written for your stack.
One of the primary use cases of using JWTs is to authenticate requests. Once a user is logged in, each subsequent request can include the JWT to access previously inaccessible protected resources and services.
To illustrate, let’s imagine an authentication layer for a set of microservices containing a user’s protected resource.
Our authentication flow happens between the following parties:
- Resource Owner (the User): the party that owns the resource to be shared. Let’s call our user Tom.
- Resource Server: the service that holds the protected resource. Our WalletService holds the Wallet resource, which is a user’s digital wallet.
- Authorization Server: the service that verifies the identity of users. Let’s call this AuthService.
- Client: the application (web/mobile/others) that makes requests to the Resource Server on behalf of the Resource Owner. Let’s have a WalletApp Android app.
If you’re familiar with OAuth2, our flow is similar to the Resource Owner Password Credentials Grant flow. Depending on your use case, other flows may be a better fit for your application.
Our entire flow goes as follows:
- Tom the Resource Owner wants to view the contents of his digital wallet through the Client.
- The Client talks to WalletService, requesting for Tom’s Wallet resource.
- Unfortunately, Wallets are a protected resource. Clients will need to pass an access token to continue.
- The Client talks to AuthService, requesting an access token. AuthService responds by asking for the user’s credentials.
- The Client redirects Tom the Resource Owner to the AuthService, which gives Tom the option to either deny or accept the Client’s request for access.
- AuthService verifies Tom’s credentials, redirects her back to the Client, and grants an Authorization Code to the Client.
- The Client presents the authorization code to the AuthService, returning an access token (a JWT) to the Client if successful.
- WalletApp presents the access token to the WalletService, requesting for Tom’s Wallet resource. Whenever the client wants to access a protected route or resource, it should send the JWT, typically in the
Authorizationheader using the
Authorization: Bearer <token>
- WalletService validates the token, decoding the JWT, and parsing its contents.
- (Optional, see Revoking Tokens) WalletService asks AuthService to validate the token.
- If the access token is valid for the requested operation and resource, WalletService returns Tom’s Wallet to the WalletApp Client.
- WalletApp shows Tom his Wallet.
Note that the Resource Owner does not share their credentials to the Client directly. Instead, users notify the Authorizer that the Client may access whatever it is that they requested, and the Client authenticates separately with an authorization code. For more details, check out the OpenID Connect spec.
In this article, we’re focusing primarily on step 8 to 12.
A Minimum Viable Authentication Service
Let’s work on an authentication service for the flow above using plain old Node + Express. Of course, you’re free to use whatever you’d like for your own authentication service.
We need at minimum a single endpoint:
Great! We can now return access tokens upon succesful login. In the next sections, we’ll take a look at introducing additional capabilities for our authentication system as well as writing an authentication middleware that we can easily use to protect the routes of future microservices.
But first, let’s learn more about the reasons why we use JWTs instead of regular plaintext tokens.
Benefits of Using JWTs for Authentication
Using a JSON Web Token as your identity object gives you a handful of advantages compared to an opaque OAuth2
1. Fine Grained Access Control: You can specify detailed access control information within the token itself as part of its payload. In the same way that you can create AWS security policies with very specific permissions, you can limit the token to only give read/write access to a single resource. In contrast, API Keys tend to have a coarse all-or-nothing access.
You can populate your tokens with private claims containing a dynamic set of scopes with JWTs. For example:
Your authentication middleware can parse this JWT metadata and perform validation without making a request to the authorization server. The API endpoint would simply check for the presence of the right scope atribute as follows.
We’ve covered this in the previous section, alongside code examples.
2. Introspectable: A JSON Web Token carries a header-like metadata that can be easily inspected for client-side validation purposes, unlike plaintext
Bearer OAuth tokens which we can’t decode and inspect without making calls to our database.
3. Expirable: JSON Web Tokens can have built-in expiry mechanisms through the
exp property. The
exp (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing.
4. Stateless: All the information needed to complete a particular request is sent along with it, including an
Authorization HTTP header which contains our JWT which serves as an ‘identity object.’ Since the payload contains all the required information for us to authenticate the user, we can avoid making repeated calls to our database.
5. Encrypted: While a JWT’s signature prevents malicious parties from tampering with it, the token’s header is only
Base64 encoded. When dealing with confidential identifiers in your tokens, you should encrypt your tokens using
At this point you might be thinking:
“Wow, that’s great! I can implement my authentication entirely stateless without having to store any session information!”
The above is true in that you can perform client side validation on the
exp expiry time claim to invalidate an expired token.
However, we notice a few issues that our current design have not addressed:
What happens when you want to log a user out of an application? Say you updated the schema of your tokens. How do you handle the old tokens that still haven’t expired? When you deploy this update to the application, how do you invalidate the current sessions? When you’re storing sessions?
At this point, we don’t have a way for our authorization server to invalidate a session that has not yet expired.
One issue with a purely stateless approach is we have no way to revoke/invalidate issued tokens before they expire. In other words, we can’t manually log out a user. If a malicious party manages to acquire a token and we KNOW that a malicious party has the token, we’d be sitting ducks. We have no way to take away access for already issued tokens.
We could have client-side logic that clears any expired session tokens during validation. However, client-side security is insufficient. To help prevent token misuse, we need the ability to revoke tokens that have already been issued.
There’s the chance that malicious parties can see your services’ request headers. Tou should use TLS for client-server and intra-service communication to ensure that nobody sniffs your network requests. Having said that, we want our authentication system to be safe even if an attacker can intercept the network communication between the client and the server.
Depending on your use case, there are two approaches we can take to support two different token invalidation capabilities. Both approaches require the use of additional storage such as Redis for storing some form of a token’s identifier.
Redis is a wonderful key-value storage and can be extremely useful to save ephemeral data such as our tokens. It includes features like automatic deletion or expiration for tokens, can handle lots of writes, and is horizontally scalable.
Both approaches also require our validation middleware to make requests to the authorization server for token verification. Let’s take a look at how we can implement them:
1. To be able to revoke all tokens belonging to a single user, we can simply sign JWTs belonging to that user with her very own private secret. You can dynamically generate these secrets or you can use a hash of their password.
Then, during our token validation process, we can retrieve this private secret from a DB/service (in our case from
KeyService) to verify the token’s signature.
Revoking the token can be done by changing or deleting that user’s secret, thus invalidating all issued tokens belonging that that user.
2. To be able to revoke an individual token, where users can have multiple tokens on different devices, will require us to generate a unique
jti identifier for each JWT, which we can use as an identifier in
KeyService for retrieving a dynamically generated, session-specific secret created for the purposes of signing and verifying a single token.
The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. One approach that can help minimize collisions is to use
uuids instead of
integers as your identifier.
For both approaches, we also need to prevent replay attacks by including unique identifiers in the
jtiand timestamps in the
iat(issued at) claim as part of your JWT payload. This ensures that each token generated is unique.
We need to add additional endpoints:
|GET||/sessions/:id||Retrieve private secret specific to user/session|
GET endpoint will be primarily used by our authentication middleware to retrieve the secret used to sign the JWT and verify if the signature is valid.
DELETE endpoint will either change or remove the secret used for the user’s session at a particular device so that the JWT signature verification fails and a
403 Forbidden response is triggered.
We also create a service wrapper for storing user/session-specific secrets used to sign JWTs, with methods
Note that an expiry mechanism is built in, which utilizes Redis’
EXPIRE feature to automatically remove sessions that have expired, thereby invalidating any issued tokens signed with that secret.
Here is our main router, updated to handle the additional endpoints and talk to
Updated Authentication Flow
Below is our updated flow with support for revoking already issued tokens:
We introduce some additional steps in our token validation process (this happens in our middleware) that communicates with an external private secret storage
KeyService to retrieve the secrets necessary for decoding and verifying the JWT signature.
As we’ve talked about, this allows us to introduce the ability to expire and manually revoke already issued tokens at the cost of some complexity.
A Minimum Viable Authentication Middleware
Alongside our AuthService, we can and should write a companion Node.js module that other developers can use to easily add authentication to their microservices. For example:
FYI, the isAuthenticated middleware checks that the Authorization: Bearer
headers in each incoming request to that route is valid. The source code is supplied below.
You can also protect ALL routes like so:
isAuthenticated middleware can be written as follows:
KeyServiceis a wrapper over the Redis storage used to store session-specific user keys, indexed by a
uuididentifier contained in the token’s
jticlaim. We’ve seen this before, except the operations defined for our middleware is strictly read-only.
JWT is a lightweight wrapper of the
jsrsasign crypto library. We use the
jsrsassign crypto library to verify our JWTs:
Be careful when choosing which crypto library you use to sign and verify JWTs. Be sure to check jwt.io for any security vulnerabilities.
Q: Why are we using both a global secret alongside the user-specific secret when computing
A: Having a global secret lets us easily invalidate ALL tokens belonging to ALL users by simply changing this single secret value, thus invaliding the JWT signatures of all already issued tokens.
Writing modules for cross-cutting concerns such as authentication in this way lets you save development time and effort on future microservices. You can quickly bootstrap new services with an increasingly rich set of capabilities as you write more and more reusable modules. Shared modules also helps keep behaviour consistent across all your different services.
Other JWT Use Cases
JSON Web Tokens can securely transmit information between parties, since its signature lets us be sure that its senders are who we expect. Other use cases involving JWTs include as tokens within reset password links. We can use JWTs to create signed hyperlinks without needing to store password reset tokens in a database.
I’ve presented one approach to building an authentication layer using JSON Web tokens. We’ve also gone through a number of the design decisions to help prevent some security loopholes.
While JWTs may seem like a fairly sane method of authentication it’s important for us to not ignore the lessons we’ve learned from older authentication schemes with years of combat experience.
Through this process, I hope I’ve shared with you how client-side authentication schemes using JWTs has its own risks and limitations which needs to be investigated throughly before going into implementation.
Let me know what you think in the comments below!
📬 Get updates straight to your inbox.
Subscribe to my newsletter to make sure you don't miss anything.