Cognito
Build with Amazon Cognito for authentication and user management. Use this skill
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 linesAmazon 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: trueon the app client — prevents user enumeration attacks via different error messages for existing vs non-existing users - Validate the
accesstoken on your API, not theidtoken — the access token carries scopes and is designed for authorization decisions - Use Cognito Groups to model roles (admin, editor, viewer) and check
cognito:groupsin 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-verifyfor 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-verifylibrary 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
Related Skills
Auth0
Build with Auth0 for enterprise authentication and identity. Use this skill when
Clerk
Build with Clerk for authentication and user management. Use this skill when the
Descope
Integrate Descope to add secure, no-code/low-code authentication and user management to your applications.
Firebase Auth
Build with Firebase Authentication for user sign-in. Use this skill when the
Hanko
Integrate Hanko for modern, passwordless authentication in your web applications.
Kinde
Build with Kinde for authentication and user management. Use this skill when the