workos-migrate-aws-cognito
WorkOS Migration: AWS Cognito
Step 1: Fetch Documentation (BLOCKING)
STOP. Do not proceed until complete.
WebFetch: https://workos.com/docs/migrate/aws-cognito
The migration guide is the source of truth. If this skill conflicts with the guide, follow the guide.
Step 2: Pre-Migration Assessment
User Data Inventory
Create a spreadsheet or document listing:
-
Authentication methods in use:
- Username/password users (count)
- Social OAuth providers (Google, Facebook, etc.) — list each
- SAML SSO connections (if any)
- MFA-enabled users (count)
-
User attributes to migrate:
- Required: email, email_verified status
- Optional: name, profile data, custom attributes
- Groups/roles (map to WorkOS Organizations)
-
Password hash accessibility:
- CRITICAL: AWS Cognito does NOT export password hashes for security reasons
- Users will need to reset passwords after migration
- Plan user communication strategy (see Step 6)
Cognito Export Limitations
Can you export password hashes from Cognito?
|
+-- NO --> Users MUST reset passwords
(Cognito security policy)
Can you export user attributes?
|
+-- YES --> Use AWS CLI: aws cognito-idp list-users
|
+-- Programmatic --> Use AWS SDK for bulk export
Important: WorkOS CAN import password hashes from other systems (bcrypt, scrypt, etc.), but AWS Cognito does not provide this data during export. This is a Cognito limitation, not a WorkOS limitation.
Step 3: Export Users from Cognito
Option A: AWS CLI (Small user bases <1000)
# List all users in a user pool
aws cognito-idp list-users \
--user-pool-id us-east-1_XXXXXX \
--region us-east-1 \
> cognito_users.json
# Verify export
jq 'length' cognito_users.json
Option B: Programmatic Export (Large user bases)
Create export script using AWS SDK:
// export-cognito-users.js
const { CognitoIdentityProviderClient, ListUsersCommand } = require('@aws-sdk/client-cognito-identity-provider');
const client = new CognitoIdentityProviderClient({ region: 'us-east-1' });
const userPoolId = 'us-east-1_XXXXXX';
async function exportUsers() {
let users = [];
let paginationToken = null;
do {
const command = new ListUsersCommand({
UserPoolId: userPoolId,
PaginationToken: paginationToken,
Limit: 60 // Max per page
});
const response = await client.send(command);
users = users.concat(response.Users);
paginationToken = response.PaginationToken;
} while (paginationToken);
return users;
}
Verify: Exported user count matches Cognito dashboard count.
Step 4: Transform User Data for WorkOS
Map Cognito user attributes to WorkOS format:
// transform-users.js
function transformCognitoUser(cognitoUser) {
const email = cognitoUser.Attributes.find(a => a.Name === 'email')?.Value;
const emailVerified = cognitoUser.Attributes.find(a => a.Name === 'email_verified')?.Value === 'true';
const firstName = cognitoUser.Attributes.find(a => a.Name === 'given_name')?.Value;
const lastName = cognitoUser.Attributes.find(a => a.Name === 'family_name')?.Value;
return {
email,
email_verified: emailVerified,
first_name: firstName,
last_name: lastName,
// WorkOS will generate new password - users must reset
};
}
Critical mapping:
email(required) → WorkOSemailemail_verified→ WorkOSemail_verified(bool)given_name,family_name→ WorkOSfirst_name,last_name- Password hashes: NOT available from Cognito export
Step 5: Import Users to WorkOS
Create Users via API
Use WorkOS User Management API to create users:
// import-to-workos.js
const { WorkOS } = require('@workos-inc/node');
const workos = new WorkOS(process.env.WORKOS_API_KEY);
async function importUser(userData) {
try {
const user = await workos.userManagement.createUser({
email: userData.email,
emailVerified: userData.email_verified,
firstName: userData.first_name,
lastName: userData.last_name,
});
console.log(`Imported: ${user.email}`);
return user;
} catch (error) {
console.error(`Failed to import ${userData.email}:`, error.message);
return null;
}
}
Rate limiting: WorkOS API has rate limits. Add delays between requests:
// Batch import with rate limiting
async function batchImport(users, delayMs = 100) {
for (const user of users) {
await importUser(user);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
Verify: Check WorkOS Dashboard user count matches import count.
Step 6: Password Reset Strategy (REQUIRED)
Since Cognito does not export password hashes, choose a reset strategy:
Password Reset Strategy?
|
+-- Immediate (Proactive)
| |
| +-> Send password reset emails to ALL users
| +-> Use WorkOS Send Password Reset Email API
| +-> Users receive email before attempting login
|
+-- On-Demand (Reactive)
|
+-> User attempts login with old password
+-> Login fails (expected)
+-> Show "Reset your password" message
+-> Trigger password reset flow
Proactive Password Reset (Recommended)
Send reset emails immediately after import:
// send-reset-emails.js
async function sendPasswordResetEmail(email) {
try {
await workos.userManagement.sendPasswordResetEmail({
email,
passwordResetUrl: 'https://yourapp.com/reset-password', // Your callback URL
});
console.log(`Sent password reset to: ${email}`);
} catch (error) {
console.error(`Failed to send reset email to ${email}:`, error.message);
}
}
// Send to all imported users
for (const user of importedUsers) {
await sendPasswordResetEmail(user.email);
await new Promise(resolve => setTimeout(resolve, 100)); // Rate limit
}
User Communication Template:
Subject: Action Required: Reset Your Password
We've upgraded our authentication system to improve security.
Please reset your password by clicking the link below:
[Reset Password Link]
This is a one-time setup. After resetting, you can sign in normally.
Reactive Password Reset
In your login error handler:
// login-handler.js
try {
const { user } = await workos.userManagement.authenticateWithPassword({
email,
password,
clientId: process.env.WORKOS_CLIENT_ID,
});
} catch (error) {
if (error.code === 'invalid_credentials') {
// Show user-friendly message
return {
error: 'Please reset your password',
resetUrl: await generatePasswordResetUrl(email),
};
}
}
Step 7: Migrate OAuth Social Logins (If Applicable)
Reuse Existing OAuth Credentials
If you have Google, Facebook, or other OAuth providers configured in Cognito:
-
Copy OAuth credentials:
- Client ID from Cognito
- Client Secret from Cognito
-
Configure in WorkOS Dashboard:
- Navigate to Authentication → Social Connections
- Add connection (e.g., Google OAuth)
- Use SAME Client ID and Client Secret
-
Add WorkOS Redirect URI to OAuth provider:
https://api.workos.com/sso/oauth/google/callbackFor Google specifically, see Google OAuth integration guide for detailed steps.
Critical: Using the same OAuth credentials means users' existing social login connections remain valid — no re-authorization needed.
Verification for Social Logins
Test each provider:
# Attempt social login with migrated user
# Should succeed without re-authorization if credentials match
Step 8: Migrate Organizations and Group Memberships
If using Cognito Groups for access control:
// Map Cognito Groups to WorkOS Organizations
const groupToOrgMapping = {
'cognito-group-admins': 'org_admin_team',
'cognito-group-users': 'org_general_users',
};
async function migrateGroupMembership(cognitoUser, workosUser) {
const groups = cognitoUser.Groups || [];
for (const group of groups) {
const orgId = groupToOrgMapping[group.GroupName];
if (orgId) {
await workos.organizations.createOrganizationMembership({
organizationId: orgId,
userId: workosUser.id,
});
}
}
}
Step 9: Update Application Code
Replace Cognito SDK Calls
Old Cognito Pattern --> New WorkOS Pattern
------------------------- ---------------------
CognitoUser.authenticateUser() --> workos.userManagement.authenticateWithPassword()
CognitoUser.signOut() --> workos.userManagement.revokeSession()
CognitoUser.getSession() --> workos.userManagement.getUser() / withAuth()
Environment Variables
Update your .env file:
# Remove Cognito vars
# AWS_COGNITO_USER_POOL_ID=...
# AWS_COGNITO_CLIENT_ID=...
# Add WorkOS vars
WORKOS_API_KEY=sk_live_...
WORKOS_CLIENT_ID=client_...
NEXT_PUBLIC_WORKOS_REDIRECT_URI=https://yourapp.com/auth/callback
Framework-Specific Integration
After user migration, integrate WorkOS AuthKit for your framework:
- Next.js: Reference skill
workos-authkit-nextjs - React (SPA): Reference skill
workos-authkit-react - Vanilla JS: Reference skill
workos-authkit-vanilla-js
Verification Checklist (ALL MUST PASS)
Run these checks to confirm successful migration:
# 1. Verify user count matches
# Cognito count
aws cognito-idp list-users --user-pool-id us-east-1_XXXXX | jq '.Users | length'
# WorkOS count (check Dashboard or API)
curl -X GET https://api.workos.com/user_management/users \
-H "Authorization: Bearer $WORKOS_API_KEY" | jq '.data | length'
# 2. Test password reset flow
# Attempt login with old password (should fail)
# Request password reset (should receive email)
# Complete reset (should succeed)
# 3. Test social login (if applicable)
# Login with Google/Facebook (should work without re-auth)
# 4. Verify organization memberships migrated
# Check user has correct roles in WorkOS Dashboard
# 5. Test authentication in application
npm run build && npm start
# Attempt login with NEW password (should succeed)
If check #1 fails: Review import logs for errors. Check API rate limiting.
If check #2 fails: Verify passwordResetUrl in API call matches your app's reset route.
If check #3 fails: Confirm OAuth credentials match exactly between Cognito and WorkOS. Check redirect URI is whitelisted.
If check #4 fails: Review group-to-organization mapping logic.
If check #5 fails: Check environment variables are loaded. Verify SDK method signatures.
Error Recovery
"Email already exists" during import
Root cause: User was already imported, or email conflicts with existing WorkOS user.
Fix:
// Add duplicate check
const existingUser = await workos.userManagement.getUser({ email });
if (existingUser) {
console.log(`Skipping duplicate: ${email}`);
return existingUser;
}
"Invalid email format"
Root cause: Cognito allowed emails that WorkOS validation rejects (e.g., missing TLD).
Fix:
// Validate before import
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
if (!isValidEmail(userData.email)) {
console.error(`Invalid email format: ${userData.email}`);
return null; // Skip or flag for manual review
}
"Rate limit exceeded"
Root cause: Importing too many users too quickly.
Fix:
// Increase delay between requests
const delayMs = 200; // Was 100, now 200
await new Promise(resolve => setTimeout(resolve, delayMs));
Password reset emails not received
Root cause: Email provider blocking, incorrect passwordResetUrl, or email not verified in WorkOS.
Fix:
- Check spam folder
- Verify
passwordResetUrlis publicly accessible - Confirm email is marked
emailVerified: trueduring import - Check WorkOS Dashboard → Settings → Email provider configuration
Social login fails with "invalid_client"
Root cause: OAuth credentials don't match, or redirect URI not whitelisted.
Fix:
- Copy exact Client ID and Secret from Cognito
- Add WorkOS callback URL to OAuth provider's allowed redirect URIs:
https://api.workos.com/sso/oauth/{provider}/callback - Wait 5-10 minutes for OAuth provider to propagate changes
"User not found" after migration
Root cause: User ID references in your database still point to Cognito IDs.
Fix:
// Maintain ID mapping during migration
const idMapping = {};
async function importWithMapping(cognitoUser) {
const workosUser = await importUser(cognitoUser);
idMapping[cognitoUser.Username] = workosUser.id;
// Update your database
await db.users.update({
where: { cognitoId: cognitoUser.Username },
data: { workosId: workosUser.id },
});
}
Post-Migration Cleanup
After confirming successful migration (1-2 weeks):
- Disable Cognito User Pool (do not delete yet)
- Remove Cognito SDK from package.json
- Archive Cognito export data (for compliance/audit)
- Update monitoring/logging to track WorkOS auth metrics
Do NOT delete Cognito User Pool immediately — keep as backup for 30-90 days in case of rollback need.
Related Skills
workos-authkit-nextjs- Integrate WorkOS AuthKit with Next.js after migrationworkos-authkit-react- Integrate WorkOS AuthKit with React SPA after migrationworkos-authkit-vanilla-js- Integrate WorkOS AuthKit with vanilla JavaScriptworkos-api-authkit- Low-level AuthKit API reference for custom implementationsworkos-mfa- Add multi-factor authentication after migrationworkos-sso- Add enterprise SSO after migrationworkos-admin-portal- Enable self-service admin portal for migrated organizations
More from workos/skills
workos
Use when the user asks for a WorkOS docs URL, term, or dashboard field (Sign-in endpoint, initiate_login_uri, Redirect URI, `WORKOS_*` env vars), or is implementing, debugging, or migrating WorkOS — AuthKit, SSO/SAML, Directory Sync, RBAC, FGA, MFA, Vault, Audit Logs, Admin Portal, Pipes (Connected Apps), Feature Flags, Radar (bot/fraud detection), webhooks, Custom Domains, or migrating from Auth0, Clerk, Cognito, Firebase, Supabase, Stytch, Descope, or Better Auth. Also triggers on @workos-inc/* imports.
606workos-widgets
Use when the user is implementing, embedding, or debugging a WorkOS Widget — specifically the User Management, User Profile, Admin Portal SSO Connection, or Admin Portal Domain Verification widgets. Handles the full stack — detecting the frontend (Next.js, React, React Router, TanStack Start, Vite, SvelteKit), generating access tokens via the backend SDK in use (Node, Python, Go, Ruby, PHP, Java, .NET), and wiring up the widget component correctly per the bundled OpenAPI spec. Also use when code imports from @workos-inc/widgets or the user pastes <UserManagement /> or <UserProfile /> JSX.
278workos-authkit-nextjs
Integrate WorkOS AuthKit with Next.js App Router (13+). Server-side rendering required.
64workos-authkit-base
Architectural reference for WorkOS AuthKit integrations. Fetch README first for implementation details.
42workos-authkit-react
Integrate WorkOS AuthKit with React single-page applications. Client-side only authentication. Use when the project is a React SPA without Next.js or React Router.
33workos-authkit-tanstack-start
Integrate WorkOS AuthKit with TanStack Start applications. Full-stack TypeScript with server functions. Use when project uses TanStack Start, @tanstack/start, or vinxi.
28