Search docs

Jump between documentation pages.

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.

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.