DEV Community

Cover image for Who's calling?
Joakim Wånggren
Joakim Wånggren

Posted on

Who's calling?

Securing APIs with token introspection

Creating secure APIs can be challenging… Should you use API keys or Access tokens? Build your own Authorization server or use an existing one? One thing is certain, you need to know that those calling your endpoints are authorized to do so.

OAuth 2.0 is a de-facto standard for authorizing requests to various sources, relying on access tokens and to a large extent JSON Web Tokens (JWT), especially in the case of OpenID Connect. The granted Access token can be forwarded to any API endpoint, Resource server, and then introspected by the service to either approve or reject the request.

Introspection in this context is the act of verifying the validity of a token. A regular request of a resource may look like this:

API Request with token

  1. Subject/User/Application gets access token from Authorization Server via one of the defined grant types. The token may contain scopes needed to access the Resource Server, like user:read scope.

  2. Application sends a request to Resource Server including the access token as Bearer token. A typical request could be https://api.example.com/users with a HTTP Header like "Authorization: Bearer 2YotnFZFEsicMWpAA".

  3. Resource Server gets the access token and introspects it by either requesting the Authorization Server directly, or in case of JWT by unpacking the token and verifying the signature of the token.

We will focus on that third step, the introspection of the token.

The introspection specification

The RFC 7662 covers how a remote introspection should work, with requests and responses. Simply put, send a request to an introspection endpoint, with either client credentials authorization or Bearer token, including token as POST param and get a response containing at least a claim named active which indicates if the token is valid or not.

POST /introspect HTTP/1.1
Host: server.example.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=2YotnFZFEjr1zCsicMWpAA

The response should according to spec at least contain a claim active, which indicates if the token is valid or not, and a couple of optional claims like to whom the token is issued, when it expires and what scopes it includes.

HTTP/1.1 200 OK
Content-Type: application/json
{
  "active": true,
  "client_id": "l238j323ds-23ij4",
  "username": "jdoe",
  "scope": "read write dolphin",
  "sub": "Z5O3upPC88QrAjx00dis",
  "aud": "https://protected.example.net/resource",
  "iss": "https://server.example.com/",
  "exp": 1419356238,
  "iat": 1419350238
}

The token state can of course be cached, but every new token requires a request to the authorization server. One way of overcoming that additional request is via JSON Web Tokens (JWT) and local introspection.

Local introspection

Much has been said about JWTs and the security considerations, and it won't be addressed here at length. Be thoughtful of what you put into it and make sure you set a proper signing algorithm.

Local introspection means the token is unpacked and validated locally, without a request to a remote server. Though this works with a shared symmetric key, it is recommended to use an asymmetric key pair to sign and validate the signature.

A JWT consists of 3 parts: A header, a body and a signature, joined to one string separated by a dot. The header field contains information of what algorithm is used and what key id was used to create the signature. The key set, or the specific public key, can be fetched from the authorization server's key set endpoint, as defined by RFC 7517. Once the public key is fetched, validate the signature using the specified algorithm. More info about JWTs can be found at https://jwt.io/.

Token introspection package

Keeping track of all that is of course mad, which is why we created a token introspection package for node that handles both local and remote introspection.

You create a promised-based introspector by providing some configuration to the package, and it accepts a token and returns the introspection result.

const tokenIntrospection = require('token-introspection')({
  jwks_uri: 'https://example.com/jwks',
  endpoint: 'https://example.com/introspect',
  client_id: 'client-id',
  client_secret: 'client-secret',
});

tokenIntrospection(token).then(console.log).catch(console.warn);

This introspector will first attempt local introspection, and if that is not possible it will try remote introspection by calling the endpoint with client_id and client_secret as Basic Auth header.


The package is unopinionated in how and in what context it is used, which is why no Express middleware or similar is provided within the package. Below are examples of how to run introspection as both as Express middleware and AWS API Gateway Authorizer.

Token introspection as middleware

Securing your routes in an Express or Express like application is typically done with a middleware that intercepts the requests before it is processed but your endpoint logic. One such middleware, using the token introspection package might look like this.

const tokenIntrospection = require('token-introspection');
const createError = require('http-errors');

const wrap = (fn) => (...args) => fn(...args).catch(args[2]);

const introspectMiddleware = (opts = {}) => {
  const introspect = tokenIntrospection(opts);

  return wrap(async (req, res, next) => {
    try {
      req.token = await introspect(req.token, 'access_token');
      next();
    } catch (err) {
      if (err instanceof tokenIntrospection.errors.TokenNotActiveError) {
        throw new createError.Unauthorized(err.message);
      }
      throw new createError.InternalServerError('An unknown error occurred when introspecting token');
    }
  });
};

// Then use the middleware
app.use(introspectMiddleware({ jwks_uri: 'https://example.com/jwks' }));

Just for clarity I'll throw in a gist of the middleware in use with caching to convey how cache can be added.

Token introspection as Lambda Authorizer

Serverless is all the rage nowadays and AWS Lambda with API Gateway is a great product in many ways for serverless deployments. The API gateway provides possibility to call a special Lambda, called an Authorizer, before your API endpoint is called. This Lambda will receive an event, and when configured correctly, that event includes the access token used to call the API endpoint. Great thing is that AWS will cache the result for a set period of time, which means the Authorizer won't be called multiple times given the same input/token.

An example of API Gateway Lambda Authorizer with token local token introspection:

const tokenIntrospection = require('token-introspection');

const introspect = tokenIntrospection({
  jwks_uri: process.env.JWKS_URI,
  jwks_cache_time: 60 * 60,
});

const hasScope = (token, scope) => token.scope && token.scope.split(' ').includes(scope);

const generatePolicy = (principalId, effect, resource, context = {}) => ({
  principalId,
  context,
  policyDocument: {
    Version: '2012-10-17',
    Statement: [{
      Effect: effect,
      Action: 'execute-api:Invoke',
      Resource: resource,
    }],
  },
});

exports.handler = async (event) => {
  let token;
  try {
    [, token] = event.authorizationToken.match(/^Bearer ([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/);
  } catch (e) {
    throw new Error('Unauthorized');
  }

  try {
    const data = await introspect(token);
    const effect = hasScope(data, process.env.SCOPE) ? 'Allow' : 'Deny';
    return generatePolicy(data.sub || data.client_id, effect, event.methodArn, data);
  } catch (e) {
    throw new Error('Unauthorized');
  }
};

The Authorizer will return 

  • Unauthorized (HTTP 401) for missing token or other introspection errors,
  • Forbidden (HTTP 403) for valid token but missing the required scope,
  • An allow-execution policy for valid token and correct scope

Hopefully this shed some light on token introspection, and how you can use it to secure your API endpoints. It is key to know those who are calling your endpoints are authorized to perform that action. 


Liked what you read? I would really appreciate any comment or suggestion, either here or Twitter or even an issue in the package repo on Github.


Cover image: Photo by Liam Tucker on Unsplash

Top comments (0)