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
- 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. - Configure a resource server with custom scopes (e.g.
my-api/read,my-api/write) and authorize them on the app client. - 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
pnpm add aws-jwt-verify3. Environment variables
# .env
COGNITO_USER_POOL_ID=us-east-1_AbCdEfGhI
COGNITO_CLIENT_ID=1example23456789
COGNITO_REQUIRED_SCOPE=my-api/read4. Plugin
// 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
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) andcognito:groups— use them for API authorization. - ID tokens carry user attributes (
email,name) and anaudclaim. Verify them withtokenUse: "id"when your UI needs profile data. - Cognito signs with RS256. The library refuses
alg: noneand symmetric algorithms by design.
See also Entra ID, Auth0, and the auth integrations overview.