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, andaws-jwt-verifyfor 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_REQUIREDcomes 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
- Enable the flow on the app clientAllow
USER_PASSWORD_AUTHand refresh tokens, and nothing you don’t use. - Compute
SECRET_HASHif neededOnly for app clients with a secret, and only on a server. - Send
InitiateAuthCommandNo AWS credentials: the SDK sends this operation unsigned. - Loop over challengesAnswer each
ChallengeNamewithRespondToAuthChallengeCommand, passing theSessionyou received, until you getAuthenticationResult. - 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: 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: 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();
}
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
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 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:
{
"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 theSECRET_HASHinputs (secret, client ID and the exact username string). WithPreventUserExistenceErrorsset toENABLED, unknown users also get an error that doesn’t reveal whether the user exists; with the defaultLEGACYthey getUserNotFoundException.UserNotConfirmedException: the user signed up but never confirmed.PasswordResetRequiredException: an imported user must reset the password first.CodeMismatchExceptionandExpiredCodeException: a wrong or stale MFA code inRespondToAuthChallenge.InvalidPasswordException: the new password inNEW_PASSWORD_REQUIREDfails 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 theAuthFlowis allowed on the app client and that every challenge response includesUSERNAME.
What are the limits?
- Request rates.
InitiateAuth,RespondToAuthChallenge, their admin versions and token refresh share theUserAuthenticationquota, 120 requests per second by default per account and Region. Cognito also limits requests per user to 10 per second for operations such asInitiateAuth. - SRP timing. In the SRP flow, the
PASSWORD_VERIFIERresponse must arrive within a few seconds or Cognito returnsNotAuthorizedException. - 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