Sign Users In to Cognito With AWS SDK v3 (InitiateAuth)

A small metal padlock resting on the keys of a black laptop keyboard

Photo by Sasun Bughdaryan on Unsplash

Cognito InitiateAuth with AWS SDK v3 starts a sign-in: send InitiateAuthCommand from @aws-sdk/client-cognito-identity-provider with AuthFlow: "USER_PASSWORD_AUTH", your app client ID and USERNAME and PASSWORD (plus SECRET_HASH if the client has a secret). You get tokens back, or a challenge such as NEW_PASSWORD_REQUIRED or SOFTWARE_TOKEN_MFA that you answer with RespondToAuthChallengeCommand.

This guide is for Node.js and TypeScript developers who build their own sign-in against a Cognito user pool instead of using managed login: a backend-for-frontend, a CLI, an internal tool or an integration test. You’ll get a small reusable module, an interactive example that handles a temporary password and TOTP MFA, token refresh, and verification of the tokens in your API.

All samples were type-checked with strict tsc against @aws-sdk/client-cognito-identity-provider 3.1142.0 and aws-jwt-verify 5.2.1, and run in September 2026 against a mocked Cognito client, a local endpoint and locally signed test tokens.

Which auth flow should you use?

AuthFlow What you send Use it when
USER_PASSWORD_AUTH USERNAME, PASSWORD Your code runs on a server or CLI you control. The password travels to Cognito over TLS.
USER_SRP_AUTH USERNAME, SRP_A, then a PASSWORD_VERIFIER response The password should never leave the client. The Secure Remote Password math is non-trivial; use a library such as amazon-cognito-identity-js or AWS Amplify rather than writing it yourself.
USER_AUTH USERNAME, optional PREFERRED_CHALLENGE Choice-based sign-in with passwords, one-time passwords or passkeys. Needs the Essentials feature plan or higher.
REFRESH_TOKEN_AUTH REFRESH_TOKEN Getting new ID and access tokens when refresh token rotation is off.
CUSTOM_AUTH USERNAME Lambda-driven custom challenges.

Each flow must be allowed on the app client (ALLOW_USER_PASSWORD_AUTH, ALLOW_USER_SRP_AUTH, ALLOW_REFRESH_TOKEN_AUTH and so on). Users who sign in through a federated identity provider can’t use InitiateAuth at all.

Prerequisites

  • Node.js 18 or later, @aws-sdk/client-cognito-identity-provider, and aws-jwt-verify for the API side.
  • A user pool, an app client ID, the client secret if the app client has one, and the Region of the pool.
  • A test user. Users you create as an administrator start with a temporary password, which is where NEW_PASSWORD_REQUIRED comes from; creating Cognito users with AdminCreateUser in SDK v3 shows how to make one.

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

  1. Enable the flow on the app clientAllow USER_PASSWORD_AUTH and refresh tokens, and nothing you don’t use.
  2. Compute SECRET_HASH if neededOnly for app clients with a secret, and only on a server.
  3. Send InitiateAuthCommandNo AWS credentials: the SDK sends this operation unsigned.
  4. Loop over challengesAnswer each ChallengeName with RespondToAuthChallengeCommand, passing the Session you received, until you get AuthenticationResult.
  5. Verify tokens where you use themYour API checks the access token’s signature and claims before trusting it.

How is SECRET_HASH calculated?

When an app client has a secret, every InitiateAuth and RespondToAuthChallenge request needs SECRET_HASH. The Cognito developer guide defines it as Base64(HMAC_SHA256(clientSecret, username + clientId)), an HMAC as described in RFC 2104 keyed with the client secret. In Node.js that’s one line with createHmac, shown in the module below; we checked its output against an independent Python implementation.

The username part is the value the user signs in with. For REFRESH_TOKEN_AUTH it’s stricter: when username is a sign-in attribute, use the username claim; when users sign in with an email or phone number instead, use their sub claim. Both are in the ID and access tokens you already have. Keep the secret itself out of code and environment variables you print; reading a Secrets Manager secret with AWS SDK v3 is a safer home for it.

Example: a reusable sign-in module

cognito-auth.ts

// cognito-auth.ts: sign users in to a Cognito user pool with InitiateAuth and RespondToAuthChallenge.
// Works for app clients with or without a client secret. InitiateAuth and RespondToAuthChallenge are
// unauthenticated API operations: the SDK sends them unsigned, so no AWS credentials are needed.
import { createHmac } from "node:crypto";
import {
  CognitoIdentityProviderClient,
  InitiateAuthCommand,
  RespondToAuthChallengeCommand,
  type AuthenticationResultType,
  type ChallengeNameType,
} from "@aws-sdk/client-cognito-identity-provider";

export interface CognitoConfig {
  region: string;
  clientId: string;
  clientSecret?: string; // only for confidential app clients; never ship it to a browser or mobile app
}

export type Challenge = { kind: "challenge"; name: ChallengeNameType; session: string; parameters: Record<string, string> };
export type AuthStep = { kind: "tokens"; tokens: AuthenticationResultType } | Challenge;

/** Base64(HMAC_SHA256(clientSecret, username + clientId)), as the Cognito developer guide specifies. */
export function secretHash(username: string, clientId: string, clientSecret: string): string {
  return createHmac("sha256", clientSecret).update(username + clientId).digest("base64");
}

export class CognitoAuth {
  private readonly client: CognitoIdentityProviderClient;
  private readonly config: CognitoConfig;

  constructor(config: CognitoConfig) {
    this.config = config;
    this.client = new CognitoIdentityProviderClient({ region: config.region });
  }

  private withHash(username: string, params: Record<string, string>): Record<string, string> {
    const { clientId, clientSecret } = this.config;
    return clientSecret ? { ...params, SECRET_HASH: secretHash(username, clientId, clientSecret) } : params;
  }

  private toStep(out: {
    AuthenticationResult?: AuthenticationResultType;
    ChallengeName?: ChallengeNameType;
    Session?: string;
    ChallengeParameters?: Record<string, string>;
  }): AuthStep {
    if (out.AuthenticationResult) return { kind: "tokens", tokens: out.AuthenticationResult };
    if (!out.ChallengeName || !out.Session) throw new Error("Cognito returned neither tokens nor a challenge");
    return { kind: "challenge", name: out.ChallengeName, session: out.Session, parameters: out.ChallengeParameters ?? {} };
  }

  /** USER_PASSWORD_AUTH sends the password to Cognito over TLS. The app client must allow ALLOW_USER_PASSWORD_AUTH. */
  async signIn(username: string, password: string): Promise<AuthStep> {
    const out = await this.client.send(new InitiateAuthCommand({
      AuthFlow: "USER_PASSWORD_AUTH",
      ClientId: this.config.clientId,
      AuthParameters: this.withHash(username, { USERNAME: username, PASSWORD: password }),
    }));
    return this.toStep(out);
  }

  /** Answers NEW_PASSWORD_REQUIRED, SOFTWARE_TOKEN_MFA, SMS_MFA, EMAIL_OTP and similar challenges. */
  async respond(step: Challenge, username: string, answers: Record<string, string>): Promise<AuthStep> {
    const out = await this.client.send(new RespondToAuthChallengeCommand({
      ClientId: this.config.clientId,
      ChallengeName: step.name,
      Session: step.session,
      ChallengeResponses: this.withHash(username, { USERNAME: username, ...answers }),
    }));
    return this.toStep(out);
  }

  /**
   * REFRESH_TOKEN_AUTH returns new ID and access tokens; the original refresh token stays valid.
   * With a client secret, hash with the username claim when username is a sign-in attribute,
   * otherwise with the user's sub claim (both are in the ID and access tokens).
   */
  async refresh(refreshToken: string, hashUsername?: string): Promise<AuthenticationResultType> {
    const params: Record<string, string> = { REFRESH_TOKEN: refreshToken };
    const out = await this.client.send(new InitiateAuthCommand({
      AuthFlow: "REFRESH_TOKEN_AUTH",
      ClientId: this.config.clientId,
      AuthParameters: hashUsername ? this.withHash(hashUsername, params) : params,
    }));
    if (!out.AuthenticationResult) throw new Error(`unexpected challenge ${out.ChallengeName ?? "(none)"}`);
    return out.AuthenticationResult;
  }
}

We confirmed the “no credentials” part by pointing the client at a local endpoint with every credential source removed: the SDK sent InitiateAuth with no Authorization header, because the client marks this operation as noAuth. Cognito doesn’t evaluate IAM policies for these calls either.

Example: handle a temporary password and MFA

sign-in-cli.ts

// sign-in-cli.ts: interactive sign-in that handles a temporary password and TOTP or SMS MFA.
// Usage: COGNITO_CLIENT_ID=... [COGNITO_CLIENT_SECRET=...] AWS_REGION=... npx tsx sign-in-cli.ts alice
import { createInterface } from "node:readline/promises";
import {
  NotAuthorizedException,
  PasswordResetRequiredException,
  UserNotConfirmedException,
  CodeMismatchException,
  InvalidPasswordException,
  TooManyRequestsException,
} from "@aws-sdk/client-cognito-identity-provider";
import { CognitoAuth, type AuthStep } from "./cognito-auth.js";

const username = process.argv[2];
if (!username) throw new Error("usage: sign-in-cli.ts <username>");
const auth = new CognitoAuth({
  region: process.env.AWS_REGION ?? "us-east-1",
  clientId: process.env.COGNITO_CLIENT_ID ?? "",
  clientSecret: process.env.COGNITO_CLIENT_SECRET,
});
const rl = createInterface({ input: process.stdin, output: process.stdout });

try {
  let step: AuthStep = await auth.signIn(username, await rl.question("Password: "));
  while (step.kind === "challenge") {
    console.log(`Challenge: ${step.name}`);
    if (step.name === "NEW_PASSWORD_REQUIRED") {
      step = await auth.respond(step, username, { NEW_PASSWORD: await rl.question("New password: ") });
    } else if (step.name === "SOFTWARE_TOKEN_MFA") {
      step = await auth.respond(step, username, { SOFTWARE_TOKEN_MFA_CODE: await rl.question("Authenticator code: ") });
    } else if (step.name === "SMS_MFA") {
      step = await auth.respond(step, username, { SMS_MFA_CODE: await rl.question("SMS code: ") });
    } else {
      throw new Error(`challenge ${step.name} isn't handled by this example`);
    }
  }
  const { IdToken, AccessToken, RefreshToken, ExpiresIn } = step.tokens;
  console.log(`Signed in. ID and access tokens expire in ${ExpiresIn} seconds.`);
  console.log(`ID token ${IdToken?.length ?? 0} chars, access token ${AccessToken?.length ?? 0} chars, refresh token ${RefreshToken ? "received" : "missing"}.`);
} catch (err) {
  if (err instanceof NotAuthorizedException) console.error("NotAuthorizedException: check the username, password and secret hash.");
  else if (err instanceof UserNotConfirmedException) console.error("UserNotConfirmedException: confirm the account first.");
  else if (err instanceof PasswordResetRequiredException) console.error("PasswordResetRequiredException: start ForgotPassword.");
  else if (err instanceof CodeMismatchException) console.error("CodeMismatchException: wrong code, sign in again.");
  else if (err instanceof InvalidPasswordException) console.error("InvalidPasswordException: the new password fails the pool's policy.");
  else if (err instanceof TooManyRequestsException) console.error("TooManyRequestsException: retry later with backoff.");
  else throw err;
  process.exitCode = 1;
} finally {
  rl.close();
}
Terminal

npm install @aws-sdk/client-cognito-identity-provider
npm install --save-dev tsx typescript @types/node
COGNITO_CLIENT_ID=1example23456789 COGNITO_CLIENT_SECRET=... AWS_REGION=us-east-1 npx tsx sign-in-cli.ts alice
Output

Password: Temp-Passw0rd!
Challenge: NEW_PASSWORD_REQUIRED
New password: N3w-Long-Passphrase!
Challenge: SOFTWARE_TOKEN_MFA
Authenticator code: 123456
Signed in. ID and access tokens expire in 3600 seconds.
ID token 1142 chars, access token 1071 chars, refresh token received.

This session ran against a mocked client: the first call returned NEW_PASSWORD_REQUIRED, the second SOFTWARE_TOKEN_MFA, the third the tokens, and every request carried the same SECRET_HASH. readline echoes what you type, so use a hidden prompt in a real tool. Each Session is valid for the app client’s AuthSessionValidity (set in minutes); if the user takes longer, start again with InitiateAuth. Enforcing MFA is a user pool setting, and the script to find Cognito user pools without MFA shows which pools don’t require it.

How do you refresh tokens?

ID and access tokens are short-lived; the refresh token lasts 30 days by default (configurable from 60 minutes to 10 years per app client). Call refresh() from the module above, which sends REFRESH_TOKEN_AUTH. That flow returns new ID and access tokens only; the original refresh token stays valid.

If you turn on refresh token rotation, which AWS recommends as a security practice, REFRESH_TOKEN_AUTH stops working for that app client and you must call GetTokensFromRefreshTokenCommand instead, which returns a new refresh token each time. To end a session, revoke the refresh token with RevokeToken, or sign the user out everywhere with GlobalSignOut.

How do you verify Cognito tokens in your API?

Cognito tokens are JSON Web Tokens as defined in RFC 7519, signed with the user pool’s keys. Decoding one isn’t enough; verify the signature and the claims. AWS’s aws-jwt-verify library does both:

verify-token.ts

// verify-token.ts: verify a Cognito access token in your API before trusting any claim in it.
import { CognitoJwtVerifier } from "aws-jwt-verify";
import { JwtExpiredError, JwtBaseError } from "aws-jwt-verify/error";

export const verifier = CognitoJwtVerifier.create({
  userPoolId: process.env.COGNITO_USER_POOL_ID ?? "us-east-1_EXAMPLE",
  tokenUse: "access", // use "id" to verify ID tokens
  clientId: process.env.COGNITO_CLIENT_ID ?? "1example23456789",
});

export async function authenticate(authorization: string | undefined): Promise<{ sub: string; username: string }> {
  const token = authorization?.replace(/^Bearer /, "");
  if (!token) throw new Error("missing bearer token");
  try {
    // Checks the signature against the pool's JWKS, then issuer, expiry, token_use and client_id.
    const payload = await verifier.verify(token);
    return { sub: payload.sub, username: String(payload.username) };
  } catch (err) {
    if (err instanceof JwtExpiredError) throw new Error("token expired: refresh it and retry");
    if (err instanceof JwtBaseError) throw new Error(`invalid token: ${err.message}`);
    throw err; // for example a network error while fetching the JWKS
  }
}

We tested it with tokens signed by a local RSA key loaded through verifier.cacheJwks(): a valid access token returned its sub and username, while an expired token, an ID token and a token for another client were all rejected. In production the verifier downloads the pool’s JWKS once and caches it. Unit tests for handlers that call Cognito follow the pattern in mocking AWS SDK v3 clients in unit tests.

Which permissions does sign-in need?

None for InitiateAuth and RespondToAuthChallenge: they’re public operations authorized by the app client ID, the secret hash and the user’s credentials, and you can’t grant or restrict them with IAM. The server-side variants, AdminInitiateAuth with ADMIN_USER_PASSWORD_AUTH and AdminRespondToAuthChallenge, are IAM-authorized and need a policy like this:

admin-sign-in-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ServerSideSignInOnly",
      "Effect": "Allow",
      "Action": [
        "cognito-idp:AdminInitiateAuth",
        "cognito-idp:AdminRespondToAuthChallenge"
      ],
      "Resource": "arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_EXAMPLE"
    }
  ]
}

If an admin call fails with an access-denied error, work through troubleshooting AWS IAM access denied errors. The IAM policy generator for TypeScript code drafts policies for the admin commands if you paste your handler.

Troubleshooting InitiateAuth errors

Catch errors with instanceof on the exception classes the client exports. The names below are from the InitiateAuth and RespondToAuthChallenge API reference:

  • NotAuthorizedException: the reference describes it as a user who isn’t authorized. Check the password first, then the SECRET_HASH inputs (secret, client ID and the exact username string). With PreventUserExistenceErrors set to ENABLED, unknown users also get an error that doesn’t reveal whether the user exists; with the default LEGACY they get UserNotFoundException.
  • UserNotConfirmedException: the user signed up but never confirmed. PasswordResetRequiredException: an imported user must reset the password first.
  • CodeMismatchException and ExpiredCodeException: a wrong or stale MFA code in RespondToAuthChallenge.
  • InvalidPasswordException: the new password in NEW_PASSWORD_REQUIRED fails the pool’s password policy.
  • TooManyRequestsException: throttling. The SDK retries with backoff; see AWS SDK v3 retries and timeouts to tune it.
  • InvalidParameterException: a parameter Cognito can’t accept. Check that the AuthFlow is allowed on the app client and that every challenge response includes USERNAME.

What are the limits?

  • Request rates. InitiateAuth, RespondToAuthChallenge, their admin versions and token refresh share the UserAuthentication quota, 120 requests per second by default per account and Region. Cognito also limits requests per user to 10 per second for operations such as InitiateAuth.
  • SRP timing. In the SRP flow, the PASSWORD_VERIFIER response must arrive within a few seconds or Cognito returns NotAuthorizedException.
  • No federated users. Social and SAML users sign in through managed login and the OAuth endpoints, not InitiateAuth.
  • Client secrets belong on servers. Browser and mobile apps should use an app client without a secret, usually with SRP.

Coming from the v2 CognitoIdentityServiceProvider class? The AWS SDK JavaScript v2 to v3 converter drafts the command-style code, and migrating a Node.js app to AWS SDK v3 covers the wider change. If sign-in runs in Lambda, reducing AWS SDK v3 Lambda bundle size keeps cold starts down.

Frequently asked questions

Does Cognito InitiateAuth need AWS credentials in SDK v3?

No. InitiateAuth and RespondToAuthChallenge are unauthenticated operations, and SDK v3 sends them unsigned. AdminInitiateAuth is the IAM-authorized server-side variant.

How do I calculate SECRET_HASH for Cognito in Node.js?

createHmac("sha256", clientSecret).update(username + clientId).digest("base64"). For refresh-token requests, use the sub claim instead of the sign-in name when username isn’t a sign-in attribute.

Why does REFRESH_TOKEN_AUTH fail after enabling refresh token rotation?

Rotation isn’t compatible with that flow. Call GetTokensFromRefreshToken instead; it also returns the new refresh token.

How do I implement USER_SRP_AUTH with AWS SDK v3?

The SDK sends the requests but doesn’t compute SRP values. Use a library that implements the protocol, such as amazon-cognito-identity-js or AWS Amplify, or use USER_PASSWORD_AUTH from a trusted server.

Related guides

Ask your AWS account in plain English

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

npx chatwithcloud