• Stars
    star
    452
  • Rank 96,761 (Top 2 %)
  • Language
    TypeScript
  • License
    Apache License 2.0
  • Created about 3 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

JS library for verifying JWTs signed by Amazon Cognito, and any OIDC-compatible IDP that signs JWTs with RS256, RS384, and RS512

AWS JWT Verify

JavaScript library for verifying JWTs signed by Amazon Cognito, and any OIDC-compatible IDP that signs JWTs with RS256 / RS384 / RS512.

Installation

npm install aws-jwt-verify

This library can be used with Node.js 14 or higher. If used with TypeScript, TypeScript 4 or higher is required.

This library can also be used in Web browsers.

Basic usage

Amazon Cognito

import { CognitoJwtVerifier } from "aws-jwt-verify";

// Verifier that expects valid access tokens:
const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
});

try {
  const payload = await verifier.verify(
    "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
  );
  console.log("Token is valid. Payload:", payload);
} catch {
  console.log("Token not valid!");
}

See all verify parameters for Amazon Cognito JWTs here.

Other IDPs

import { JwtRsaVerifier } from "aws-jwt-verify";

const verifier = JwtRsaVerifier.create({
  issuer: "https://example.com/", // set this to the expected "iss" claim on your JWTs
  audience: "<audience>", // set this to the expected "aud" claim on your JWTs
  jwksUri: "https://example.com/.well-known/jwks.json", // set this to the JWKS uri from your OpenID configuration
});

try {
  const payload = await verifier.verify("eyJraWQeyJhdF9oYXNoIjoidk...");
  console.log("Token is valid. Payload:", payload);
} catch {
  console.log("Token not valid!");
}

See all verify parameters for JWTs from any IDP here.

Philosophy of this library

  • Do one thing and do it well. Focus solely on verifying JWTs.
  • Pure TypeScript library that can be used in Node.js v14 and above (both CommonJS and ESM supported), as well in the modern evergreen Web browser.
  • Support both Amazon Cognito as well as any other OIDC-compatible IDP as first class citizen.
  • 0 runtime dependencies, batteries included. This library includes all necessary code to validate RS256/RS384/RS512-signed JWTs. E.g. it contains a simple (and pluggable) HTTP helper to fetch the JWKS from the JWKS URI, and it includes a simple ASN.1 encoder to transform JWKs into DER-encoded RSA public keys (in order to verify JWTs with Node.js native crypto calls).
  • Opinionated towards the best practices as described by the IETF in JSON Web Token Best Current Practices.
  • Make it easy for users to use this library in a secure way. For example, this library requires users to specify issuer and audience, as these should be checked for (see best practices linked to above).

Currently, only signature algorithms RS256 , RS384 and RS512 are supported.

Intended Usage

This library was specifically designed to be easy to use in:

Usage in the Web browser

Many webdev toolchains (e.g. CreateReactApp) make including npm libraries in your web app easy, in which case using this library in your web app should just work.

If you need to bundle this library manually yourself, be aware that this library uses subpath imports, to automatically select the Web crypto implementation when bundling for the browser. This is supported out-of-the-box by webpack and esbuild. An example of using this library in a Vite web app, with Cypress tests, is included in this repository here.

Table of Contents

Verifying JWTs from Amazon Cognito

Create a CognitoJwtVerifier instance and use it to verify JWTs:

import { CognitoJwtVerifier } from "aws-jwt-verify";

// Verifier that expects valid access tokens:
const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
});

try {
  const payload = await verifier.verify(
    "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
  );
  console.log("Token is valid. Payload:", payload);
} catch {
  console.log("Token not valid!");
}

You can also use verifySync, if you've made sure the JWK has already been cached, see further below.

CognitoJwtVerifier verify parameters

Except the User Pool ID, parameters provided when creating the CognitoJwtVerifier act as defaults, that can be overridden upon calling verify or verifySync.

Supported parameters are:

  • tokenUse (mandatory): verify that the JWT's token_use claim matches your expectation. Set to either id or access. Set to null to skip checking token_use.
  • clientId (mandatory): verify that the JWT's aud (id token) or client_id (access token) claim matches your expectation. Provide a string, or an array of strings to allow multiple client ids (i.e. one of these client ids must match the JWT). Set to null to skip checking client id (not recommended unless you know what you are doing).
  • groups (optional): verify that the JWT's cognito:groups claim matches your expectation. Provide a string, or an array of strings to allow multiple groups (i.e. one of these groups must match the JWT).
  • scope (optional): verify that the JWT's scope claim matches your expectation (only of use for access tokens). Provide a string, or an array of strings to allow multiple scopes (i.e. one of these scopes must match the JWT). See also Checking scope.
  • graceSeconds (optional, default 0): to account for clock differences between systems, provide the number of seconds beyond JWT expiry (exp claim) or before "not before" (nbf claim) you will allow.
  • customJwtCheck (optional): your custom function with additional JWT (and JWK) checks to execute (see also below).
  • includeRawJwtInErrors (optional, default false): set to true if you want to peek inside the invalid JWT when verification fails. Refer to: Peek inside invalid JWTs.
import { CognitoJwtVerifier } from "aws-jwt-verify";

const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>", // mandatory, can't be overridden upon calling verify
  tokenUse: "id", // needs to be specified here or upon calling verify
  clientId: "<client_id>", // needs to be specified here or upon calling verify
  groups: "admins", // optional
  graceSeconds: 0, // optional
  scope: "my-api/read", // optional
  customJwtCheck: (payload, header, jwk) => {}, // optional
});

try {
  const payload = await verifier.verify("eyJraWQeyJhdF9oYXNoIjoidk...", {
    groups: "users", // Cognito groups overridden: should be users (not admins)
  });
  console.log("Token is valid. Payload:", payload);
} catch {
  console.log("Token not valid!");
}

Checking scope

If you provide scopes to the CognitoJwtVerifier, the verifier will make sure the scope claim in the JWT includes at least one of those scopes:

import { CognitoJwtVerifier } from "aws-jwt-verify";

const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access", // scopes are only present on Cognito access tokens
  clientId: "<client_id>",
  scope: ["my-api:write", "my-api:admin"],
});

try {
  const payload = await verifier.verify("eyJraWQeyJhdF9oYXNoIjoidk...");
  console.log("Token is valid. Payload:", payload);
} catch {
  console.log("Token not valid!");
}

So a JWT payload like the following would have a valid scope:

{
  "client_id": "<client_id>",
  "scope": "my-api:write someotherscope yetanotherscope", // scope string is split on spaces to gather the array of scopes to compare with
  "iat": 1234567890,
  "...": "..."
}

This scope would not be valid:

{
  "client_id": "<client_id>",
  "scope": "my-api:read someotherscope yetanotherscope", // Neither "my-api:write" nor "my-api:admin" present
  "iat": 1234567890,
  "...": "..."
}

Custom JWT and JWK checks

It's possible to provide a function with your own custom JWT checks. This function will be called if the JWT is valid, at the end of the JWT verification.

The function will be called with:

  • the decoded JWT header
  • the decoded JWT payload
  • the JWK that was used to verify the JWT

Throw an error in this function if you want to reject the JWT.

import { CognitoJwtVerifier } from "aws-jwt-verify";

const idTokenVerifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "id",
  clientId: "<client_id>",
  customJwtCheck: async ({ header, payload, jwk }) => {
    if (header.someHeaderField !== "expected") {
      throw new Error("something wrong with the header");
    }
    if (payload.somePayloadField !== "expected") {
      throw new Error("something wrong with the payload");
    }
    if (jwk.someJwkfField !== "expected") {
      throw new Error("something wrong with the jwk");
    }
    await someAsyncCheck(...); // can call out to a DB or do whatever
  },
});

// This will now throw, even if the JWT is otherwise valid, if your custom function throws:
await idTokenVerifier.verify("eyJraWQeyJhdF9oYXNoIjoidk...");

Note that customJwtCheck may be an async function, but only if you use verify (not supported for verifySync).

Trusting multiple User Pools

If you want to allow JWTs from multiple User Pools, provide an array with these User Pools upon creating the verifier:

import { CognitoJwtVerifier } from "aws-jwt-verify";

// This verifier will trust both User Pools
const idTokenVerifier = CognitoJwtVerifier.create([
  {
    userPoolId: "<user_pool_id>",
    tokenUse: "id",
    clientId: "<client_id>", // clientId is mandatory at verifier level now, to disambiguate between User Pools
  },
  {
    userPoolId: "<user_pool_id_2>",
    tokenUse: "id",
    clientId: "<client_id_2>",
  },
]);

try {
  const idTokenPayload = await idTokenVerifier.verify(
    "eyJraWQeyJhdF9oYXNoIjoidk..." // token must be signed by either of the User Pools
  );
  console.log("Token is valid. Payload:", idTokenPayload);
} catch {
  console.log("Token not valid!");
}

Using the generic JWT RSA verifier for Cognito JWTs

The generic JwtRsaVerifier (see below) can also be used for Cognito, which is useful if you want to define a verifier that trusts multiple IDPs, i.e. Cognito and another IDP.

In this case, leave audience to null, but rather manually add validateCognitoJwtFields in the customJwtCheck. (Only Cognito ID tokens have an audience claim, Cognito Access token have a client_id claim instead. The validateCognitoJwtFields function handles this difference automatically for you)

import { JwtRsaVerifier } from "aws-jwt-verify";
import { validateCognitoJwtFields } from "aws-jwt-verify/cognito-verifier";

const verifier = JwtRsaVerifier.create([
  {
    issuer: "https://cognito-idp.eu-west-1.amazonaws.com/<user_pool_id>",
    audience: null, // audience (~clientId) is checked instead, by the Cognito specific checks below
    customJwtCheck: ({ payload }) =>
      validateCognitoJwtFields(payload, {
        tokenUse: "access", // set to "id" or "access" (or null if both are fine)
        clientId: "<client_id>", // provide the client id, or an array of client ids (or null if you do not want to check client id)
        groups: ["admin", "others"], // optional, provide a group name, or array of group names
      }),
  },
  {
    issuer: "https://example.com/my/other/idp",
    audience: "myaudience", // do specify audience for other IDPs
  },
]);

Verifying JWTs from any OIDC-compatible IDP

The generic JwtRsaVerifier works for any OIDC-compatible IDP that signs JWTs with RS256/RS384/RS512:

import { JwtRsaVerifier } from "aws-jwt-verify";

const verifier = JwtRsaVerifier.create({
  issuer: "https://example.com/", // set this to the expected "iss" claim on your JWTs
  audience: "<audience>", // set this to the expected "aud" claim on your JWTs
  jwksUri: "https://example.com/.well-known/jwks.json", // set this to the JWKS uri from your OpenID configuration
});

try {
  const payload = await verifier.verify("eyJraWQeyJhdF9oYXNoIjoidk...");
  console.log("Token is valid. Payload:", payload);
} catch {
  console.log("Token not valid!");
}

Support Multiple IDP's:

const verifier = JwtRsaVerifier.create([
  {
    issuer: "https://example.com/idp1",
    audience: "expectedAudienceIdp1",
  },
  {
    issuer: "https://example.com/idp2",
    audience: "expectedAudienceIdp2",
  },
]);

try {
  const otherPayload = await verifier.verify("eyJraWQeyJhdF9oYXNoIjoidk..."); // Token must be from either idp1 or idp2
  console.log("Token is valid. Payload:", otherPayload);
} catch {
  console.log("Token not valid!");
}

JwtRsaVerifier verify parameters

Except issuer, parameters provided when creating the JwtRsaVerifier act as defaults, that can be overridden upon calling verify or verifySync.

Supported parameters are:

  • jwksUri (optional, can only be provided at verifier level): the URI where the JWKS can be downloaded from. To find this URI for your IDP, consult your IDP's OpenId configuration (e.g. by opening the OpenId configuration in your browser). Usually, it is ${issuer}/.well-known/jwks.json, which is the default value that will be used if you don't explicitly provide jwksUri.
  • audience (mandatory): verify that the JWT's aud claim matches your expectation. Provide a string, or an array of strings to allow multiple client ids (i.e. one of these audiences must match the JWT). Set to null to skip checking audience (not recommended unless you know what you are doing). Note that a JWT's aud claim might be an array of audiences. The JwtRsaVerifier will in that case make sure that at least one of these audiences matches with at least one of the audiences that were provided to the verifier.
  • scope (optional): verify that the JWT's scope claim matches your expectation (only of use for access tokens). Provide a string, or an array of strings to allow multiple scopes (i.e. one of these scopes must match the JWT). See also Checking scope.
  • graceSeconds (optional, default 0): to account for clock differences between systems, provide the number of seconds beyond JWT expiry (exp claim) or before "not before" (nbf claim) you will allow.
  • customJwtCheck (optional): your custom function with additional JWT checks to execute (see Custom JWT and JWK checks).
  • includeRawJwtInErrors (optional, default false): set to true if you want to peek inside the invalid JWT when verification fails. Refer to: Peek inside invalid JWTs.

Peeking inside unverified JWTs

You can peek into the payload of an unverified JWT as follows.

Note: this does NOT verify a JWT, do not trust the returned payload and header! For most use cases, you would not want to call this function directly yourself, rather you would call verify() with the JWT, which would call this function (and others) for you.

import { decomposeUnverifiedJwt } from "aws-jwt-verify/jwt";

// danger! payload is sanity checked and JSON-parsed, but otherwise unverified, trust nothing in it!
const { payload } = decomposeUnverifiedJwt(
  "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
);

Verification errors

When verification of a JWT fails, this library will throw an error. All errors are defined in src/error.ts and can be imported and tested for like so:

import { CognitoJwtVerifier } from "aws-jwt-verify";
import { JwtExpiredError } from "aws-jwt-verify/error";

const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
});

try {
  const payload = await verifier.verify(
    "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
  );
} catch (err) {
  // An error is thrown, so the JWT is not valid
  // Use `instanceof` to test for specific error cases:
  if (err instanceof JwtExpiredError) {
    console.error("JWT expired!");
  }
  throw err;
}

Peek inside invalid JWTs

If you want to peek inside invalid JWTs, set includeRawJwtInErrors to true when creating the verifier. The thrown error will then include the raw JWT:

import { CognitoJwtVerifier } from "aws-jwt-verify";
import { JwtInvalidClaimError } from "aws-jwt-verify/error";

const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
  includeRawJwtInErrors: true, // can also be specified as parameter to the `verify` call
});

try {
  const payload = await verifier.verify(
    "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
  );
} catch (err) {
  if (err instanceof JwtInvalidClaimError) {
    // You can log the payload of the raw JWT, e.g. to aid in debugging and alerting on authentication errors
    // Be careful not to disclose information on the error reason to the the client
    console.error("JWT invalid because:", err.message);
    console.error("Raw JWT:", err.rawJwt.payload);
  }
  throw new Error("Unauthorized");
}

The instanceof check in the catch block above is crucial, because not all errors will include the rawJwt, only errors that subclass JwtInvalidClaimError will. In order to understand why this makes sense, you should know that this library verifies JWTs in 3 stages, that all must succeed for the JWT to be considered valid:

  • Stage 1: Verify JWT structure and JSON parse the JWT
  • Stage 2: Verify JWT cryptographic signature (i.e. RS256/RS384/RS512)
  • Stage 3: Verify JWT claims (such as e.g. its expiration)

Only in case of stage 3 verification errors, will the raw JWT be included in the error (if you set includeRawJwtInErrors to true). This way, when you look at the invalid raw JWT in the error, you'll know that its structure and signature are at least valid (stages 1 and 2 succeeded).

Note that if you use custom JWT checks, you are in charge of throwing errors in your custom code. You can (optionally) subclass your errors from JwtInvalidClaimError, so that the raw JWT will be included on the errors you throw as well:

import { CognitoJwtVerifier } from "aws-jwt-verify";
import { JwtInvalidClaimError } from "aws-jwt-verify/error";

class CustomError extends JwtInvalidClaimError {}

const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
  includeRawJwtInErrors: true,
  customJwtCheck: ({ payload }) => {
    if (payload.custom_claim !== "expected")
      throw new CustomError("Invalid JWT", payload.custom_claim, "expected");
  },
});

try {
  const payload = await verifier.verify(
    "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
  );
} catch (err) {
  if (err instanceof JwtInvalidClaimError) {
    console.error("JWT invalid:", err.rawJwt.payload);
  }
  throw new Error("Unauthorized");
}

The JWKS cache

The JWKS cache is responsible for fetching the JWKS from the JWKS URI, caching it, and selecting the right JWK from it. Both the CognitoJwtVerifier and the (generic) JwtRsaVerifier utilize an in-memory JWKS cache. For each issuer a JWKS cache is maintained, and each JWK in a JWKS is selected and cached using its kid (key id). The JWKS for an issuer will be fetched once initially, and thereafter only upon key rotations (detected by the occurrence of a JWT with a kid that is not yet in the cache).

Note: examples below work the same for CognitoJwtVerifier and JwtRsaVerifier.

Loading the JWKS from file

If e.g. your runtime environment doesn't have internet access, or you want to prevent the fetch over the network, you can load the JWKS explicitly yourself:

import { CognitoJwtVerifier } from "aws-jwt-verify";
import { readFileSync } from "fs";

const idTokenVerifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "id",
  clientId: "<client_id>",
});

const jwks = JSON.parse(readFileSync("jwks.json", { encoding: "utf-8" }));
idTokenVerifier.cacheJwks(jwks);

// Because the JWKS doesn't need to be downloaded now, you can use verifySync:
try {
  const idTokenPayload = idTokenVerifier.verifySync(
    "eyJraWQeyJhdF9oYXNoIjoidk..."
  );
  console.log("Token is valid. Payload:", payload);
} catch {
  console.log("Token not valid!");
}

// Async verify will of course work as well (and will use the cache also):
try {
  const idTokenPayload = await idTokenVerifier.verify(
    "eyJraWQeyJhdF9oYXNoIjoidk..."
  );
  console.log("Token is valid. Payload:", idTokenPayload);
} catch {
  console.log("Token not valid!");
}

Note that the verifier will still try to fetch the JWKS, if it encounters a JWT with a kid that is not in it's cached JWKS (i.e. to cater for key rotations).

Rate limiting

Both the CognitoJwtVerifier and the JwtRsaVerifier enforce a rate limit of 1 JWKS download per JWKS uri per 10 seconds. This protects users of this library from inadvertently flooding the JWKS uri with requests, and prevents wasting time doing network calls.

The rate limit works as follows (implemented by the penaltyBox, see below). When the verifier fetches the JWKS and fails to locate the JWT's kid in the JWKS, an error is thrown, and a timer of 10 seconds is started. Until that timer completes, the verifier will refuse to fetch the particular JWKS uri again. It will instead throw an error immediately on verify calls where that would require the JWKS to be downloaded.

The verifier will continue to verify JWTs for which the right JWK is already present in the cache, also it will still try other JWKS uris (for other issuers).

It is possible to implement a different rate limiting scheme yourself, by customizing the JWKS cache, or the penaltyBox implementation, see below.

Explicitly hydrating the JWKS cache

In a long running Node.js API (e.g. a Fargate container), it might make sense to hydrate the JWKS cache upon server start up. This will speed up the first JWT verification, as the JWKS doesn't have to be downloaded anymore.

This call will always fetch the current, latest, JWKS for each of the verifier's issuers (even though the JWKS might have been fetched and cached before):

const verifier = JwtRsaVerifier.create([
  {
    issuer: "https://example.com/idp1",
    audience: "myappclient1",
  },
  {
    issuer: "https://example.com/idp2",
    audience: "myappclient2",
  },
]);

// Fetch and cache the JWKS for all configured issuers
await verifier.hydrate();

Note: it is only useful to call this method if your calling process has an idle time window, in which it might just as well fetch the JWKS. For example, during container start up, when the load balancer does not yet route traffic to the container. Calling this method inside API Gateway custom authorizers or Lambda@Edge has no benefit (in fact, awaiting the call as part of the Lambda handler would even hurt performance as it bypasses the existing cached JWKS).

Clearing the JWKS cache

If you have a predefined rotation schedule for your JWKS, you could set the refresh interval of the verifier aligned to this schedule:

import { JwtRsaVerifier } from "aws-jwt-verify";

const verifier = JwtRsaVerifier.create({
  issuer: "https://example.com/",
  audience: "<audience>",
});

setInterval(() => {
  verifier.cacheJwks({ keys: [] }); // empty cache, by loading an empty JWKS
}, 1000 * 60 * 60 * 4); // For a 4 hour refresh schedule

If an automated rotation does not fit your use case, and you need to clear out the JWKS cache, you could use:

verifier.cacheJwks({ keys: [] });

Customizing the JWKS cache

When you instantiate a CognitoJwtVerifier or JwtRsaVerifier without providing a JwksCache, the SimpleJwksCache is used:

import { JwtRsaVerifier } from "aws-jwt-verify";
import { SimpleJwksCache } from "aws-jwt-verify/jwk";

const verifier = JwtRsaVerifier.create({
  issuer: "http://my-tenant.my-idp.com",
});

// Equivalent:
const verifier2 = JwtRsaVerifier.create(
  {
    issuer: "http://my-tenant.my-idp.com",
  },
  {
    jwksCache: new SimpleJwksCache(),
  }
);

The SimpleJwksCache can be tailored by using a different penaltyBox and/or fetcher (see below).

Alternatively, you can implement an entirely custom JwksCache yourself, by creating a class that implements the interface JwksCache (from "aws-jwt-verify/jwk"). This allows for highly custom scenario's, e.g. you could implement a JwksCache with custom logic for selecting a JWK from the JWKS.

Sharing the JWKS cache amongst different verifiers

If you want to define multiple verifiers for the same JWKS uri, it makes sense to share the JWKS cache, so the JWKS will be downloaded and cached once:

import { JwtRsaVerifier } from "aws-jwt-verify";
import { SimpleJwksCache } from "aws-jwt-verify/jwk";

const sharedJwksCache = new SimpleJwksCache();

const verifierA = JwtRsaVerifier.create(
  {
    jwksUri: "https://example.com/keys/jwks.json",
    issuer: "https://example.com/",
    audience: "<audience>",
  },
  {
    jwksCache: sharedJwksCache,
  }
);

const verifierB = JwtRsaVerifier.create(
  {
    jwksUri: "https://example.com/keys/jwks.json", // same JWKS URI, so sharing cache makes sense
    issuer: "https://example.com/",
    audience: "<audience>",
  },
  {
    jwksCache: sharedJwksCache,
  }
);

Using a different JsonFetcher with SimpleJwksCache

When instantiating SimpleJwksCache, the fetcher property can be populated with an instance of a class that implements the interface JsonFetcher (from "aws-jwt-verify/https"), such as the SimpleJsonFetcher (which is the default).

The purpose of the fetcher, is to execute fetches against the JWKS uri (HTTPS GET) and parse the resulting JSON file. The default implementation, the SimpleJsonFetcher, has basic machinery to do fetches over HTTPS. It does 1 (immediate) retry in case of connection errors.

By supplying a custom fetcher when instantiating SimpleJwksCache, instead of SimpleJsonFetcher, you can implement any retry and backoff scheme you want, or use another HTTPS library:

import { JwtRsaVerifier } from "aws-jwt-verify";
import { SimpleJwksCache } from "aws-jwt-verify/jwk";
import { JsonFetcher } from "aws-jwt-verify/https";
import axios from "axios";

// Use axios to do the HTTPS fetches
class CustomFetcher implements JsonFetcher {
  instance = axios.create();
  public async fetch(uri: string) {
    return this.instance.get(uri).then((response) => response.data);
  }
}

const verifier = JwtRsaVerifier.create(
  {
    issuer: "http://my-tenant.my-idp.com",
  },
  {
    jwksCache: new SimpleJwksCache({
      fetcher: new CustomFetcher(),
    }),
  }
);

Configuring the JWKS response timeout and other HTTP options with JsonFetcher

The following configurations are equivalent, use the latter one to set a custom fetch timeout and other HTTP options.

import { CognitoJwtVerifier } from "aws-jwt-verify";

// No jwksCache configured explicitly,
// so the default `SimpleJwksCache` with `SimpleJsonFetcher` will be used,
// with a default response timeout of 1500 ms.:
const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access", // or "id"
  clientId: "<client_id>",
});

Equivalent explicit configuration:

import { CognitoJwtVerifier } from "aws-jwt-verify";
import { SimpleJwksCache } from "aws-jwt-verify/jwk";
import { SimpleJsonFetcher } from "aws-jwt-verify/https";

const verifier = CognitoJwtVerifier.create(
  {
    userPoolId: "<your user pool id>",
    tokenUse: "access", // or "id",
    clientId: "<your client id>",
  },
  {
    jwksCache: new SimpleJwksCache({
      fetcher: new SimpleJsonFetcher({
        defaultRequestOptions: {
          responseTimeout: 1500,
          // You can add additional request options:
          // For NodeJS: https://nodejs.org/api/http.html#httprequestoptions-callback
          // For Web (init object): https://developer.mozilla.org/en-US/docs/Web/API/fetch#syntax
        },
      }),
    }),
  }
);

Using a different penaltyBox with SimpleJwksCache

When instantiating SimpleJwksCache, the penaltyBox property can be populated with an instance of a class that implements the interface PenaltyBox (from "aws-jwt-verify/jwk"), such as the SimplePenaltyBox (which is the default).

The SimpleJwksCache will always do await penaltyBox.wait(jwksUri, kid) before asking the fetcher to fetch the JWKS.

By supplying a custom penaltyBox when instantiating SimpleJwksCache, instead of SimplePenaltyBox, you can implement any waiting scheme you want, in your implementation of the wait function.

The SimpleJwksCache will call penaltyBox.registerSuccessfulAttempt(jwksUri, kid) when it succeeds in locating the right JWK in the JWKS, and call penaltyBox.registerFailedAttempt(jwksUri, kid) otherwise. You need to process these calls, so that you can determine the right amount of waiting in your wait implementation.

import { JwtRsaVerifier } from "aws-jwt-verify";
import {
  SimpleJwksCache,
  SimplePenaltyBox,
  PenaltyBox,
} from "aws-jwt-verify/jwk";

// In this example we use the SimplePenaltyBox, but override the default wait period
const verifier = JwtRsaVerifier.create(
  {
    issuer: "http://my-tenant.my-idp.com",
  },
  {
    jwksCache: new SimpleJwksCache({
      penaltyBox: new SimplePenaltyBox({ waitSeconds: 1 }),
    }),
  }
);

// Or implement your own penaltyBox
// The example here just stupidly waits 5 second always,
// even on the first fetch of the JWKS uri
class CustomPenaltyBox implements PenaltyBox {
  public async wait(jwksUri: string, kid: string) {
    // implement something better
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
  public registerFailedAttempt(jwksUri: string, kid: string) {
    // implement
  }
  public registerSuccessfulAttempt(jwksUri: string, kid: string) {
    // implement
  }
}
const verifier2 = JwtRsaVerifier.create(
  {
    issuer: "http://my-tenant.my-idp.com",
  },
  {
    jwksCache: new SimpleJwksCache({ penaltyBox: new CustomPenaltyBox() }),
  }
);

Usage Examples

CloudFront Lambda@Edge

The verifier should be instantiated outside the Lambda handler, so the verifier's cache can be reused for subsequent requests for as long as the Lambda functions stays "hot".

This is an example of a Viewer Request Lambda@Edge function, that inspects each incoming request. It requires each incoming request to have a valid JWT (in this case an access token that includes scope "read") in the HTTP "Authorization" header.

const { CognitoJwtVerifier } = require("aws-jwt-verify");

// Create the verifier outside the Lambda handler (= during cold start),
// so the cache can be reused for subsequent invocations. Then, only during the
// first invocation, will the verifier actually need to fetch the JWKS.
const jwtVerifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
  scope: "read",
});

exports.handler = async (event) => {
  const { request } = event.Records[0].cf;
  const accessToken = request.headers["authorization"][0].value;
  try {
    await jwtVerifier.verify(accessToken);
  } catch {
    return {
      status: "403",
      body: "Unauthorized",
    };
  }
  return request; // allow request to proceed
};

API Gateway Lambda Authorizer - REST

The verifier should be instantiated outside the Lambda handler, so the verifier's cache can be reused for subsequent requests for as long as the Lambda functions stays "hot".

Two types of API Gateway Lambda authorizers could be created - token based and request-based. For both the types of authorizers, you could use the AWS API Gateway Lambda Authorizer BluePrint as a reference pattern where the token validation could be achieved as follows

For token based authorizers, where lambda event payload is set to Token and token source is set to (http) Header with name authorization:

const { CognitoJwtVerifier } = require("aws-jwt-verify");

// Create the verifier outside the Lambda handler (= during cold start),
// so the cache can be reused for subsequent invocations. Then, only during the
// first invocation, will the verifier actually need to fetch the JWKS.
const jwtVerifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
  scope: "read",
});

exports.handler = async (event) => {
  const accessToken = event.authorizationToken;

  let payload;
  try {
    // If the token is not valid, an error is thrown:
    payload = await jwtVerifier.verify(accessToken);
  } catch {
    // API Gateway wants this *exact* error message, otherwise it returns 500 instead of 401:
    throw new Error("Unauthorized");
  }

  // Proceed with additional authorization logic
  // ...
};

For request based authorizers, where lambda event payload is set to Request and identity source is set to (http) Header with name authorization:

const { CognitoJwtVerifier } = require("aws-jwt-verify");

// Create the verifier outside the Lambda handler (= during cold start),
// so the cache can be reused for subsequent invocations. Then, only during the
// first invocation, will the verifier actually need to fetch the JWKS.
const jwtVerifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
  scope: "read",
});

exports.handler = async (event) => {
  const accessToken = event.headers["authorization"];

  let payload;
  try {
    // If the token is not valid, an error is thrown:
    payload = await jwtVerifier.verify(accessToken);
  } catch {
    // API Gateway wants this *exact* error message, otherwise it returns 500 instead of 401:
    throw new Error("Unauthorized");
  }

  // Proceed with additional authorization logic
  // ...
};

HTTP API Lambda Authorizer

An example of a sample HTTP Lambda authorizer is included here as part of the test suite for the solution (format 2.0).

AppSync Lambda Authorizer

The verifier should be instantiated outside the Lambda handler, so the verifier's cache can be reused for subsequent requests for as long as the Lambda functions stays "hot".

This is an example of AppSync Lambda Authorization function, that validates the JWT is valid (in this case an access token that includes scope "read") along with other authorization business logic

const { CognitoJwtVerifier } = require("aws-jwt-verify");

// Create the verifier outside the Lambda handler (= during cold start),
// so the cache can be reused for subsequent invocations. Then, only during the
// first invocation, will the verifier actually need to fetch the JWKS.
const jwtVerifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
  scope: "read",
});

exports.handler = async (event) => {
  const accessToken = event.authorizationToken;
  try {
    await jwtVerifier.verify(accessToken);
  } catch {
    return {
      isAuthorized: false,
    };
  }
  //Proceed with additional authorization logic
};

Fastify

const { CognitoJwtVerifier } = require("aws-jwt-verify");
const fastify = require("fastify")({ logger: true });

// Create the verifier outside your route handlers,
// so the cache is persisted and can be shared amongst them.
const jwtVerifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
  scope: "read",
});

fastify.get("/", async (request, reply) => {
  try {
    // A valid JWT is expected in the HTTP header "authorization"
    await jwtVerifier.verify(request.headers.authorization);
  } catch (authErr) {
    fastify.log.error(authErr);
    const err = new Error();
    err.statusCode = 403;
    throw err;
  }
  return { private: "only visible to users sending a valid JWT" };
});

const startFastify = async () => {
  try {
    await fastify.listen(3000);
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

// Hydrate the JWT verifier, and start Fastify.
// Hydrating the verifier makes sure the JWKS is loaded into the JWT verifier,
// so it can verify JWTs immediately without any latency.
// (Alternatively, just start Fastify, the JWKS will be downloaded when the first JWT is being verified then)
Promise.all([jwtVerifier.hydrate(), () => fastify.listen(3000)]).catch(
  (err) => {
    fastify.log.error(err);
    process.exit(1);
  }
);

Express

const { CognitoJwtVerifier } = require("aws-jwt-verify");
const express = require("express");
const app = express();
const port = 3000;

// Create the verifier outside your route handlers,
// so the cache is persisted and can be shared amongst them.
const jwtVerifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
  scope: "read",
});

app.get("/", async (req, res, next) => {
  try {
    // A valid JWT is expected in the HTTP header "authorization"
    await jwtVerifier.verify(req.header("authorization"));
  } catch (err) {
    console.error(err);
    return res.status(403).json({ statusCode: 403, message: "Forbidden" });
  }
  res.json({ private: "only visible to users sending a valid JWT" });
});

// Hydrate the JWT verifier, then start express.
// Hydrating the verifier makes sure the JWKS is loaded into the JWT verifier,
// so it can verify JWTs immediately without any latency.
// (Alternatively, just start express, the JWKS will be downloaded when the first JWT is being verified then)
jwtVerifier
  .hydrate()
  .catch((err) => {
    console.error(`Failed to hydrate JWT verifier: ${err}`);
    process.exit(1);
  })
  .then(() =>
    app.listen(port, () => {
      console.log(`Example app listening at http://localhost:${port}`);
    })
  );

Security

See CONTRIBUTING for more information.

License

This project is licensed under the Apache-2.0 License.

More Repositories

1

git-secrets

Prevents you from committing secrets and credentials into git repositories
Shell
11,616
star
2

llrt

LLRT (Low Latency Runtime) is an experimental, lightweight JavaScript runtime designed to address the growing demand for fast and efficient Serverless applications.
JavaScript
8,074
star
3

aws-shell

An integrated shell for working with the AWS CLI.
Python
7,182
star
4

mountpoint-s3

A simple, high-throughput file client for mounting an Amazon S3 bucket as a local file system.
Rust
4,475
star
5

autogluon

AutoGluon: AutoML for Image, Text, and Tabular Data
Python
4,348
star
6

gluonts

Probabilistic time series modeling in Python
Python
3,686
star
7

aws-sdk-rust

AWS SDK for the Rust Programming Language
Rust
3,014
star
8

deequ

Deequ is a library built on top of Apache Spark for defining "unit tests for data", which measure data quality in large datasets.
Scala
2,871
star
9

aws-lambda-rust-runtime

A Rust runtime for AWS Lambda
Rust
2,829
star
10

amazon-redshift-utils

Amazon Redshift Utils contains utilities, scripts and view which are useful in a Redshift environment
Python
2,643
star
11

diagram-maker

A library to display an interactive editor for any graph-like data.
TypeScript
2,359
star
12

amazon-ecr-credential-helper

Automatically gets credentials for Amazon ECR on docker push/docker pull
Go
2,261
star
13

amazon-eks-ami

Packer configuration for building a custom EKS AMI
Shell
2,164
star
14

aws-lambda-powertools-python

A developer toolkit to implement Serverless best practices and increase developer velocity.
Python
2,148
star
15

aws-well-architected-labs

Hands on labs and code to help you learn, measure, and build using architectural best practices.
Python
1,834
star
16

aws-config-rules

[Node, Python, Java] Repository of sample Custom Rules for AWS Config.
Python
1,473
star
17

smithy

Smithy is a protocol-agnostic interface definition language and set of tools for generating clients, servers, and documentation for any programming language.
Java
1,356
star
18

aws-support-tools

Tools and sample code provided by AWS Premium Support.
Python
1,290
star
19

open-data-registry

A registry of publicly available datasets on AWS
Python
1,199
star
20

sockeye

Sequence-to-sequence framework with a focus on Neural Machine Translation based on PyTorch
Python
1,181
star
21

aws-lambda-powertools-typescript

Powertools is a developer toolkit to implement Serverless best practices and increase developer velocity.
TypeScript
1,179
star
22

dgl-ke

High performance, easy-to-use, and scalable package for learning large-scale knowledge graph embeddings.
Python
1,144
star
23

aws-sdk-ios-samples

This repository has samples that demonstrate various aspects of the AWS SDK for iOS, you can get the SDK source on Github https://github.com/aws-amplify/aws-sdk-ios/
Swift
1,038
star
24

amazon-kinesis-video-streams-webrtc-sdk-c

Amazon Kinesis Video Streams Webrtc SDK is for developers to install and customize realtime communication between devices and enable secure streaming of video, audio to Kinesis Video Streams.
C
1,031
star
25

aws-sdk-android-samples

This repository has samples that demonstrate various aspects of the AWS SDK for Android, you can get the SDK source on Github https://github.com/aws-amplify/aws-sdk-android/
Java
1,018
star
26

aws-solutions-constructs

The AWS Solutions Constructs Library is an open-source extension of the AWS Cloud Development Kit (AWS CDK) that provides multi-service, well-architected patterns for quickly defining solutions
TypeScript
1,013
star
27

aws-lambda-go-api-proxy

lambda-go-api-proxy makes it easy to port APIs written with Go frameworks such as Gin (https://gin-gonic.github.io/gin/ ) to AWS Lambda and Amazon API Gateway.
Go
1,005
star
28

aws-cfn-template-flip

Tool for converting AWS CloudFormation templates between JSON and YAML formats.
Python
991
star
29

eks-node-viewer

EKS Node Viewer
Go
947
star
30

multi-model-server

Multi Model Server is a tool for serving neural net models for inference
Java
936
star
31

ec2-spot-labs

Collection of tools and code examples to demonstrate best practices in using Amazon EC2 Spot Instances.
Jupyter Notebook
905
star
32

aws-mobile-appsync-sdk-js

JavaScript library files for Offline, Sync, Sigv4. includes support for React Native
TypeScript
902
star
33

aws-saas-boost

AWS SaaS Boost is a ready-to-use toolset that removes the complexity of successfully running SaaS workloads in the AWS cloud.
Java
901
star
34

fargatecli

CLI for AWS Fargate
Go
891
star
35

fortuna

A Library for Uncertainty Quantification.
Python
882
star
36

aws-api-gateway-developer-portal

A Serverless Developer Portal for easily publishing and cataloging APIs
JavaScript
879
star
37

ecs-refarch-continuous-deployment

ECS Reference Architecture for creating a flexible and scalable deployment pipeline to Amazon ECS using AWS CodePipeline
Shell
842
star
38

dynamodb-data-mapper-js

A schema-based data mapper for Amazon DynamoDB.
TypeScript
818
star
39

goformation

GoFormation is a Go library for working with CloudFormation templates.
Go
812
star
40

flowgger

A fast data collector in Rust
Rust
796
star
41

aws-js-s3-explorer

AWS JavaScript S3 Explorer is a JavaScript application that uses AWS's JavaScript SDK and S3 APIs to make the contents of an S3 bucket easy to browse via a web browser.
HTML
771
star
42

aws-icons-for-plantuml

PlantUML sprites, macros, and other includes for Amazon Web Services services and resources
Python
737
star
43

aws-devops-essential

In few hours, quickly learn how to effectively leverage various AWS services to improve developer productivity and reduce the overall time to market for new product capabilities.
Shell
674
star
44

aws-apigateway-lambda-authorizer-blueprints

Blueprints and examples for Lambda-based custom Authorizers for use in API Gateway.
C#
660
star
45

amazon-ecs-nodejs-microservices

Reference architecture that shows how to take a Node.js application, containerize it, and deploy it as microservices on Amazon Elastic Container Service.
Shell
650
star
46

aws-deployment-framework

The AWS Deployment Framework (ADF) is an extensive and flexible framework to manage and deploy resources across multiple AWS accounts and regions based on AWS Organizations.
Python
636
star
47

amazon-kinesis-client

Client library for Amazon Kinesis
Java
621
star
48

aws-lambda-web-adapter

Run web applications on AWS Lambda
Rust
610
star
49

dgl-lifesci

Python package for graph neural networks in chemistry and biology
Python
594
star
50

data-on-eks

DoEKS is a tool to build, deploy and scale Data & ML Platforms on Amazon EKS
HCL
590
star
51

aws-security-automation

Collection of scripts and resources for DevSecOps and Automated Incident Response Security
Python
585
star
52

aws-glue-libs

AWS Glue Libraries are additions and enhancements to Spark for ETL operations.
Python
565
star
53

python-deequ

Python API for Deequ
Python
535
star
54

aws-athena-query-federation

The Amazon Athena Query Federation SDK allows you to customize Amazon Athena with your own data sources and code.
Java
507
star
55

amazon-dynamodb-lock-client

The AmazonDynamoDBLockClient is a general purpose distributed locking library built on top of DynamoDB. It supports both coarse-grained and fine-grained locking.
Java
469
star
56

shuttle

Shuttle is a library for testing concurrent Rust code
Rust
465
star
57

ami-builder-packer

An example of an AMI Builder using CI/CD with AWS CodePipeline, AWS CodeBuild, Hashicorp Packer and Ansible.
465
star
58

route53-dynamic-dns-with-lambda

A Dynamic DNS system built with API Gateway, Lambda & Route 53.
Python
461
star
59

aws-servicebroker

AWS Service Broker
Python
461
star
60

diagram-as-code

Diagram-as-code for AWS architecture.
Go
459
star
61

amazon-ecs-local-container-endpoints

A container that provides local versions of the ECS Task Metadata Endpoint and ECS Task IAM Roles Endpoint.
Go
456
star
62

datawig

Imputation of missing values in tables.
JavaScript
454
star
63

aws-config-rdk

The AWS Config Rules Development Kit helps developers set up, author and test custom Config rules. It contains scripts to enable AWS Config, create a Config rule and test it with sample ConfigurationItems.
Python
444
star
64

ecs-refarch-service-discovery

An EC2 Container Service Reference Architecture for providing Service Discovery to containers using CloudWatch Events, Lambda and Route 53 private hosted zones.
Go
444
star
65

ssosync

Populate AWS SSO directly with your G Suite users and groups using either a CLI or AWS Lambda
Go
443
star
66

handwritten-text-recognition-for-apache-mxnet

This repository lets you train neural networks models for performing end-to-end full-page handwriting recognition using the Apache MXNet deep learning frameworks on the IAM Dataset.
Jupyter Notebook
442
star
67

awscli-aliases

Repository for AWS CLI aliases.
437
star
68

snapchange

Lightweight fuzzing of a memory snapshot using KVM
Rust
436
star
69

threat-composer

A simple threat modeling tool to help humans to reduce time-to-value when threat modeling
TypeScript
426
star
70

aws-security-assessment-solution

An AWS tool to help you create a point in time assessment of your AWS account using Prowler and Scout as well as optional AWS developed ransomware checks.
423
star
71

lambda-refarch-mapreduce

This repo presents a reference architecture for running serverless MapReduce jobs. This has been implemented using AWS Lambda and Amazon S3.
JavaScript
422
star
72

aws-lambda-cpp

C++ implementation of the AWS Lambda runtime
C++
409
star
73

pgbouncer-fast-switchover

Adds query routing and rewriting extensions to pgbouncer
C
396
star
74

aws-sdk-kotlin

Multiplatform AWS SDK for Kotlin
Kotlin
392
star
75

aws-cloudsaga

AWS CloudSaga - Simulate security events in AWS
Python
389
star
76

amazon-kinesis-producer

Amazon Kinesis Producer Library
C++
385
star
77

soci-snapshotter

Go
383
star
78

serverless-photo-recognition

A collection of 3 lambda functions that are invoked by Amazon S3 or Amazon API Gateway to analyze uploaded images with Amazon Rekognition and save picture labels to ElasticSearch (written in Kotlin)
Kotlin
378
star
79

amazon-sagemaker-workshop

Amazon SageMaker workshops: Introduction, TensorFlow in SageMaker, and more
Jupyter Notebook
378
star
80

serverless-rules

Compilation of rules to validate infrastructure-as-code templates against recommended practices for serverless applications.
Go
378
star
81

logstash-output-amazon_es

Logstash output plugin to sign and export logstash events to Amazon Elasticsearch Service
Ruby
374
star
82

kinesis-aggregation

AWS libraries/modules for working with Kinesis aggregated record data
Java
370
star
83

smithy-rs

Code generation for the AWS SDK for Rust, as well as server and generic smithy client generation.
Rust
369
star
84

syne-tune

Large scale and asynchronous Hyperparameter and Architecture Optimization at your fingertips.
Python
367
star
85

graphstorm

Enterprise graph machine learning framework for billion-scale graphs for ML scientists and data scientists.
Python
366
star
86

dynamodb-transactions

Java
354
star
87

amazon-kinesis-client-python

Amazon Kinesis Client Library for Python
Python
354
star
88

aws-sigv4-proxy

This project signs and proxies HTTP requests with Sigv4
Go
351
star
89

aws-serverless-data-lake-framework

Enterprise-grade, production-hardened, serverless data lake on AWS
Python
349
star
90

amazon-kinesis-agent

Continuously monitors a set of log files and sends new data to the Amazon Kinesis Stream and Amazon Kinesis Firehose in near-real-time.
Java
342
star
91

rds-snapshot-tool

The Snapshot Tool for Amazon RDS automates the task of creating manual snapshots, copying them into a different account and a different region, and deleting them after a specified number of days
Python
337
star
92

amazon-kinesis-scaling-utils

The Kinesis Scaling Utility is designed to give you the ability to scale Amazon Kinesis Streams in the same way that you scale EC2 Auto Scaling groups โ€“ up or down by a count or as a percentage of the total fleet. You can also simply scale to an exact number of Shards. There is no requirement for you to manage the allocation of the keyspace to Shards when using this API, as it is done automatically.
Java
333
star
93

amazon-kinesis-video-streams-producer-sdk-cpp

Amazon Kinesis Video Streams Producer SDK for C++ is for developers to install and customize for their connected camera and other devices to securely stream video, audio, and time-encoded data to Kinesis Video Streams.
C++
332
star
94

landing-zone-accelerator-on-aws

Deploy a multi-account cloud foundation to support highly-regulated workloads and complex compliance requirements.
TypeScript
330
star
95

statelint

A Ruby gem that provides a command-line validator for Amazon States Language JSON files.
Ruby
330
star
96

generative-ai-cdk-constructs

AWS Generative AI CDK Constructs are sample implementations of AWS CDK for common generative AI patterns.
TypeScript
327
star
97

route53-infima

Library for managing service-level fault isolation using Amazon Route 53.
Java
326
star
98

aws-automated-incident-response-and-forensics

326
star
99

mxboard

Logging MXNet data for visualization in TensorBoard.
Python
326
star
100

crossplane-on-eks

Crossplane bespoke composition blueprints for AWS resources
HCL
319
star