# Protect a DaloyJS API with AWS Cognito

[Amazon Cognito](https://aws.amazon.com/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`](https://github.com/awslabs/aws-jwt-verify) library, pure TypeScript, zero runtime dependencies, and edge-runtime compatible via Web Crypto.

**Diagram: Cognito access-token verification**

Participants: Client app, Cognito (user pool), DaloyJS API, Cognito JWKS

1. **Client app -> Cognito (user pool)** (async) - Sign in via hosted UI / authorization-code flow - Cognito issues access + ID tokens (RS256)
2. **Client app -> DaloyJS API** (request) - Call API with Authorization: Bearer <access token>
3. **DaloyJS API -> Cognito JWKS** (async) - CognitoJwtVerifier fetches signing keys (cached) - hydrate() pre-warms the JWKS cache
4. **DaloyJS API -> DaloyJS API** (note) - Verify signature, tokenUse, clientId, scope & cognito:groups
5. **DaloyJS API -> Client app** (response) - Return 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.ts";
import { requireAuth } from "./plugins/auth.ts"; // from the Overview page

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

app.get(
  "/me",
  {
    hooks: requireAuth(process.env.COGNITO_REQUIRED_SCOPE!),
    responses: {
      200: {
        description: "OK",
        body: z.object({ sub: z.string(), groups: z.array(z.string()) }),
      },
    },
  },
  ({ 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](/docs/auth/entra-id), [Auth0](/docs/auth/auth0), and the [auth integrations overview](/docs/auth).

---

Source: https://daloyjs.dev/docs/auth/aws-cognito