identity & security

Adding JSON Web Token API Keys to a DenyList

Learn how to add JWT API keys to a DenyList so they are no longer valid.

Mar 10, 20157 min read

tl;dr: If you understand why and how to support blacklisting JWTs, then skip to the code.

In a previous post, we proposed an approach to using JSON Web Tokens as API Keys, going over some of the benefits of doing so and also providing some examples based on our API v2 scenarios. This post follows up by explaining an aspect that was not covered before: how to add JWT API keys to a DenyList so they are no longer valid.

If you need an in-depth introduction to JSON Web Tokens, check out the free ebook below.

Interested in getting up-to-speed with JWTs as soon as possible?

Download the free ebookJWT Handbook

A Real World Example

Let's for a second assume that GitHub used JSON Web Tokens as API Keys and one of them was accidentally published on the web. You would want to make sure an app can no longer access your information by revoking that token:

Framing the Problem

Providing support for blacklisting JWTs poses the following questions:

  1. How are JWTs individually identified?
  2. Who should be able to revoke JWTs?
  3. How are tokens revoked?
  4. How do we avoid adding overhead?

This blog post aims to answer the previous questions by leveraging our experience from implementing this feature in our API v2.

1. How are JWTs individually identified?

To revoke a JWT we need to be able to tell one token apart from another one. The JWT spec proposes the

jti
(JWT ID) as a means to identify a token. From the specification: > The jti (JWT ID) claim provides a unique identifier for the JWT. 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.

As a quick reminder, this is how the claims section of one of our JWT API tokens looks like:

The tokens accepted by our API use the

aud
claim to determine the tenant for which the JWT is valid. If we use the
(aud, jti)
pair as the token's identifier then each tenant is in charge of guaranteeing that there's no duplication among their tokens.

Similarly, if a token does not include the

jti
claim we do not allow it to be revoked.

2. Who should be able to revoke JWTs?

If anyone could revoke our API keys then unfortunately they wouldn't be of much use. We need a way of restricting who can revoke a JWT.

If anyone could revoke our API keys then unfortunately they wouldn't be of much use.

Tweet This

The way we solved it in our API is by defining a specific scope (permission) that allows blacklisting tokens. If you generate a JWT like the one shown in the next figure you will be able to revoke JWTs:

Notice the

blacklist
action nested inside the
scopes
object.

3. How are tokens revoked?

To blacklist/revoke a token, you need a JWT API key (referred to as

JWT_API_KEY
) like the one described in #2. With it you can issue a
POST
request to
/api/v2/blacklists/tokens
as shown below (new lines added for clarity):

curl -H "Authorization: Bearer {JWT_API_KEY}"
-X POST
-H "Content-Type: application/json"
-d '{"aud":"u6nnAxGVjbBd8etXjj554YKGAG5HuVrp","jti":"test-token"}'
https://login.auth0.com/api/v2/blacklists/tokens

The complete documentation for the endpoint is here but basically you need to:

  • Send the
    aud
    and
    jti
    claims of the JWT to revoke.
  • Send the JWT with the permissions necessary to blacklist tokens in the Authorization header.

To get the revoked tokens you can issue a

GET
to
/api/v2/blacklists/tokens
. You can use the docs to figure out the how.

4. How do we avoid adding overhead?

You might be thinking:

Wasn't the whole point of using JWTs avoiding a DB query?

Well, that is a benefit, though hardly the whole point. There is a caveat though: that question only applies if you have an application with a single issuer, not a multi-tenant system.

If there is more than one tenant, you don't want all of them to share the same secret. You still have to perform a database query to map the

aud
claim to the required
secret
.

With that in mind, these are some of the optimizations that you can implement:

  • Optimization 1: The aforementioned operation involves I/O so it can be performed in parallel with our query to verify if a token has been revoked.

    Of course, you can also add a caching layer with a reasonable expiration time to avoid the DB trips altogether.

  • Optimization 2: Skip the expiration check if the

    jti
    claim is not part of the JWT.

  • Optimization 3: To reduce the size of the revoked tokens store you could automatically remove JWTs from it once their

    exp
    is reached (assuming there is one).

Implementation

We have shipped version 1.3.0 of the open source express-jwt with support for multi-tenancy and blacklisted tokens. We also put together a sample that shows everything working together. The sample is based on our API v2 implementation.

The following code snippets use show the core sample parts:

Securing the endpoint

The first thing we have is an API that we would like to protect. The

express-jwt
middleware is configured by providing:

  • secret
    • A function in charge of retrieving the secret.
  • isRevoked
    • A function in charge of checking if a JWT is revoked.
var expressJwt = require('express-jwt');
// to protect /api routes with JWTs
app.use('/api', expressJwt({
  secret: secretCallback,
  isRevoked: isRevokedCallback
}));

Handling multi-tenancy

The implementation for the

secretCallback
function reads the backing data store to retrieve the secret for a tenant. It caches the secrets using the tenant identifier as the cache key.

If the data layer provides an encrypted tenant secret, it needs to be decrypted before calling

done
.

var LRU = require('lru-cache');

var secretsCache = LRU({ /* options */ });

var secretCallback = function(req, payload, done){
  var audience = payload.aud;
  var cachedSecret = secretsCache.get(audience);

  if (cachedSecret) { return done(null, cachedSecret); }

  data.getTenantByIdentifier(audience, function(err, tenant){
    if (err) { return done(err); }
    if (!tenant) { return done(new Error('missing_secret')); }

    var secret = utilities.decrypt(tenant.secret);
    secretsCache.set(audience, secret);
    done(null, secret);
  });
};

Supporting revoked JWTs

Similarly, the

isRevokedCallback
implementation caches whether a token is revoked or not using the
(aud, jti)
pair as the cache key. It also skips the check in case the
jti
claim is not present.

var jtiCache = LRU({ /* options */ });

var isRevokedCallback = function(req, payload, done){
  var tokenId = payload.jti;
  if (!tokenId){
    // if it does not have jti it cannot be revoked
    return done(null, false);
  }

  var tokenIdentifier = payload.aud + ':' + payload.jti;
  var blacklisted = jtiCache.get(tokenIdentifier);
  if (typeof blacklisted !== 'undefined') { return done(null, blacklisted); }

  data.getRevokedTokenByIdentifier(tokenIdentifier, function(err, token){
    if (err) { return done(err); }
    blacklisted = !!token;
    jtiCache.set(tokenIdentifier,blacklisted)
    return done(null, blacklisted);
  });
};

Conclusion

Most of the aforementioned content applies to blacklisting JWTs in general, not just JWT API keys.

Hopefully this blog post has provided some useful ideas on how to tackle this problem.

If you have any comments or questions don't hesitate to post them!

You an also get involved in express-jwt!

Aside: Delegating JWT Implementation to the Experts

JWTs are an integral part of the OpenID Connect standard, an identity layer that sits on top of the OAuth2 framework. Auth0 is an OpenID Connect certified identity platform. This means that if you pick Auth0 you can be sure it is 100% interoperable with any third party system that also follows the specification.

The OpenID Connect specification requires the use of the JWT format for ID tokens, which contain user profile information (such as the user's name and email) represented in the form of claims. These claims are statements about the user, which can be trusted if the consumer of the token can verify its signature.

While the OAuth2 specification doesn't mandate a format for access tokens, used to grant applications access to APIs on behalf of users, the industry has widely embraced the use of JWTs for these as well.

As a developer, you shouldn't have to worry about directly validating, verifying, or decoding authentication-related JWTs in your services. You can use modern SDKs from Auth0 to handle the correct implementation and usage of JWTs, knowing that they follow the latest industry best practices and are regularly updated to address known security risks.

For example, the Auth0 SDK for Single Page Applications provides a method for extracting user information from an ID Token,

auth0.getUser
.

If you want to try out the Auth0 platform, sign up for a free account and get started! With your free account, you will have access to the following features:

To learn more about JWTs, their internal structure, the different types of algorithms that can be used with them, and other common uses for them, check out the JWT Handbook.