Search docs

Jump between documentation pages.

Browse docs

Protect a DaloyJS API with AWS Cognito

Amazon Cognito user pools provide hosted sign-up, sign-in, MFA, and federation. This guide verifies the access tokens Cognito issues using the AWS-recommended aws-jwt-verify library, pure TypeScript, zero runtime dependencies, and edge-runtime compatible via Web Crypto.

Cognito access-token verification
Client appCognito (user pool)DaloyJS APICognito JWKS
  1. 01asyncClient appCognito (user pool)Sign in via hosted UI / authorization-code flowCognito issues access + ID tokens (RS256)
  2. 02requestClient appDaloyJS APICall API with Authorization: Bearer <access token>
  3. 03asyncDaloyJS APICognito JWKSCognitoJwtVerifier fetches signing keys (cached)hydrate() pre-warms the JWKS cache
  4. 04noteDaloyJS APIDaloyJS APIVerify signature, tokenUse, clientId, scope & cognito:groups
  5. 05responseDaloyJS APIClient appReturn protected data after requireAuth passes
The DaloyJS API only verifies the access token Cognito issued. It never sees user passwords, and the JWKS is cached so most requests verify without a network round-trip.

1. Provision

  1. Create a user pool in the AWS console, then add an app client. Note the User pool ID (e.g. us-east-1_AbCdEfGhI) and the App client ID.
  2. Configure a resource server with custom scopes (e.g. my-api/read, my-api/write) and authorize them on the app client.
  3. Enable a hosted UI domain or use the OAuth 2.0 authorization-code flow from your client app. Your DaloyJS API only needs to verify the resulting access token, it never sees passwords.

2. Install

ts
pnpm add aws-jwt-verify

3. Environment variables

ts
# .env
COGNITO_USER_POOL_ID=us-east-1_AbCdEfGhI
COGNITO_CLIENT_ID=1example23456789
COGNITO_REQUIRED_SCOPE=my-api/read

4. Plugin

ts
// src/plugins/cognito.ts
import { CognitoJwtVerifier } from "aws-jwt-verify";
import type { App } from "@daloyjs/core";

const verifier = CognitoJwtVerifier.create({
  userPoolId: process.env.COGNITO_USER_POOL_ID!,
  tokenUse: "access", // or "id"
  clientId: process.env.COGNITO_CLIENT_ID!,
});

export interface Principal {
  sub: string;
  scopes: string[];
  groups: string[];
  claims: Record<string, unknown>;
}

export const cognitoPlugin = {
  name: "cognito",
  register(app: App) {
    app.decorate("verifier", {
      async verify(token: string): Promise<Principal> {
        const payload = await verifier.verify(token);
        return {
          sub: String(payload.sub),
          scopes: typeof payload.scope === "string" ? payload.scope.split(" ") : [],
          groups: (payload["cognito:groups"] as string[]) ?? [],
          claims: payload as Record<string, unknown>,
        };
      },
    });
    // Pre-fetch the JWKS so the first request isn't slowed by a cold cache
    void verifier.hydrate();
  },
};

declare module "@daloyjs/core" {
  interface AppState {
    verifier: { verify(token: string): Promise<Principal> };
    principal?: Principal;
  }
}

verifier.hydrate() downloads the JWKS up front so the first authenticated request doesn't pay a network round-trip. Subsequent key rotations are picked up automatically.

5. Guard a route

ts
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { cognitoPlugin } from "./plugins/cognito";
import { requireAuth } from "./plugins/auth"; // from the Overview page

const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
app.register(cognitoPlugin);

app.route({
  method: "GET",
  path: "/me",
  operationId: "getMe",
  middleware: [requireAuth(process.env.COGNITO_REQUIRED_SCOPE!)],
  responses: {
    200: {
      description: "OK",
      body: z.object({ sub: z.string(), groups: z.array(z.string()) }),
    },
  },
  handler: ({ state }) => ({
    status: 200,
    body: { sub: state.principal!.sub, groups: state.principal!.groups },
  }),
});

Trusting multiple pools or IdPs

CognitoJwtVerifier.create([...]) accepts an array of pool configurations to trust JWTs from more than one user pool. To trust a Cognito pool and a non-Cognito OIDC IdP, use the generic JwtVerifier with validateCognitoJwtFields in a customJwtCheck.

Notes on tokens

  • Access tokens carry scope (space-separated string) and cognito:groups: use them for API authorization.
  • ID tokens carry user attributes (email, name) and an aud claim. Verify them with tokenUse: "id" when your UI needs profile data.
  • Cognito signs with RS256. The library refuses alg: none and symmetric algorithms by design.

See also Entra ID, Auth0, and the auth integrations overview.