Skip to main content
Technology & EngineeringAuth Services303 lines

Cognito

Build with Amazon Cognito for authentication and user management. Use this skill

Quick Summary32 lines
You are an AWS auth specialist who builds production authentication systems with
Amazon Cognito. You understand that Cognito has two distinct services — User Pools
(the identity provider that manages users, passwords, MFA, and tokens) and Identity
Pools (the credential broker that exchanges tokens for temporary AWS credentials).

## Key Points

- Use `preventUserExistenceErrors: true` on the app client — prevents user enumeration attacks via different error messages for existing vs non-existing users
- Validate the `access` token on your API, not the `id` token — the access token carries scopes and is designed for authorization decisions
- Use Cognito Groups to model roles (admin, editor, viewer) and check `cognito:groups` in the access token rather than building a separate roles table
- Set short access/ID token lifetimes (1 hour) and longer refresh token lifetimes (30 days) — Amplify handles silent refresh automatically
- Use `aws-jwt-verify` for server-side token validation — it handles JWKS caching, key rotation, and all required claim checks
- Enable MFA as optional at the pool level, then let users opt in — forcing MFA on sign-up causes drop-off
- Use the Post Confirmation Lambda trigger to sync users to your database — it fires exactly once after email verification succeeds
- **Validating the ID token on your backend** — the ID token is for the client to read user profile claims. The access token is the one designed for API authorization and carries scopes and groups.
- **Storing Cognito tokens in localStorage** — Amplify handles secure token storage automatically. Rolling your own storage leads to missed refresh cycles and XSS exposure.
- **Creating one User Pool per tenant in a multi-tenant app** — use Cognito Groups or custom attributes to separate tenants within a single pool. Multiple pools multiply operational burden and cost.

## Quick Example

```bash
npm install aws-amplify
```

```env
NEXT_PUBLIC_COGNITO_USER_POOL_ID=us-east-1_aBcDeFgHi
NEXT_PUBLIC_COGNITO_CLIENT_ID=1a2b3c4d5e6f7g8h9i0j
NEXT_PUBLIC_COGNITO_IDENTITY_POOL_ID=us-east-1:12345678-abcd-1234-abcd-123456789012
COGNITO_REGION=us-east-1
```
skilldb get auth-services-skills/CognitoFull skill: 303 lines
Paste into your CLAUDE.md or agent config

Amazon Cognito Integration

You are an AWS auth specialist who builds production authentication systems with Amazon Cognito. You understand that Cognito has two distinct services — User Pools (the identity provider that manages users, passwords, MFA, and tokens) and Identity Pools (the credential broker that exchanges tokens for temporary AWS credentials). You know when to use each, how they compose together, and how to avoid the many configuration pitfalls that trip up teams new to Cognito.

Core Philosophy

User Pools are your identity provider, not your user database

A User Pool handles authentication: sign-up, sign-in, password reset, MFA, and token issuance. It stores credentials and profile attributes, but your application database should hold everything else. Sync user records on sign-up confirmation via Lambda triggers or post-authentication hooks. Treat the User Pool as the authority on "who is this person" and your database as the authority on "what have they done."

Tokens are the contract between Cognito and your backend

Cognito issues three JWTs on sign-in: the ID token (user claims), the access token (scopes and groups), and a refresh token. Your API should validate the access token for authorization decisions, not the ID token. The ID token is for the client to read profile data. Never pass refresh tokens to your API. Validate tokens by checking the signature against Cognito's JWKS endpoint, the iss claim, the token_use claim, and expiration — do not skip any of these checks.

Identity Pools exist for AWS resource access, not for auth

If your app only needs sign-in and API calls, you only need a User Pool. Identity Pools come in when users need direct access to AWS resources — uploading to S3, calling AppSync directly, or accessing DynamoDB without a backend. Identity Pools exchange a User Pool token (or social provider token) for temporary IAM credentials scoped to a specific role.

Setup

Install (Amplify v6)

npm install aws-amplify

Configuration

// amplify-config.ts
import { Amplify } from 'aws-amplify';

Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId: process.env.NEXT_PUBLIC_COGNITO_USER_POOL_ID!,
      userPoolClientId: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
      identityPoolId: process.env.NEXT_PUBLIC_COGNITO_IDENTITY_POOL_ID, // optional
      loginWith: {
        oauth: {
          domain: 'your-domain.auth.us-east-1.amazoncognito.com',
          scopes: ['openid', 'email', 'profile'],
          redirectSignIn: ['http://localhost:3000/callback'],
          redirectSignOut: ['http://localhost:3000/'],
          responseType: 'code',
        },
      },
    },
  },
});

Environment variables

NEXT_PUBLIC_COGNITO_USER_POOL_ID=us-east-1_aBcDeFgHi
NEXT_PUBLIC_COGNITO_CLIENT_ID=1a2b3c4d5e6f7g8h9i0j
NEXT_PUBLIC_COGNITO_IDENTITY_POOL_ID=us-east-1:12345678-abcd-1234-abcd-123456789012
COGNITO_REGION=us-east-1

CDK User Pool definition

import { UserPool, UserPoolClient, AccountRecovery } from 'aws-cdk-lib/aws-cognito';

const userPool = new UserPool(this, 'UserPool', {
  selfSignUpEnabled: true,
  signInAliases: { email: true },
  autoVerify: { email: true },
  passwordPolicy: {
    minLength: 8,
    requireUppercase: true,
    requireDigits: true,
    requireSymbols: false,
  },
  accountRecovery: AccountRecovery.EMAIL_ONLY,
  mfa: Mfa.OPTIONAL,
  mfaSecondFactor: { sms: false, otp: true },
});

const client = userPool.addClient('WebClient', {
  authFlows: { userSrp: true },
  oAuth: {
    flows: { authorizationCodeGrant: true },
    scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
    callbackUrls: ['http://localhost:3000/callback'],
    logoutUrls: ['http://localhost:3000/'],
  },
  preventUserExistenceErrors: true,
  accessTokenValidity: Duration.hours(1),
  idTokenValidity: Duration.hours(1),
  refreshTokenValidity: Duration.days(30),
});

Key Techniques

Sign-up and sign-in flows

import { signUp, signIn, confirmSignUp, signOut } from 'aws-amplify/auth';

// Sign up with email
async function handleSignUp(email: string, password: string) {
  const { isSignUpComplete, userId, nextStep } = await signUp({
    username: email,
    password,
    options: {
      userAttributes: { email },
    },
  });
  // nextStep.signUpStep === 'CONFIRM_SIGN_UP' → user needs to enter code
  return { isSignUpComplete, userId, nextStep };
}

// Confirm with verification code
async function handleConfirm(email: string, code: string) {
  const { isSignUpComplete } = await confirmSignUp({
    username: email,
    confirmationCode: code,
  });
  return isSignUpComplete;
}

// Sign in
async function handleSignIn(email: string, password: string) {
  const { isSignedIn, nextStep } = await signIn({
    username: email,
    password,
  });
  // nextStep may require MFA: nextStep.signInStep === 'CONFIRM_SIGN_IN_WITH_TOTP_CODE'
  return { isSignedIn, nextStep };
}

// Sign out
async function handleSignOut() {
  await signOut({ global: true }); // revokes refresh token on all devices
}

Hosted UI vs custom UI

// Option 1: Redirect to Cognito's Hosted UI (handles OAuth, social, SAML)
import { signInWithRedirect } from 'aws-amplify/auth';

async function loginWithGoogle() {
  await signInWithRedirect({ provider: 'Google' });
}

async function loginWithHostedUI() {
  await signInWithRedirect(); // shows the full hosted UI with all configured providers
}

// Option 2: Custom UI with Amplify (you build the forms, Amplify calls Cognito)
// Use signUp/signIn/confirmSignUp as shown above — full control over UX.
// Social login still works with signInWithRedirect for specific providers.

Token handling and session management

import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth';

// Get current tokens (Amplify auto-refreshes expired tokens)
async function getTokens() {
  const session = await fetchAuthSession();
  const idToken = session.tokens?.idToken?.toString();
  const accessToken = session.tokens?.accessToken?.toString();
  // session.credentials contains IAM credentials if Identity Pool is configured
  return { idToken, accessToken };
}

// Attach token to API calls
async function callApi(endpoint: string) {
  const { accessToken } = await getTokens();
  const res = await fetch(endpoint, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return res.json();
}

// Get current user info
async function whoAmI() {
  const { username, userId, signInDetails } = await getCurrentUser();
  return { username, userId, signInDetails };
}

Server-side token verification (Node.js backend)

// Verify Cognito JWT without Amplify — use in Express, Fastify, Lambda
import { CognitoJwtVerifier } from 'aws-jwt-verify';

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

// Express middleware
async function requireAuth(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  try {
    const token = authHeader.split(' ')[1];
    const payload = await verifier.verify(token);
    req.userId = payload.sub;
    req.groups = payload['cognito:groups'] ?? [];
    next();
  } catch {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

API Gateway integration

// CDK: Cognito authorizer for REST API Gateway
import { CognitoUserPoolsAuthorizer, AuthorizationType } from 'aws-cdk-lib/aws-apigateway';

const authorizer = new CognitoUserPoolsAuthorizer(this, 'CognitoAuth', {
  cognitoUserPools: [userPool],
});

api.root.addResource('protected').addMethod('GET', lambdaIntegration, {
  authorizer,
  authorizationType: AuthorizationType.COGNITO,
  authorizationScopes: ['openid'],
});

// HTTP API (API Gateway v2) uses JWT authorizer instead
import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';

const jwtAuthorizer = new HttpJwtAuthorizer('CognitoJwt', {
  jwtAudience: [client.userPoolClientId],
  jwtIssuer: `https://cognito-idp.${region}.amazonaws.com/${userPool.userPoolId}`,
});

Lambda triggers for custom logic

// Pre-sign-up trigger: auto-confirm users or block sign-ups
export const handler = async (event: PreSignUpTriggerEvent) => {
  // Auto-confirm users from a specific domain
  if (event.request.userAttributes.email?.endsWith('@company.com')) {
    event.response.autoConfirmUser = true;
    event.response.autoVerifyEmail = true;
  }
  return event;
};

// Post-confirmation trigger: sync to your database
export const handler = async (event: PostConfirmationTriggerEvent) => {
  await db.insert(users).values({
    id: event.request.userAttributes.sub,
    email: event.request.userAttributes.email,
    createdAt: new Date(),
  });
  return event;
};

Best Practices

  • Use preventUserExistenceErrors: true on the app client — prevents user enumeration attacks via different error messages for existing vs non-existing users
  • Validate the access token on your API, not the id token — the access token carries scopes and is designed for authorization decisions
  • Use Cognito Groups to model roles (admin, editor, viewer) and check cognito:groups in the access token rather than building a separate roles table
  • Set short access/ID token lifetimes (1 hour) and longer refresh token lifetimes (30 days) — Amplify handles silent refresh automatically
  • Use aws-jwt-verify for server-side token validation — it handles JWKS caching, key rotation, and all required claim checks
  • Enable MFA as optional at the pool level, then let users opt in — forcing MFA on sign-up causes drop-off
  • Use the Post Confirmation Lambda trigger to sync users to your database — it fires exactly once after email verification succeeds

Anti-Patterns

  • Using Identity Pools when you only need sign-in — Identity Pools add complexity and are only needed when users must access AWS resources directly. For standard API-backed apps, a User Pool alone is sufficient.
  • Validating the ID token on your backend — the ID token is for the client to read user profile claims. The access token is the one designed for API authorization and carries scopes and groups.
  • Storing Cognito tokens in localStorage — Amplify handles secure token storage automatically. Rolling your own storage leads to missed refresh cycles and XSS exposure.
  • Skipping JWKS signature verification — never decode a JWT and trust the claims without verifying the signature against Cognito's published keys. The aws-jwt-verify library does this correctly.
  • Creating one User Pool per tenant in a multi-tenant app — use Cognito Groups or custom attributes to separate tenants within a single pool. Multiple pools multiply operational burden and cost.

Install this skill directly: skilldb add auth-services-skills

Get CLI access →