Cognito AdminCreateUser With AWS SDK v3: Create Users From Code

A row of brass keys hanging on hooks against a dark wooden wall

Photo by Silas Köhler on Unsplash

To call Cognito AdminCreateUser with AWS SDK v3, send an AdminCreateUserCommand from @aws-sdk/client-cognito-identity-provider with the UserPoolId, a Username and UserAttributes such as email. Cognito emails a temporary password and the user starts in FORCE_CHANGE_PASSWORD. Add MessageAction: "SUPPRESS" to send nothing, then AdminSetUserPassword with Permanent: true.

You create Cognito users from code when self sign-up is off: an admin panel that invites staff, a migration from another identity store, a B2B app where each customer’s admin adds their team, or test users for a staging pool. The admin API does all of that, but the defaults surprise people: an invitation goes out unless you say otherwise, a re-run fails on the first existing user, and the user can’t sign in until they change a password they may never have received.

This guide is for Node.js and TypeScript developers. You’ll get a module that wraps Cognito AdminCreateUser with AWS SDK v3 for both the invite and the silent flow, sets permanent passwords, adds users to groups and can be re-run safely, plus the IAM policy and the errors you’ll meet.

What does AdminCreateUser do?

It creates a user in a user pool as an administrator, authorized with IAM credentials rather than a user’s token. What happens next depends on two parameters:

Flow Parameters Result
Invite No MessageAction, DesiredDeliveryMediums: ["EMAIL"] Cognito generates a temporary password and emails it using the pool’s invitation template. Status FORCE_CHANGE_PASSWORD until the user signs in and sets a new one
Silent MessageAction: "SUPPRESS", plus AdminSetUserPassword with Permanent: true No message. The user can sign in immediately with the password you set; status CONFIRMED
Resend MessageAction: "RESEND" for an existing user Sends a new invitation with a new temporary password and resets its expiry
Passwordless No TemporaryPassword, pool with email or SMS one-time codes enabled A user with no password who signs in with codes

A temporary password is valid for the pool’s TemporaryPasswordValidityDays, 7 days by default. After that, the user can’t sign in until you resend the invitation or set a password. Note the default for DesiredDeliveryMediums is SMS, so set EMAIL explicitly for email invites.

Prerequisites

How to call Cognito AdminCreateUser with AWS SDK v3, step by step

  1. Create the client in the pool’s RegionThe Region is the prefix of the pool ID. A client in the wrong Region returns ResourceNotFoundException.
  2. Check how users sign inIf the pool signs users in with email only, pass the email as Username; Cognito stores a generated username instead, returned in the response. Later admin calls accept the email as an alias.
  3. Set attributesemail is required for email invites. Set email_verified to "true" only if you’ve verified the address yourself; otherwise the user can’t use it to reset a forgotten password until it’s verified. Custom attributes need the custom: prefix.
  4. Choose invite or silentOmit MessageAction to send the invitation, or set SUPPRESS.
  5. Set a permanent password in the silent flowAdminSetUserPassword with Permanent: true moves the user to CONFIRMED.
  6. Add groupsAdminAddUserToGroup per group. Group membership appears in the cognito:groups claim of the user’s tokens.
  7. Make it re-runnableCatch UsernameExistsException and read the user with AdminGetUser instead of failing.

Example: a re-runnable user creation module

cognito-users.ts

// cognito-users.ts
// Create Cognito user pool users from code with AWS SDK for JavaScript v3:
// AdminCreateUser (invite or silent), AdminSetUserPassword (permanent), AdminAddUserToGroup,
// and an idempotent wrapper that treats UsernameExistsException as "already there".
import { randomBytes } from "node:crypto";
import {
  AdminAddUserToGroupCommand,
  AdminCreateUserCommand,
  AdminGetUserCommand,
  AdminSetUserPasswordCommand,
  CognitoIdentityProviderClient,
  UsernameExistsException,
  type AttributeType,
} from "@aws-sdk/client-cognito-identity-provider";

const cognito = new CognitoIdentityProviderClient({}); // Region must match the user pool's Region

export interface NewUser {
  email: string;
  name?: string;
  groups?: string[];
  /** "invite": Cognito emails a temporary password. "silent": no message; you set the password. */
  mode: "invite" | "silent";
  password?: string; // required for "silent"
}

export interface CreatedUser {
  username: string; // the value Cognito stored; a generated ID in email-only sign-in pools
  sub: string;
  status: string;
  created: boolean;
}

const attr = (Name: string, Value: string): AttributeType => ({ Name, Value });

/** A temporary password: 20 random characters plus one of each class most pool policies require. */
export function temporaryPassword(): string {
  return `${randomBytes(15).toString("base64url")}Aa1!`;
}

export async function createUser(userPoolId: string, u: NewUser): Promise<CreatedUser> {
  const attributes = [attr("email", u.email), attr("email_verified", "true")];
  if (u.name) attributes.push(attr("name", u.name));

  let created = true;
  let username = u.email;
  let sub = "";
  let status = "";
  try {
    const { User } = await cognito.send(
      new AdminCreateUserCommand({
        UserPoolId: userPoolId,
        Username: u.email,
        UserAttributes: attributes,
        DesiredDeliveryMediums: ["EMAIL"],
        // Omit MessageAction to send the invitation; SUPPRESS sends nothing.
        MessageAction: u.mode === "silent" ? "SUPPRESS" : undefined,
        TemporaryPassword: u.mode === "silent" ? temporaryPassword() : undefined,
      }),
    );
    username = User?.Username ?? u.email;
    sub = User?.Attributes?.find((a) => a.Name === "sub")?.Value ?? "";
    status = User?.UserStatus ?? "";
  } catch (err) {
    if (!(err instanceof UsernameExistsException)) throw err;
    // Already created by an earlier run: read it instead of failing, so the script can be re-run.
    created = false;
    const existing = await cognito.send(new AdminGetUserCommand({ UserPoolId: userPoolId, Username: u.email }));
    username = existing.Username ?? u.email;
    sub = existing.UserAttributes?.find((a) => a.Name === "sub")?.Value ?? "";
    status = existing.UserStatus ?? "";
  }

  // Silent mode: replace the throwaway temporary password with a permanent one, so status becomes CONFIRMED.
  if (u.mode === "silent" && created) {
    if (!u.password) throw new Error(`silent mode needs a password for ${u.email}`);
    await cognito.send(
      new AdminSetUserPasswordCommand({ UserPoolId: userPoolId, Username: username, Password: u.password, Permanent: true }),
    );
    status = "CONFIRMED";
  }

  for (const group of u.groups ?? []) {
    // The group must already exist (CreateGroup); a missing group throws ResourceNotFoundException.
    await cognito.send(new AdminAddUserToGroupCommand({ UserPoolId: userPoolId, Username: username, GroupName: group }));
  }
  return { username, sub, status, created };
}

A small import script that reads users from a JSON file:

create-users.ts

// create-users.ts
// Usage: AWS_PROFILE=admin AWS_REGION=eu-west-1 npx tsx create-users.ts eu-west-1_AbCdEf123 users.json
// users.json: [{ "email": "[email protected]", "name": "Ana", "groups": ["editors"], "mode": "invite" }]
import { readFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
import { createUser, type NewUser } from "./cognito-users.js";

const [userPoolId, file] = process.argv.slice(2);
if (!userPoolId || !file) throw new Error("usage: create-users.ts <userPoolId> <users.json>");
const users = JSON.parse(readFileSync(file, "utf8")) as NewUser[];

let failures = 0;
for (const u of users) {
  try {
    const r = await createUser(userPoolId, u);
    console.log(`${r.created ? "created" : "exists "} ${u.email} -> ${r.username} ${r.status} groups=${(u.groups ?? []).join(",") || "-"}`);
  } catch (err) {
    failures++;
    console.error(`failed  ${u.email}: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
  }
  await sleep(100); // stay well under the user-creation request rate for large imports
}
if (failures) process.exit(1);
users.json

[
  { "email": "[email protected]", "name": "Ana", "groups": ["editors"], "mode": "invite" },
  { "email": "[email protected]", "name": "CI bot", "groups": ["readers"], "mode": "silent", "password": "load-this-from-secrets-manager" }
]
Terminal

npm install @aws-sdk/client-cognito-identity-provider
npm install --save-dev tsx typescript @types/node
npm pkg set type=module

AWS_PROFILE=admin AWS_REGION=eu-west-1 npx tsx create-users.ts eu-west-1_AbCdEf123 users.json
# created [email protected] -> 3264f4a8-7011-70c6-9e1a-5a8f2c0d4b11 FORCE_CHANGE_PASSWORD groups=editors
# created [email protected] -> 7274b4c8-e0a1-7055-2b3c-9d8e7f6a5b40 CONFIRMED groups=readers

# Run it again: nothing is duplicated
# exists  [email protected] -> 3264f4a8-7011-70c6-9e1a-5a8f2c0d4b11 FORCE_CHANGE_PASSWORD groups=editors
# exists  [email protected] -> 7274b4c8-e0a1-7055-2b3c-9d8e7f6a5b40 CONFIRMED groups=readers

IDs are illustrative; this pool signs users in with email, so the stored usernames are generated. Don’t keep real passwords in a JSON file: load them at runtime with the guide to get a Secrets Manager secret value with SDK v3, and never put them in function configuration, which the script to find secrets in Lambda environment variables catches.

Invite or silent: which flow should you use?

Use the invite flow for people. The user proves they control the mailbox by using the temporary password, and you never handle their real password. Customize the invitation template in the pool, and keep the {####} placeholder: without it, Cognito doesn’t deliver the invitation at all. For production volumes, configure the pool to send through Amazon SES rather than the default Cognito sender, which has a small daily quota; the guide to send email with Amazon SES and SDK v3 covers verified identities and the sandbox.

Use the silent flow for service accounts, load tests and migrations where you already hold a password that meets the pool’s policy. Treat those passwords like any other credential: long, random, stored in a secrets manager and rotated. The OWASP Authentication Cheat Sheet recommends favoring length over composition rules and allowing passwords of at least 64 characters, which is a good baseline when you set the pool’s password policy.

After creation, store the user’s sub as the key in your own database, not the email: the sub never changes, while emails do. The guide to DynamoDB UpdateItem with AWS SDK v3 shows a conditional upsert that suits a profile table keyed by sub.

Which IAM permissions does creating users need?

cognito-admin-create-user-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CreateUsersInOnePool",
      "Effect": "Allow",
      "Action": [
        "cognito-idp:AdminCreateUser",
        "cognito-idp:AdminGetUser",
        "cognito-idp:AdminSetUserPassword",
        "cognito-idp:AdminAddUserToGroup"
      ],
      "Resource": "arn:aws:cognito-idp:eu-west-1:123456789012:userpool/eu-west-1_AbCdEf123"
    }
  ]
}

Drop AdminSetUserPassword if you only use the invite flow; it lets the caller take over any account in the pool. AdminAddUserToGroup is also sensitive when groups map to IAM roles or admin rights in your app. The guide to review a generated IAM policy for least privilege explains how to split these across roles, and the free IAM policy generator for TypeScript derives the list from cognito-users.ts. To double-check actions by hand, see how to find the IAM actions your AWS SDK for JavaScript code needs.

Troubleshooting AdminCreateUser errors

  • UsernameExistsException. The user already exists. The module reads it instead; to send a fresh invitation, call again with MessageAction: "RESEND".
  • AliasExistsException. You set email_verified to true and another user already has that verified email. Fix the duplicate, or set ForceAliasCreation to move the alias, which locks the other user out of signing in with it.
  • InvalidPasswordException. The temporary or permanent password doesn’t meet the pool’s policy. Read the policy with DescribeUserPool.
  • InvalidParameterException. A required attribute is missing, a custom attribute lacks custom:, or the username format doesn’t match the pool’s sign-in settings.
  • CodeDeliveryFailureException, or no email arrives. Check the pool’s email configuration and SES sending status, and that the invitation template contains {####}.
  • TooManyRequestsException. User creation has its own request-rate quota. The SDK retries throttling errors, and the guide to configure retries and timeouts in AWS SDK v3 shows how to allow more attempts; for large imports, slow down or request an increase.
  • AccessDeniedException or NotAuthorizedException. Missing cognito-idp permissions, or a policy scoped to another pool ARN. The steps to troubleshoot AWS IAM access denied errors decode the first.

Limits and pricing

The module creates users one at a time. For hundreds of thousands of users, a CSV user import job (CreateUserImportJob) is faster, and imported users start in RESET_REQUIRED; for users you can authenticate against an old system, a user migration Lambda trigger moves them on first sign-in. AdminCreateUser can also run your pool’s Lambda triggers, which receive ClientMetadata. Cognito bills user pools by monthly active users, with rates that depend on the pool’s feature plan (Lite, Essentials or Plus); check the Amazon Cognito pricing page before bulk-creating test users in a paid plan.

Moving code from SDK v2? The AWS SDK v2 to v3 code converter drafts adminCreateUser calls as commands, and the guide to migrate a Node.js app from AWS SDK v2 to v3 covers error handling, which changes most. To look up a user pool’s settings without writing code, ChatWithCloud answers questions about your AWS account in plain English from the terminal; how ChatWithCloud works explains what runs locally.

Frequently asked questions

How do I create a Cognito user without sending an email?

Call AdminCreateUser with MessageAction: "SUPPRESS". The user gets no message; set their password with AdminSetUserPassword, using Permanent: true if they should sign in without changing it.

How do I get a Cognito user out of FORCE_CHANGE_PASSWORD?

Either the user signs in with the temporary password and answers the NEW_PASSWORD_REQUIRED challenge, or an administrator calls AdminSetUserPassword with Permanent: true, which sets the status to CONFIRMED.

How long is a Cognito temporary password valid?

For the pool’s TemporaryPasswordValidityDays, which defaults to 7 days. After it expires, call AdminCreateUser again with MessageAction: "RESEND", or set a new password.

Can I call AdminCreateUser from the browser?

No. Admin operations need IAM credentials, so call them from a backend, a Lambda function or a script. Browsers use SignUp with an app client instead.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud