Assume an IAM Role With AWS SDK v3 (STS AssumeRole)

A ring of metal keys resting on a dark wooden surface

Photo by rc.xyz NFT gallery on Unsplash

To call STS AssumeRole with AWS SDK v3, send AssumeRoleCommand from @aws-sdk/client-sts with a RoleArn and a RoleSessionName. The response’s Credentials object holds AccessKeyId, SecretAccessKey, SessionToken and Expiration. Map them to accessKeyId, secretAccessKey, sessionToken and expiration and pass that object as credentials to any other client.

Most code never calls STS directly. A profile with role_arn or the fromTemporaryCredentials provider assumes the role for you, and the guide to AWS SDK v3 credential providers, including assume role covers those. This guide is for the cases where you want the raw sts assumerole aws sdk v3 call: audit scripts that walk many accounts, tools that need the assumed-role ARN or session token size, code that sets session tags or a source identity, and anyone debugging why a role can’t be assumed.

You’ll get a working TypeScript example, a refreshing credential provider, a cross-account loop with a session policy, the trust and identity policies both sides need, and a checklist for AccessDenied. All samples were type-checked with strict tsc and run against aws-sdk-client-mock with @aws-sdk/client-sts 3.1141.0 in September 2026.

When should you call AssumeRole yourself?

Situation Use
One app, one role, credentials refreshed for you fromTemporaryCredentials or a role_arn profile
You need AssumedRoleUser.Arn, SourceIdentity or SessionTokenUtilization from the response AssumeRoleCommand
A loop over dozens of accounts with per-account error handling AssumeRoleCommand per account
Handing temporary credentials to another process or tool AssumeRoleCommand, then pass the four values on
Code running in Lambda, ECS or EC2 with its own role Nothing: the default chain uses the platform role

Prerequisites

  • Node.js 18 or later and a project with "type": "module" in package.json, because the samples use top-level await. Install @aws-sdk/client-sts, @aws-sdk/client-s3, @aws-sdk/client-sqs and @aws-sdk/types, plus tsx to run TypeScript.
  • Base credentials the SDK can find: a profile, IAM Identity Center session or environment variables. If you use ChatWithCloud, the same profiles work; see connecting ChatWithCloud to AWS profiles, SSO and roles.
  • A target role whose trust policy allows your identity, shown under permissions below.

How to call sts assumerole with AWS SDK v3, step by step

  1. Create an STS client with your base credentialsnew STSClient({ region }) uses the default chain, so the caller is whatever profile or role you already run as.
  2. Send AssumeRoleCommandPass RoleArn, a meaningful RoleSessionName and, if the trust policy demands them, ExternalId, MFA values or a SourceIdentity.
  3. Check the responseCredentials is optional in the SDK types, so guard against undefined before using it.
  4. Map the field namesSTS returns PascalCase (AccessKeyId); SDK clients expect camelCase (accessKeyId).
  5. Create clients with the credentialsPass the object as credentials. Each client signs its requests with the role until Expiration.
  6. Refresh before expiryNothing renews a static credentials object. Call AssumeRole again, or pass a provider function instead of an object.

Example: assume a role and use its credentials

assume-role.ts

// assume-role.ts: assume a role once and use its temporary credentials for another client.
// Usage: ROLE_ARN=arn:aws:iam::210987654321:role/ReadOnlyAudit EXTERNAL_ID=... npx tsx assume-role.ts
import { STSClient, AssumeRoleCommand, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";

const region = process.env.AWS_REGION ?? "us-east-1";
const roleArn = process.env.ROLE_ARN;
if (!roleArn) throw new Error("Set ROLE_ARN to the role you want to assume");

// The STS client itself uses your normal credentials (profile, SSO, environment, instance role).
const sts = new STSClient({ region });

const { Credentials, AssumedRoleUser } = await sts.send(
  new AssumeRoleCommand({
    RoleArn: roleArn,
    RoleSessionName: `audit-${process.env.USER ?? "script"}`.slice(0, 64), // 2-64 chars: letters, digits, _+=,.@-
    DurationSeconds: 3600, // 900 up to the role's maximum session duration; 3600 max when chaining roles
    ExternalId: process.env.EXTERNAL_ID, // only if the trust policy requires sts:ExternalId
  }),
);
if (!Credentials?.AccessKeyId || !Credentials.SecretAccessKey) throw new Error("AssumeRole returned no credentials");

// Map the STS response shape onto the SDK's credential shape.
const credentials = {
  accessKeyId: Credentials.AccessKeyId,
  secretAccessKey: Credentials.SecretAccessKey,
  sessionToken: Credentials.SessionToken,
  expiration: Credentials.Expiration,
};

const who = await new STSClient({ region, credentials }).send(new GetCallerIdentityCommand({}));
console.log(`Assumed ${AssumedRoleUser?.Arn} until ${Credentials.Expiration?.toISOString()}`);
console.log(`Calls now run as account ${who.Account}`);

const s3 = new S3Client({ region, credentials });
const { Buckets } = await s3.send(new ListBucketsCommand({}));
console.log(`${Buckets?.length ?? 0} buckets visible to the role`);
Output

Assumed arn:aws:sts::210987654321:assumed-role/ReadOnlyAudit/audit-jane until 2026-09-28T14:50:35.649Z
Calls now run as account 210987654321
2 buckets visible to the role

The output comes from a run against mocked responses; the account and ARN are examples. GetCallerIdentity is the quickest proof that the new credentials work, and the assumed-role ARN ends with your session name, which is also what the target account sees in CloudTrail. To see what that session is allowed to do, the script to check the permissions of your currently assumed IAM role resolves the role and lists its policies.

Which AssumeRole parameters matter?

These limits come from the STS AssumeRole API reference as of September 2026:

Parameter Rules
RoleSessionName Required. 2 to 64 characters: letters, digits and _+=,.@-. It appears in the assumed-role ARN and the target account’s CloudTrail logs.
DurationSeconds 900 up to the role’s maximum session duration (1 to 12 hours; default 3,600). With role chaining, anything above 3,600 fails.
ExternalId 2 to 1,224 characters. Needed when the trust policy checks sts:ExternalId, typical for third-party access.
Policy and PolicyArns Session policies. Up to 10 managed policy ARNs; inline and managed plaintext together can’t exceed 2,048 characters. The session gets the intersection with the role’s policies.
SourceIdentity 2 to 64 characters, not starting with aws:. It persists through role chaining and can’t be changed within the session.
Tags and TransitiveTagKeys Up to 50 session tags; keys up to 128 characters, values up to 256. Transitive tags carry over to chained sessions.
SerialNumber and TokenCode The MFA device and its 6-digit code, when the trust policy requires aws:MultiFactorAuthPresent.

Session policies and tags also count toward the session token’s size. Watch SessionTokenUtilization in the response (it replaces the deprecated PackedPolicySize) if you pass many tags.

How do you refresh credentials before they expire?

A credentials object is static. Pass a function instead and the SDK calls it whenever it needs credentials, so the function can call AssumeRole again shortly before Expiration. This provider caches the result, refreshes five minutes early and makes sure a burst of parallel requests triggers only one STS call:

refreshing-role-credentials.ts

// refreshing-role-credentials.ts: a credential provider that calls AssumeRole explicitly and
// refreshes five minutes before the session expires. Useful when you need the raw AssumeRole
// response (session tags, source identity, token size) and still want long-running clients.
import { STSClient, AssumeRoleCommand, type AssumeRoleCommandInput } from "@aws-sdk/client-sts";
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@aws-sdk/types";

const REFRESH_BEFORE_MS = 5 * 60_000;

export function assumeRoleProvider(sts: STSClient, input: AssumeRoleCommandInput): AwsCredentialIdentityProvider {
  let cached: AwsCredentialIdentity | undefined;
  let pending: Promise<AwsCredentialIdentity> | undefined;

  const fetchCredentials = async (): Promise<AwsCredentialIdentity> => {
    const { Credentials } = await sts.send(new AssumeRoleCommand(input));
    if (!Credentials?.AccessKeyId || !Credentials.SecretAccessKey || !Credentials.Expiration) {
      throw new Error(`AssumeRole for ${input.RoleArn} returned incomplete credentials`);
    }
    return {
      accessKeyId: Credentials.AccessKeyId,
      secretAccessKey: Credentials.SecretAccessKey,
      sessionToken: Credentials.SessionToken,
      expiration: Credentials.Expiration,
    };
  };

  return async () => {
    const fresh = cached?.expiration && cached.expiration.getTime() - Date.now() > REFRESH_BEFORE_MS;
    if (cached && fresh) return cached;
    // One AssumeRole call at a time, even when many requests ask for credentials at once.
    pending ??= fetchCredentials().finally(() => {
      pending = undefined;
    });
    cached = await pending;
    return cached;
  };
}
use-refreshing.ts

// use-refreshing.ts: long-running worker that keeps using an assumed role.
import { STSClient } from "@aws-sdk/client-sts";
import { SQSClient, ReceiveMessageCommand } from "@aws-sdk/client-sqs";
import { assumeRoleProvider } from "./refreshing-role-credentials.js";

const region = process.env.AWS_REGION ?? "us-east-1";
const credentials = assumeRoleProvider(new STSClient({ region }), {
  RoleArn: "arn:aws:iam::210987654321:role/QueueWorker",
  RoleSessionName: "queue-worker",
  DurationSeconds: 3600,
  SourceIdentity: "queue-worker", // needs sts:SetSourceIdentity; can't be changed later in the session
  Tags: [{ Key: "team", Value: "payments" }], // needs sts:TagSession in the trust policy
});

const sqs = new SQSClient({ region, credentials });
const queueUrl = "https://sqs.us-east-1.amazonaws.com/210987654321/orders";
for (let i = 0; i < 3; i++) {
  const { Messages } = await sqs.send(new ReceiveMessageCommand({ QueueUrl: queueUrl, WaitTimeSeconds: 20 }));
  console.log(`poll ${i + 1}: ${Messages?.length ?? 0} messages`);
}

In a mocked test, four concurrent calls to the provider produced one AssumeRole request, and a fifth call reused the cached credentials. If you don’t need anything from the raw response, fromTemporaryCredentials gives you the same refresh behavior with less code. The queue example builds on the guide to send and receive SQS messages with AWS SDK v3.

Warning: a session can’t be extended. When an assumed-role session calls AssumeRole for another role (role chaining), the new session is capped at one hour, so a long-running worker must keep refreshing from base credentials that last longer.

Example: loop over accounts with one role

Audits across an AWS Organization usually assume the same role name in every account. Catch errors per account so one missing role doesn’t stop the run, and use a session policy to narrow a broad role to what this script needs:

cross-account-audit.ts

// cross-account-audit.ts: assume the same read-only role in several accounts and collect one fact from each.
// Usage: ACCOUNTS=111111111111,222222222222 ROLE_NAME=ReadOnlyAudit npx tsx cross-account-audit.ts
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";

const region = process.env.AWS_REGION ?? "us-east-1";
const accounts = (process.env.ACCOUNTS ?? "").split(",").map((a) => a.trim()).filter((a) => /^\d{12}$/.test(a));
const roleName = process.env.ROLE_NAME ?? "ReadOnlyAudit";
const sts = new STSClient({ region });

// A session policy narrows the role for this run: the session gets only what both policies allow.
const sessionPolicy = JSON.stringify({
  Version: "2012-10-17",
  Statement: [{ Effect: "Allow", Action: "s3:ListAllMyBuckets", Resource: "*" }],
});

const results: { Account: string; Buckets: number | string; Error: string }[] = [];
for (const account of accounts) {
  try {
    const { Credentials } = await sts.send(
      new AssumeRoleCommand({
        RoleArn: `arn:aws:iam::${account}:role/${roleName}`,
        RoleSessionName: `bucket-audit-${account}`,
        DurationSeconds: 900, // shortest allowed; enough for one account
        Policy: sessionPolicy,
      }),
    );
    if (!Credentials?.AccessKeyId || !Credentials.SecretAccessKey) throw new Error("no credentials returned");
    const s3 = new S3Client({
      region,
      credentials: {
        accessKeyId: Credentials.AccessKeyId,
        secretAccessKey: Credentials.SecretAccessKey,
        sessionToken: Credentials.SessionToken,
        expiration: Credentials.Expiration,
      },
    });
    const { Buckets } = await s3.send(new ListBucketsCommand({}));
    results.push({ Account: account, Buckets: Buckets?.length ?? 0, Error: "" });
  } catch (err) {
    // One broken account (missing role, wrong trust policy, SCP) shouldn't stop the audit.
    results.push({ Account: account, Buckets: "-", Error: err instanceof Error ? `${err.name}: ${err.message}` : String(err) });
  }
}
console.table(results);

Roles created for one-off cross-account scripts tend to stay long after the audit ends. Run the scripts to find IAM roles trusted by external AWS accounts and find unused IAM roles with RoleLastUsed now and then. Roles assumed through federation rather than by another account depend on an identity provider, and the script to audit IAM SAML and OIDC identity providers checks those providers and their trust policies.

Permissions needed

Cross-account AssumeRole needs both sides. The target role’s trust policy names who may assume it, and the caller’s identity policy allows sts:AssumeRole on the role’s ARN. Session tags need sts:TagSession and a source identity needs sts:SetSourceIdentity, again on both sides. Trust policy on ReadOnlyAudit in each member account:

trust-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAuditRunnerWithExternalId",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111111111111:role/AuditRunner" },
      "Action": "sts:AssumeRole",
      "Condition": { "StringEquals": { "sts:ExternalId": "4f1c2a9e-audit" } }
    },
    {
      "Sid": "AllowSessionTagsAndSourceIdentity",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111111111111:role/AuditRunner" },
      "Action": ["sts:TagSession", "sts:SetSourceIdentity"]
    }
  ]
}

Identity policy for the caller (AuditRunner in the audit account):

caller-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AssumeAuditRoleInMemberAccounts",
      "Effect": "Allow",
      "Action": ["sts:AssumeRole", "sts:TagSession", "sts:SetSourceIdentity"],
      "Resource": "arn:aws:iam::*:role/ReadOnlyAudit"
    }
  ]
}

Within one account, the trust policy alone is enough when it names the principal directly. Check both documents against least privilege with how to review a generated IAM policy for least privilege, and list what the code itself calls with the IAM policy generator for TypeScript AWS SDK code or the guide to finding the IAM actions your AWS SDK for JavaScript code needs.

Troubleshooting AccessDenied and other AssumeRole errors

An AssumeRole denial reaches your code as an error whose name is AccessDenied. Check these causes, most of which the IAM troubleshooting docs list:

  • The caller’s policy doesn’t allow sts:AssumeRole on that role ARN. A service control policy on the caller’s account can also block it.
  • The trust policy doesn’t name your account, role or user. If you now run as an SSO role, its ARN differs from the IAM user the policy was written for.
  • A trust policy condition isn’t met: wrong or missing ExternalId, no MFA, a missing required source identity or session tag, or a date or IP condition.
  • The role name is wrong. Role names are case sensitive in the ARN.
  • Tags or source identity without sts:TagSession or sts:SetSourceIdentity. The whole call fails, not just the extra.

The step-by-step method in troubleshooting AWS IAM access denied errors applies here, and CloudTrail shows which principal and session name made each AssumeRole call; check CloudTrail is logging in every Region first.

Other errors the API reference lists for AssumeRole, as SDK v3 exception classes:

  • MalformedPolicyDocumentException: the session policy JSON is invalid.
  • PackedPolicyTooLargeException: session policies plus tags made the token too large; the message reports both sizes in bytes.
  • RegionDisabledException: STS isn’t activated in that Region for the account; an administrator activates it in the IAM console.
  • ExpiredTokenException: listed for expired tokens; if you see it from AssumeRole, refresh your base session, for example with aws sso login.

A DurationSeconds above the role’s maximum, or above 3,600 during role chaining, also fails. Lower it or raise the role’s maximum session duration.

Limits of calling AssumeRole directly

  • Credentials from AssumeRole can’t call STS GetFederationToken or GetSessionToken.
  • A session policy can only remove permissions; it never adds to what the role allows.
  • Every assumed session is capped at 12 hours, one hour when chained. There’s no extension, only a new call.
  • Each client created with a static object keeps using it after expiry and starts failing. Use a provider function for anything long-running, and set timeouts so a slow STS call doesn’t stall requests, as in AWS SDK v3 retry and timeout settings.
  • Unit tests shouldn’t call STS at all; mock AWS SDK v3 clients in Jest or Vitest as these samples were tested.

The client’s source and changelog live in the client-sts package in the aws-sdk-js-v3 repository. ChatWithCloud itself uses whatever profile you pick, including role profiles; the ChatWithCloud security model explains where credentials stay.

Frequently asked questions

What is the maximum DurationSeconds for STS AssumeRole?

43,200 seconds (12 hours), but only if the role’s maximum session duration allows it. The default is 3,600. When you assume a role with credentials from another assumed role, the limit is one hour.

How do I use AssumeRole credentials with another AWS SDK v3 client?

Map AccessKeyId, SecretAccessKey, SessionToken and Expiration to accessKeyId, secretAccessKey, sessionToken and expiration, and pass the object, or a function returning it, as credentials when you create the client.

Is fromTemporaryCredentials the same as calling AssumeRoleCommand?

It calls AssumeRole under the hood and refreshes automatically, but it hides the response. Call AssumeRoleCommand yourself when you need the assumed-role ARN, source identity or token size, or per-account error handling.

Why does AssumeRole return AccessDenied when my policy allows sts:AssumeRole?

Both sides must agree. The target role’s trust policy must also name your principal, and any condition in it, such as an external ID or MFA, must be met. A service control policy on your account can deny it too.

Related guides

Ask your AWS account in plain English

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

npx chatwithcloud