Skip to main content

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_AUTH flow enabled and a client secret
  • User Pool Groups: gw-user and org-admin
  • IAM Policy granting Cognito admin operations on the user pool

AWS Setup

  1. Create a User Pool (Cognito > User Pools > Create):

    Pool Name: gw-marketplace-users
    Attributes:
    - custom:gwUserId (String, mutable)
    - custom:orgId (String, mutable)
    - custom:gwApplicationId (String, mutable)
    Password Policy: Min 8 chars (relax for programmatic users)
  2. Create an App Client (App integration > App clients > Create):

    Client Name: gw-marketplace-client
    Generate secret: Yes
    Auth Flows: ALLOW_ADMIN_USER_PASSWORD_AUTH
  3. Create Groups (Groups > Create group):

    • gw-user -- All marketplace users
    • org-admin -- Organization administrators
  4. 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 (in AuthParameters)
  • InitiateAuth (in AuthParameters)
  • 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.

OperationCommandPurpose
Create UserAdminCreateUserCommandCreate with temp password
Set PasswordAdminSetUserPasswordCommandSet permanent password
AuthenticateAdminInitiateAuthCommandADMIN_USER_PASSWORD_AUTH flow
Get UserAdminGetUserCommandRetrieve user details
Update AttributesAdminUpdateUserAttributesCommandModify custom attributes
Add to GroupAdminAddUserToGroupCommandAssign group membership
List UsersListUsersCommandSearch by filter
Revoke TokenRevokeTokenCommandInvalidate refresh token
Global Sign OutAdminUserGlobalSignOutCommandInvalidate 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

  1. FORCE_CHANGE_PASSWORD state: AdminCreateUser always puts users in this state. You must call AdminSetUserPassword with Permanent: true before authenticating.

  2. 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.

  3. 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.

  4. Custom attribute prefix: All custom attributes are accessed with the custom: prefix in API calls and token claims. Do not omit the prefix.

  5. IAM permissions scope: Scope IAM permissions to the specific user pool ARN. Overly broad permissions (e.g., cognito-idp:* on *) are a security risk.

  6. Region-specific endpoints: Cognito endpoints are region-specific. Ensure COGNITO_REGION matches the region where your user pool was created.

  7. Token expiration: Access tokens default to 1 hour, refresh tokens to 30 days. Configure these in App client settings > Token expiration.

  8. ListUsers filter limitations: Cognito's filter syntax is limited. You cannot use AND/OR operators or filter by multiple attributes simultaneously. Design your username scheme (gw-{userId}) for efficient lookups.

Additional Resources