AWS Cognito Integration
Integrate AWS Cognito User Pools as the identity provider for your marketplace application. This guide uses the Cognito Admin APIs (signed with AWS Signature V4) for user provisioning and the ADMIN_USER_PASSWORD_AUTH flow for token acquisition.
Prerequisites
- AWS Account with IAM credentials that have Cognito permissions
- Cognito User Pool with:
- Custom attributes:
custom:gwUserId,custom:orgId,custom:gwApplicationId - Password policy: minimum 8 characters
- App client with
ALLOW_ADMIN_USER_PASSWORD_AUTHflow enabled and a client secret
- Custom attributes:
- User Pool Groups:
gw-userandorg-admin - IAM Policy granting Cognito admin operations on the user pool
AWS Setup
-
Create a User Pool (Cognito > User Pools > Create):
Pool Name: gw-marketplace-usersAttributes:- custom:gwUserId (String, mutable)- custom:orgId (String, mutable)- custom:gwApplicationId (String, mutable)Password Policy: Min 8 chars (relax for programmatic users) -
Create an App Client (App integration > App clients > Create):
Client Name: gw-marketplace-clientGenerate secret: YesAuth Flows: ALLOW_ADMIN_USER_PASSWORD_AUTH -
Create Groups (Groups > Create group):
gw-user-- All marketplace usersorg-admin-- Organization administrators
-
Create IAM Policy for the service account:
{"Effect": "Allow","Action": ["cognito-idp:AdminCreateUser","cognito-idp:AdminSetUserPassword","cognito-idp:AdminInitiateAuth","cognito-idp:AdminGetUser","cognito-idp:AdminUpdateUserAttributes","cognito-idp:AdminAddUserToGroup","cognito-idp:ListUsers","cognito-idp:RevokeToken","cognito-idp:AdminUserGlobalSignOut"],"Resource": "arn:aws:cognito-idp:{region}:{account}:userpool/{pool-id}"}
Environment Variables
# AWS Region
COGNITO_REGION=us-east-1
# User Pool Configuration
COGNITO_USER_POOL_ID=us-east-1_aBcDeFgHi
COGNITO_CLIENT_ID=your-app-client-id
COGNITO_CLIENT_SECRET=your-app-client-secret
# IAM Credentials (for Admin API access)
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=your-secret-key
# Marketplace Configuration
MARKETPLACE_APP_ID=your-registered-app-id
Exchange Flow
sequenceDiagram
participant SDK as Provider SDK
participant Backend as Your Backend
participant Cognito as AWS Cognito
SDK->>Backend: POST /auth/exchange (Bearer marketplace-jwt)
Backend->>Backend: Validate JWT (signature, iss, applicationId, exp)
Backend->>Cognito: ListUsers (filter: username = "gw-{userId}")
Note right of Backend: Signed with AWS SigV4
alt User exists
Cognito-->>Backend: User record
Backend->>Cognito: AdminUpdateUserAttributes
else User not found
Backend->>Cognito: AdminCreateUser (SUPPRESS message)
Cognito-->>Backend: Created user
Backend->>Cognito: AdminSetUserPassword (permanent)
Backend->>Cognito: AdminAddUserToGroup (gw-user)
end
Backend->>Cognito: AdminInitiateAuth (ADMIN_USER_PASSWORD_AUTH)
Cognito-->>Backend: AccessToken + RefreshToken + IdToken
Backend-->>SDK: Tokens + user info
Backend Implementation
import { createHash, createHmac } from 'crypto';
import {
CognitoIdentityProviderClient,
AdminCreateUserCommand,
AdminSetUserPasswordCommand,
AdminInitiateAuthCommand,
AdminUpdateUserAttributesCommand,
AdminAddUserToGroupCommand,
ListUsersCommand,
RevokeTokenCommand,
} from '@aws-sdk/client-cognito-identity-provider';
const client = new CognitoIdentityProviderClient({
region: process.env.COGNITO_REGION,
});
const USER_POOL_ID = process.env.COGNITO_USER_POOL_ID!;
const CLIENT_ID = process.env.COGNITO_CLIENT_ID!;
const CLIENT_SECRET = process.env.COGNITO_CLIENT_SECRET!;
// Deterministic password derivation
function derivedPassword(userId: string): string {
const hash = createHash('sha256')
.update(`${userId}${CLIENT_SECRET}`)
.digest('hex');
return `Gw!1${hash.slice(0, 28)}`;
}
// Compute SECRET_HASH required when app client has a secret
function computeSecretHash(username: string): string {
return createHmac('sha256', CLIENT_SECRET)
.update(username + CLIENT_ID)
.digest('base64');
}
// Find user by username
async function findUser(gwUserId: string) {
const username = `gw-${gwUserId}`;
const result = await client.send(new ListUsersCommand({
UserPoolId: USER_POOL_ID,
Filter: `username = "${username}"`,
Limit: 1,
}));
return result.Users && result.Users.length > 0 ? result.Users[0] : null;
}
// Create user with custom attributes
async function createUser(claims: any) {
const username = `gw-${claims.userId}`;
const password = derivedPassword(claims.userId);
// Create with temporary password (SUPPRESS welcome email)
await client.send(new AdminCreateUserCommand({
UserPoolId: USER_POOL_ID,
Username: username,
TemporaryPassword: password,
MessageAction: 'SUPPRESS',
UserAttributes: [
{ Name: 'email', Value: claims.email ?? `${claims.userId}@gw-marketplace.local` },
{ Name: 'email_verified', Value: 'true' },
{ Name: 'custom:gwUserId', Value: claims.userId },
{ Name: 'custom:orgId', Value: claims.orgId },
{ Name: 'custom:gwApplicationId', Value: claims.applicationId },
],
}));
// Set permanent password (skip FORCE_CHANGE_PASSWORD state)
await client.send(new AdminSetUserPasswordCommand({
UserPoolId: USER_POOL_ID,
Username: username,
Password: password,
Permanent: true,
}));
return username;
}
// Update custom attributes
async function updateUserAttributes(username: string, claims: any) {
await client.send(new AdminUpdateUserAttributesCommand({
UserPoolId: USER_POOL_ID,
Username: username,
UserAttributes: [
{ Name: 'custom:orgId', Value: claims.orgId },
{ Name: 'custom:gwApplicationId', Value: claims.applicationId },
],
}));
}
// Add user to a group
async function addToGroup(username: string, groupName: string) {
await client.send(new AdminAddUserToGroupCommand({
UserPoolId: USER_POOL_ID,
Username: username,
GroupName: groupName,
}));
}
// Authenticate via Admin auth flow
async function adminAuth(username: string, password: string) {
const result = await client.send(new AdminInitiateAuthCommand({
UserPoolId: USER_POOL_ID,
ClientId: CLIENT_ID,
AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
AuthParameters: {
USERNAME: username,
PASSWORD: password,
SECRET_HASH: computeSecretHash(username),
},
}));
return result.AuthenticationResult;
}
SECRET_HASH Computation
When your app client has a client secret enabled, every authentication request must include a SECRET_HASH. This is an HMAC-SHA256 of username + clientId using the client secret as the key:
import { createHmac } from 'crypto';
function computeSecretHash(username: string): string {
return createHmac('sha256', CLIENT_SECRET)
.update(username + CLIENT_ID)
.digest('base64');
}
The SECRET_HASH must be included in:
AdminInitiateAuth(inAuthParameters)InitiateAuth(inAuthParameters)ConfirmSignUp,ForgotPassword, and other client flows
Token Structure
Cognito access tokens contain groups under cognito:groups:
{
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_aBcDeFgHi",
"exp": 1714000000,
"sub": "uuid-of-user",
"cognito:username": "gw-user-abc123",
"cognito:groups": ["gw-user", "org-admin"],
"custom:gwUserId": "user-abc123",
"custom:orgId": "org-456",
"custom:gwApplicationId": "app-789",
"email": "user@example.com"
}
Decode token claims:
function decodeAccessToken(accessToken: string) {
const payload = JSON.parse(
Buffer.from(accessToken.split('.')[1], 'base64url').toString()
);
return {
email: payload.email ?? null,
username: payload['cognito:username'] ?? payload.sub ?? null,
roles: payload['cognito:groups'] ?? [],
orgId: payload['custom:orgId'] ?? null,
gwUserId: payload['custom:gwUserId'] ?? null,
gwApplicationId: payload['custom:gwApplicationId'] ?? null,
};
}
Session Revocation
Revoke refresh tokens using the RevokeToken API:
async function revokeSession(refreshToken: string) {
// RevokeToken does not require SigV4 -- uses client credentials
const endpoint = `https://cognito-idp.${process.env.COGNITO_REGION}.amazonaws.com`;
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-amz-json-1.1',
'X-Amz-Target': 'AWSCognitoIdentityProviderService.RevokeToken',
},
body: JSON.stringify({
Token: refreshToken,
ClientId: CLIENT_ID,
ClientSecret: CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Revoke failed: ${res.status}`);
}
For immediate global sign-out (invalidates all tokens):
import { AdminUserGlobalSignOutCommand } from '@aws-sdk/client-cognito-identity-provider';
async function globalSignOut(username: string) {
await client.send(new AdminUserGlobalSignOutCommand({
UserPoolId: USER_POOL_ID,
Username: username,
}));
}
Admin API Reference
All admin operations require AWS SigV4-signed requests. Use the @aws-sdk/client-cognito-identity-provider package.
| Operation | Command | Purpose |
|---|---|---|
| Create User | AdminCreateUserCommand | Create with temp password |
| Set Password | AdminSetUserPasswordCommand | Set permanent password |
| Authenticate | AdminInitiateAuthCommand | ADMIN_USER_PASSWORD_AUTH flow |
| Get User | AdminGetUserCommand | Retrieve user details |
| Update Attributes | AdminUpdateUserAttributesCommand | Modify custom attributes |
| Add to Group | AdminAddUserToGroupCommand | Assign group membership |
| List Users | ListUsersCommand | Search by filter |
| Revoke Token | RevokeTokenCommand | Invalidate refresh token |
| Global Sign Out | AdminUserGlobalSignOutCommand | Invalidate all tokens |
Filter Syntax
Cognito uses a simplified filter syntax (not OData):
# Filter by username (exact match)
username = "gw-user-abc123"
# Filter by email
email = "user@example.com"
# Filter by custom attribute
"custom:gwUserId" = "user-abc123"
# Filter by status
status = "Enabled"
User Lifecycle: FORCE_CHANGE_PASSWORD
When you create a user with AdminCreateUser, Cognito puts them in FORCE_CHANGE_PASSWORD state. The two-step pattern resolves this:
// Step 1: Create user with temporary password
await client.send(new AdminCreateUserCommand({
UserPoolId: USER_POOL_ID,
Username: username,
TemporaryPassword: password,
MessageAction: 'SUPPRESS', // Don't send welcome email
UserAttributes: [...],
}));
// Step 2: Immediately set permanent password
await client.send(new AdminSetUserPasswordCommand({
UserPoolId: USER_POOL_ID,
Username: username,
Password: password,
Permanent: true, // Transitions from FORCE_CHANGE_PASSWORD to CONFIRMED
}));
Without step 2, the AdminInitiateAuth call will return a NEW_PASSWORD_REQUIRED challenge instead of tokens.
Common Gotchas
-
FORCE_CHANGE_PASSWORD state:
AdminCreateUseralways puts users in this state. You must callAdminSetUserPasswordwithPermanent: truebefore authenticating. -
SECRET_HASH required: If your app client has a secret, every auth call needs the HMAC-SHA256 hash. Missing this returns
NotAuthorizedException: Unable to verify secret hash. -
Custom attributes must be pre-defined: Custom attributes (
custom:gwUserId, etc.) must be defined in the user pool schema at creation time. They cannot be added after the pool is created. -
Custom attribute prefix: All custom attributes are accessed with the
custom:prefix in API calls and token claims. Do not omit the prefix. -
IAM permissions scope: Scope IAM permissions to the specific user pool ARN. Overly broad permissions (e.g.,
cognito-idp:*on*) are a security risk. -
Region-specific endpoints: Cognito endpoints are region-specific. Ensure
COGNITO_REGIONmatches the region where your user pool was created. -
Token expiration: Access tokens default to 1 hour, refresh tokens to 30 days. Configure these in App client settings > Token expiration.
-
ListUsers filter limitations: Cognito's filter syntax is limited. You cannot use
AND/ORoperators or filter by multiple attributes simultaneously. Design your username scheme (gw-{userId}) for efficient lookups.