Migrate from AWS Cognito to Ionize
Move your User Pool to Kratos
Cognito has unique features (User Pool, Identity Pool, AWS IAM integration). Migrating to Ionize replaces auth with control over your own data.
What translates
| Cognito | Ionize |
|---|---|
| User Pool | Kratos identities |
| Identity Pool (for AWS IAM) | Custom: Hydra issues token, exchange for IAM credentials |
| App clients | Hydra OAuth2 clients |
| Lambda triggers | Kratos web_hooks |
| User attributes | Kratos traits |
| Groups | Kratos role trait |
User export
Cognito doesn't allow exporting password hashes (sealed). Two migration approaches:
Approach A: JIT migration via Cognito OIDC
Keep Cognito as IdP, federate to Ionize:
- In Cognito, configure your domain as OIDC issuer.
- In Ionize, add Cognito as OIDC provider.
- Users sign in via Cognito → Ionize creates local identity.
- Decommission later.
# kratos.yml
selfservice:
methods:
oidc:
config:
providers:
- id: cognito-legacy
provider: generic
issuer_url: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXX
client_id: ...
client_secret: ...
scope: [openid, email, profile]Approach B: Force password reset
Export user attributes (email, name), Cognito allows via CSV.
aws cognito-idp list-users --user-pool-id us-east-1_XXXXX > users.jsonImport to Kratos with random passwords. Force everyone to reset.
// migrate.js
for (const u of users.Users) {
await fetch(`${KRATOS_ADMIN}/admin/identities`, {
method: "POST",
body: JSON.stringify({
schema_id: "default",
traits: {
email: u.Attributes.find(a => a.Name === "email").Value,
first_name: u.Attributes.find(a => a.Name === "given_name")?.Value,
},
credentials: {
password: { config: { password: crypto.randomUUID() } }
},
state: "active",
}),
});
}Then bulk-send recovery emails.
Cognito-specific gotchas
Federated identities (Google/Facebook)
If users signed up via Cognito's "Sign in with Google":
- Cognito has its own subject (
cognito:username = google_1234567890). - Ionize directly federates with Google.
JIT migration handles this naturally, user signs in with Google → Ionize stores Google's subject directly. No "Cognito intermediary" needed.
MFA
Cognito's SMS-based MFA is least secure. Ionize migration is good time to push users to WebAuthn / TOTP.
Force re-enrollment after migration:
// pre-login hook
if (!hasNonSmsMfa(identity)) {
return Response.json({
redirect_to: "/settings?required=upgrade_mfa",
});
}Custom Cognito attributes
Cognito allows custom attributes like custom:tenant_id. Map to Kratos traits:
"traits": {
"type": "object",
"properties": {
"email": { "type": "string" },
"tenant_id": { "type": "string" }
}
}Cognito groups
Cognito groups → Kratos role traits:
// during import:
const groups = await cognito.adminListGroupsForUser(...);
const primaryGroup = groups[0]?.GroupName ?? "user";
// Map to role
const role = mapping[primaryGroup] ?? "user";AWS IAM credential exchange (Identity Pool)
Cognito Identity Pool lets you exchange a User Pool token for AWS IAM credentials. Ionize doesn't directly replace this.
If you need it:
- Use AWS STS's
AssumeRoleWithWebIdentitywith your Ionize-issued OIDC token. - Configure AWS IAM role trust policy to trust your Ionize issuer.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::ACCOUNT:oidc-provider/ciam.your-domain" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"ciam.your-domain:aud": "your-app-client-id"
}
}
}]
}User flow:
- User signs in with Ionize → ID token.
- App calls AWS STS with ID token → temporary IAM credentials.
- Uses credentials to call AWS APIs directly.
Lambda triggers → Kratos hooks
Cognito has triggers like "PreSignUp," "PostConfirmation," etc.
Map to Kratos hook points:
| Cognito Trigger | Kratos Hook |
|---|---|
| PreSignUp | registration.before |
| PostConfirmation | registration.after |
| PreAuthentication | login.before |
| PostAuthentication | login.after |
| CustomMessage | courier templates |
Rewrite each Lambda as a Kratos webhook handler.
App SDK migration
Cognito uses Amplify SDK (aws-amplify):
import { signIn } from "aws-amplify/auth";
await signIn({ username, password });Ionize is OAuth2 standard:
import { IonizeClient } from "@nannier/sdk";
const client = new IonizeClient({ ... });
await client.signIn(username, password);Or use any OIDC library, Ionize is standard.
Domain migration
If you used Cognito's hosted UI (https://your-pool.auth.us-east-1.amazoncognito.com), users have it bookmarked.
Set up a 301 redirect or maintain that domain pointing at Ionize for a transition period.
Costs
Cognito: ~$0.0055/MAU. At 100k MAU = $550/mo. Ionize self-hosted: ~$80/mo.
Plus Cognito charges for advanced security ($0.05/MAU). Ionize has equivalent free.
Migration ROI: under a year for most.
Compliance
If you used Cognito for SOC 2 compliance evidence, that goes away. You inherit it from your own Ionize deployment, but you need to demonstrate equivalent controls.
See Compliance, SOC 2.