npm.io
3.1.0 • Published 3d ago

cognito-toolkit

Licence
BSD-3-Clause
Version
3.1.0
Deps
1
Size
83 kB
Vulns
0
Weekly
0
Stars
6

cognito-toolkit NPM version

AWS Cognito authentication & authorization for web apps: a middleware family for Koa, Express, Fetch-style servers (Bun, Deno, Cloudflare Workers, …), and AWS Lambda with route guards and an auth-cookie convenience, built on AWS's official aws-jwt-verify verifier, plus utilities for obtaining machine-to-machine (client_credentials) access tokens. ESM-only, one runtime dependency (aws-jwt-verify, itself dependency-free).

The port family mirrors dynamodb-toolkit's adapters (./koa, ./express, ./fetch, ./lambda), so the two toolkits compose without glue — e.g. auth.isAuthenticated(createFetchAdapter(adapter)).

v3 is a breaking reshape. The homegrown verifier of 1.x is retired — token verification is delegated to aws-jwt-verify, AWS's own zero-dependency verifier. The sister packages koa-cognito-middleware and cognito-express-middleware moved here as the subpath exports cognito-toolkit/koa and cognito-toolkit/express; the standalone packages are frozen re-exports. See Migrating.

Install

npm install cognito-toolkit

Requires Node 20+ (also runs on the latest Bun and Deno). ESM-only; CommonJS consumers load it via require(esm) (Node 20.19+ / 22.12+) and the named exports:

const {makeGetUser, CognitoJwtVerifier} = require('cognito-toolkit');
const {makeAuth} = require('cognito-toolkit/koa'); // or /express, /fetch, /lambda

Usage

Koa
import Koa from 'koa';
import Router from 'koa-router';
import {CognitoJwtVerifier} from 'cognito-toolkit';
import {makeAuth} from 'cognito-toolkit/koa';

// configure verification on the verifier — pools, app clients, token type
const verifier = CognitoJwtVerifier.create({
  userPoolId: 'us-east-1_MY_USER_POOL',
  clientId: 'my-app-client-id',
  tokenUse: 'access'
});

const auth = makeAuth({verifier});

const app = new Koa();
app.use(auth.getUser); // ctx.state.user = decoded payload or null

const router = new Router();
router.get('/a', ctx => (ctx.body = 'all allowed'));
router.get('/b', auth.isAuthenticated, ctx => (ctx.body = 'all authenticated'));
router.post('/c', auth.hasGroup('user-type/writers'), ctx => (ctx.body = 'only the writers group'));
router.post('/d', auth.hasScope('writers'), ctx => (ctx.body = 'only with a writers scope'));
app.use(router.routes()).use(router.allowedMethods());
Express
import express from 'express';
import cookieParser from 'cookie-parser';
import {CognitoJwtVerifier} from 'cognito-toolkit';
import {makeAuth} from 'cognito-toolkit/express';

const verifier = CognitoJwtVerifier.create({
  userPoolId: 'us-east-1_MY_USER_POOL',
  clientId: 'my-app-client-id',
  tokenUse: 'access'
});

const auth = makeAuth({verifier});

const app = express();
app.use(cookieParser()); // only needed for the auth-cookie features
app.use(auth.getUser); // req.user = decoded payload or null

app.get('/a', (req, res) => res.send('all allowed'));
app.get('/b', auth.isAuthenticated, (req, res) => res.send('all authenticated'));
app.post('/c', auth.hasGroup('user-type/writers'), (req, res) => res.send('only the writers group'));
app.post('/d', auth.hasScope('writers'), (req, res) => res.send('only with a writers scope'));
Fetch-style servers (Bun, Deno, Cloudflare Workers, …)

Requests are immutable in the Fetch world, so there is no state property: guards wrap handlers, and getUser(request) is a memoized lookup — one verification per request no matter how often it's called. Extra server args (Bun's server, Deno's info, Cloudflare's env/ctx) flow through untouched.

import {CognitoJwtVerifier} from 'cognito-toolkit';
import {makeAuth} from 'cognito-toolkit/fetch';

const verifier = CognitoJwtVerifier.create({userPoolId: 'us-east-1_MY_USER_POOL', clientId: 'my-app-client-id', tokenUse: 'access'});
const auth = makeAuth({verifier});

export default {
  fetch: auth.isAuthenticated(async request => {
    const user = await auth.getUser(request); // memoized — already verified by the guard
    return Response.json(user);
  })
};

// per-route guards wrap individual handlers:
const writers = auth.hasGroup('writers')(async request => new Response('ok'));
AWS Lambda

Same wrapper model over Lambda events — API Gateway v1 and v2, Function URLs, and ALB event shapes are auto-detected (headers case-insensitively, v2 cookies array, ALB multi-value mode mirrored on responses).

import {CognitoJwtVerifier} from 'cognito-toolkit';
import {makeAuth} from 'cognito-toolkit/lambda';

const verifier = CognitoJwtVerifier.create({userPoolId: 'us-east-1_MY_USER_POOL', clientId: 'my-app-client-id', tokenUse: 'access'});
const auth = makeAuth({verifier});

export const handler = auth.hasGroup('admins')(async (event, context) => ({statusCode: 200, body: 'ok'}));

Behind API Gateway proper, prefer the built-in Cognito / JWT authorizer — it rejects bad tokens without invoking your function. This port's niche is Function URLs (whose only auth options are IAM or none), ALB targets (pair it with aws-jwt-verify's AlbJwtVerifier when the ALB's authenticate-cognito action forwards x-amzn-oidc-data), and local-debug bridges.

Framework-free
import {makeGetUser, CognitoJwtVerifier} from 'cognito-toolkit';

const verifier = CognitoJwtVerifier.create({userPoolId: 'us-east-1_MY_USER_POOL', clientId: 'my-app-client-id', tokenUse: 'access'});
const getUser = makeGetUser(verifier);

const user = await getUser(tokenFromAnywhere); // decoded payload or null — never throws on a bad token

A rule of thumb for custom validators: an isAllowed rule covers per-route logic; anything about the token itself (audience, token use, extra claim checks) belongs on the verifier — see the aws-jwt-verify documentation for customJwtCheck, multi-pool setups, the generic JwtVerifier for non-Cognito OIDC issuers, and AlbJwtVerifier for ALB-forwarded tokens. All three verifier classes are re-exported here for convenience.

Enable debug logging with the NODE_DEBUG=cognito-toolkit environment variable.

Full docs: this README plus the wikibrowse or search it. Building a web app? Browser usage covers the client side: 401-driven login redirects, return URLs, and renewal timers.

API

makeAuth(options)cognito-toolkit/{koa,express,fetch,lambda}

Builds a middleware bundle bound to one verifier and one options set. There are no module-level singletons — create as many bundles as you need. Options (shared across all four ports; Koa/Express semantics shown, Fetch/Lambda differences below):

  • verifierrequired. An aws-jwt-verify verifier (CognitoJwtVerifier / JwtVerifier), or anything with an async verify(token) that resolves to a payload and throws on an invalid token.
  • authHeader — header carrying the bare token (no Bearer stripping — see Security). A falsy value disables the header source. Default: 'Authorization'.
  • authCookie — cookie carrying the token; also the cookie written by the auth-cookie helpers. A falsy value disables both. Default: 'auth'. (Express reads it from req.cookies — add cookie-parser.)
  • source — custom token source: ctx => token (Koa) / req => token (Express). Overrides the header / cookie lookups.
  • setAuthCookieOptions — when set (an object of cookie options, {} is fine), every authenticated request refreshes the auth cookie automatically.
  • stateUserProperty — where the user lands: ctx.state[prop] (Koa) / req[prop] (Express). Default: 'user'.
  • throwOnError — verification failures throw (as aws-jwt-verify error classes) instead of yielding an anonymous request. Default: false.
The middleware bundle
  • getUser — authenticates every request: the decoded payload (or null) is placed on ctx.state.user / req.user (see stateUserProperty). An authenticated user additionally carries _token (the raw token) and a bound setAuthCookie method.
  • isAuthenticated — guard: 401 unless a user is present.
  • hasGroup(group) — guard: 401 without a user, 403 unless the cognito:groups claim contains group.
  • hasScope(scope) — guard: 401 without a user, 403 unless the space-separated scope claim contains scope.
  • isAllowed(validator) — guard with a custom async rule (ctx | req, groups, scopes) => boolean; a falsy result yields 401 (anonymous) or 403 (authenticated).
  • setAuthCookie(ctx, options?) / setAuthCookie(req, res, options?) — manually persist the current user's token into the auth cookie (skipped when the cookie already holds it). The cookie expires with the token; the domain defaults to the request's hostname.
  • stateUserProperty — the resolved property name (for generic code).

The auth cookie enables a "login once" flow: authenticate via a header once (e.g. after an OAuth2 redirect), persist the token as a cookie, and let subsequent requests authenticate from the cookie automatically.

Fetch and Lambda differences

Requests/events are not decorated in these ports (Fetch Requests are immutable; Lambda events stay pristine), so the bundle shape shifts:

  • No stateUserPropertygetUser(request | event) is a memoized async lookup instead of a middleware: guards and handlers share one verification per request.
  • Guards wrap handlers (auth.hasGroup('g')(handler)) rather than chaining; denials are a Response (Fetch) or a result envelope like {statusCode: 401} (Lambda, mirroring ALB multi-value header mode when the trigger uses it).
  • The automatic auth-cookie refresh applies to responses passing through guards; setAuthCookie is async and returns the response/result to use (Fetch may clone an immutable response; Lambda picks the shape — v2 cookies array, v1/ALB headers). Cookies are serialized in-house: Path=/ and HttpOnly by default, Domain from the request hostname.
makeGetUser(verifier, options?)

The framework-free core (default export): wraps a verifier into token => Promise<payload | null>. An absent token or a failed verification resolves to null; with options.throwOnError verification failures throw instead (an absent token still resolves to null). The returned function carries prime() — pre-fetches the verifier's JWKS (e.g. to avoid first-request latency on a serverless cold start; maps to the verifier's hydrate()).

utils/lazy-access-token

Obtain an OAuth2 client_credentials access token (from a user-pool domain's /oauth2/token) on demand, cached until it nears expiry.

import {createLazyAccessToken} from 'cognito-toolkit/utils/lazy-access-token';

const auth = createLazyAccessToken({
  url: 'https://auth.my-domain.com/oauth2/token',
  clientId: process.env.AUTH_CLIENT_ID,
  secret: process.env.AUTH_CLIENT_SECRET
});

const token = await auth.authorize(); // cached until shortly before expiry
// use token.access_token immediately; call authorize() again when you need it
  • authorize() — returns a cached unexpired token or fetches a fresh one.
  • getToken() — returns the current token (or null) without fetching.
utils/renewable-access-token

Same token, but renewed proactively on a timer instead of on demand.

import {createRenewableAccessToken} from 'cognito-toolkit/utils/renewable-access-token';

const auth = createRenewableAccessToken({
  url: 'https://auth.my-domain.com/oauth2/token',
  clientId: process.env.AUTH_CLIENT_ID,
  secret: process.env.AUTH_CLIENT_SECRET
});

await auth.retrieveToken(); // fetch once; auto-renews thereafter
const token = auth.getToken(); // always read the live token
// on shutdown:
auth.cancelRenewal(true);
  • retrieveToken() — fetches a token and schedules a refresh shortly before expiry. The refresh timer is unrefed, so it never keeps the process alive on its own.
  • cancelRenewal(clearToken?) — cancels the scheduled refresh (or a pending retry); pass true to also drop the cached token.
  • getToken() — returns the current token (or null). The renewal swaps it out over time, so always read it fresh.
  • A failed scheduled refresh keeps the previous token and retries every retryInterval ms (default: one minute) until it succeeds or is cancelled; pass onError to observe the failures (direct retrieveToken() calls throw instead).

Each holder keeps its own state — create one per credential set.

TypeScript

The package ships hand-written .d.ts sidecars — no build step, no separate @types package. The middleware typings use real framework types (@types/koa, @types/express, @types/aws-lambda), which TypeScript consumers of those ports typically already have; the Fetch port needs only the standard Request / Response globals. The payload type flows from the verifier: makeGetUser and makeAuth are generic over it (defaulting to aws-jwt-verify's JwtPayload), and anything satisfying TokenVerifier<P> — an object with an async, throwing verify(jwt) — types through, including hand-rolled test stand-ins.

Security

Verification is aws-jwt-verify's job — algorithm policy, signature, issuer, expiry, and claim checks are all theirs (and battle-tested). Two knobs deserve emphasis because they are on the verifier, not on this package:

  • clientId — restrict to your app client id(s). Without it, a token minted for any app client in the same user pool (including a low-trust one) is accepted.
  • tokenUse — APIs almost always want 'access'; pin it so an id token (meant for the client) can't be used in its place.

Tokens must be the bare JWT — there is no Authorization: Bearer handling. If your clients send that prefix, strip it in a custom source:

const auth = makeAuth({verifier, source: ctx => (ctx.headers.authorization || '').replace(/^Bearer\s+/i, '') || null});

Migrating

From koa-cognito-middleware / cognito-express-middleware 1.x

The features are all here; the wiring changed:

  • Importimport {makeAuth} from 'cognito-toolkit/koa' (or /express) instead of requiring the standalone package.
  • Pool options → a verifier. getUser({region, userPoolId}) becomes makeAuth({verifier: CognitoJwtVerifier.create({userPoolId, clientId, tokenUse})}) — note that clientId is required by aws-jwt-verify (pass null to explicitly opt out) and that pinning tokenUse is now first-class.
  • Guards moved off the function. getUser.isAuthenticated / hasGroup / hasScope / isAllowed and getUser.stateUserProperty were statics shared by every consumer of the module; they are now members of the per-instance bundle returned by makeAuth — same names, same semantics, no cross-app coupling.
  • The auth-cookie behavior (authCookie, setAuthCookieOptions, user.setAuthCookie) is preserved. The cookie domain now defaults to the request hostname — the old host default broke on Express 5, which keeps the port on req.host.
From cognito-toolkit 1.x

1.x exported makeGetUser(pools, options?) with a homegrown verifier behind it. In v3 you build the verifier yourself and hand it over:

// 1.x
const getUser = makeGetUser({region: 'us-east-1', userPoolId: 'us-east-1_X'}, {audience: 'client', tokenUse: 'access'});
// 3.x
const getUser = makeGetUser(CognitoJwtVerifier.create({userPoolId: 'us-east-1_X', clientId: 'client', tokenUse: 'access'}));
  • Pool lists map to CognitoJwtVerifier.create([...]); non-Cognito OIDC issuers map to JwtVerifier.create({issuer, audience, jwksUri?}).
  • 1.x options map onto verifier properties: audienceclientId, validatecustomJwtCheck, clockTolerancegraceSeconds; algorithms policy and JWKS rotation handling are aws-jwt-verify internals now.
  • throwOnError remains, but throws aws-jwt-verify error classes (aws-jwt-verify/error) instead of CognitoAuthError.
  • The utils/ token helpers are unchanged since the 2.x factories: module-level setCredentials() / authorize() / getToken() of 1.x became createLazyAccessToken / createRenewableAccessToken instances.
  • Version 2.0.0 was a zero-dependency rewrite of the verifier that was never published — AWS's aws-jwt-verify covers that ground, which is exactly why v3 delegates to it.

Release notes

  • 3.1.0 New middleware ports: cognito-toolkit/fetch (Bun, Deno, Cloudflare Workers, …) and cognito-toolkit/lambda (API Gateway, Function URLs, ALB); renewable tokens survive failed renewals (retryInterval + onError); AlbJwtVerifier re-exported; browser-side flow documented in the wiki.
  • 3.0.0 Breaking reshape: verification delegated to AWS's aws-jwt-verify; the Koa & Express middlewares absorbed as cognito-toolkit/koa / cognito-toolkit/express with per-instance makeAuth bundles.
  • 2.0.0 Zero-dependency rewrite of the 1.x verifier — never published; superseded by 3.0.0.
  • 1.0.6 Updated dependencies.
  • 1.0.5 Updated dependencies.
  • 1.0.4 Updated dependencies.
  • 1.0.3 Updated dependencies.
  • 1.0.2 Updated dependencies.
  • 1.0.1 Added support for multiple pools.
  • 1.0.0 The initial public release.

The full release notes are in the wiki: Release notes.

License

The 3-Clause BSD License

Keywords