{"slug": "how-iam-identity-center-powers-secure-access-for-claude-ai-agents-in-node-js", "title": "How IAM Identity Center Powers Secure Access for Claude AI Agents in Node.js, Explained Simply", "summary": "A developer outlined a pattern for giving Claude-powered Lambda functions short-lived, automatically rotated AWS credentials by using IAM Identity Center permission sets and STS AssumeRoleWithSAML instead of hard-coded static keys. The approach defines a minimal policy in TypeScript using the satisfies operator to catch typos at compile time, then exchanges a SAML assertion for temporary credentials valid for up to one hour before calling Bedrock. The pattern is presented as reusable for any LLM-backed tool.", "body_md": "Human users and AI agents both need the right permissions, but mixing SSO with programmatic access is tricky. In just a few minutes you’ll see how to let a Claude‑powered Lambda speak to Bedrock without ever hard‑coding static keys. The pattern works for any LLM‑backed tool you build today.\n\nWhen a human logs into the AWS console we usually think about **IAM Identity Center** (formerly AWS SSO) as a single‑sign‑on (SSO) portal. It lets people click a link, type a password, and get a web console with all the permissions they need.  \n\nAn **AI agent**—for example a Lambda function that talks to Claude on Bedrock—needs the same “right‑things‑can‑do” guarantee, but it can’t click a button. If you give the function long‑lived static credentials, you expose a secret that never rotates.  \n\n**In plain English**: IAM Identity Center can hand out short‑lived, automatically‑rotated credentials for programs, just like it hands a temporary badge to a visitor.  \n\nThe benefit is two‑fold:\n\nThink of IAM Identity Center as a hotel front desk. A human guest gets a room key that works for a few hours. An AI agent is a robot concierge that needs a key to fetch a specific amenity (the Claude model). The front desk can issue a temporary key for the robot, and the key expires automatically, keeping the hotel secure.\n\nA **permission set** groups one or more IAM policies and is assigned to a user or group in IAM Identity Center. For a machine, you create a dedicated permission set that includes only the actions needed to call Bedrock and read any supporting resources (like a Secrets Manager secret that holds the SAML assertion).  \n\n**Tip**: Keep the permission set tiny. If the agent only needs `bedrock:InvokeModel`, don’t also grant `s3:*`.  \n\n`ClaudeAgentAccess`.\n`bedrock:InvokeModel` on the Claude model ARN.\n`sts:AssumeRoleWithSAML` on the SAML provider you will use.\n`secretsmanager:GetSecretValue`).\n`satisfies`\nInstead of hand‑editing JSON in the console, you can write the policy in TypeScript and let the compiler verify its shape. The `satisfies` operator ensures the object conforms to the IAM policy schema at compile time.\n\n``` python\n// src/policy.ts\nimport type { PolicyDocument } from \"aws-sdk/clients/iam\";\n\n/**\n * Inline policy granting the minimal rights for a Claude‑powered Lambda.\n * The `satisfies` keyword makes TypeScript check that every required field exists.\n */\nexport const claudeAgentPolicy = {\n  Version: \"2012-10-17\",\n  Statement: [\n    {\n      Effect: \"Allow\",\n      Action: [\n        \"bedrock:InvokeModel\",               // call Claude on Bedrock\n        \"sts:AssumeRoleWithSAML\",            // exchange SAML for temporary creds\n        \"secretsmanager:GetSecretValue\",     // read the SAML assertion stored securely\n      ],\n      Resource: \"*\", // you could lock this down to specific ARNs for tighter security\n    },\n  ],\n} satisfies PolicyDocument;\n```\n\n**Key takeaway**: Using `satisfies` catches typos (like `\"bedrock:InvokModel\"`) before you ever deploy, turning a runtime surprise into a compile‑time warning.\n\nYou can copy the resulting JSON into the console’s **Inline Policy** editor when you create the permission set.\n\nNow that the permission set exists, the Lambda must **assume** the role represented by that set. The standard way is to call the AWS Security Token Service (STS) operation `AssumeRoleWithSAML`. The flow looks like this:\n\n`AssumeRoleWithSAML`, passing the SAML assertion and the ARN of the permission set’s `AccessKeyId`, `SecretAccessKey`, `SessionToken`) that are valid for up to 1 hour.\nBelow is a minimal, fully‑commented Lambda handler written in TypeScript for **Node.js 22**. It uses the AWS SDK v3 packages you asked for: `@aws-sdk/client-sts`, `@aws-sdk/client-secrets-manager`, and `@aws-sdk/client-bedrock-runtime`. (The `client-iam` and `client-lambda` packages are not needed directly in the handler but are listed in the service context for completeness.)\n\n``` js\n// src/handler.ts\nimport {\n  STSClient,\n  AssumeRoleWithSAMLCommand,\n} from \"@aws-sdk/client-sts\";\nimport {\n  SecretsManagerClient,\n  GetSecretValueCommand,\n} from \"@aws-sdk/client-secrets-manager\";\nimport {\n  BedrockRuntimeClient,\n  InvokeModelCommand,\n} from \"@aws-sdk/client-bedrock-runtime\";\nimport type { Handler, Context, Callback } from \"aws-lambda\";\n\n// Environment variables you set in the Lambda console\nconst {\n  SAML_SECRET_ID,          // Secrets Manager name that holds the Base64‑encoded SAML assertion\n  IAM_ROLE_ARN,            // The role ARN linked to the permission set\n  CLAUDE_MODEL_ID,         // e.g. \"anthropic.claude-v2:1\"\n  REGION = \"us-east-1\",    // Bedrock region\n} = process.env;\n\n// Re‑use SDK clients – they are cheap to create and benefit from connection pooling\nconst sts = new STSClient({ region: REGION });\nconst secrets = new SecretsManagerClient({ region: REGION });\n\n/**\n * Helper: fetch the raw SAML assertion (Base64 string) from Secrets Manager.\n */\nasync function getSamlAssertion(): Promise<string> {\n  const cmd = new GetSecretValueCommand({ SecretId: SAML_SECRET_ID });\n  const resp = await secrets.send(cmd);\n  if (!resp.SecretString) {\n    throw new Error(\"SAML secret is empty\");\n  }\n  return resp.SecretString;\n}\n\n/**\n * Helper: exchange the SAML assertion for temporary AWS credentials.\n * Throws ExpiredTokenException if the SAML token is older than 1 hour.\n */\nasync function assumeRoleWithSaml(samlAssertion: string) {\n  const cmd = new AssumeRoleWithSAMLCommand({\n    RoleArn: IAM_ROLE_ARN,\n    PrincipalArn: `arn:aws:iam::${process.env.AWS_ACCOUNT_ID}:saml-provider/YourIdCProvider`,\n    SAMLAssertion: Buffer.from(samlAssertion).toString(\"base64\"),\n    DurationSeconds: 3600, // 1 hour, matches the SAML token lifetime\n  });\n\n  const resp = await sts.send(cmd);\n  if (!resp.Credentials) {\n    throw new Error(\"Failed to obtain temporary credentials\");\n  }\n  return resp.Credentials;\n}\n\n/**\n * Main Lambda entry point.\n * It fetches a SAML token, swaps it for short‑lived credentials,\n * then calls Claude on Bedrock with those credentials.\n */\nexport const handler: Handler = async (\n  event: unknown,\n  _context: Context,\n  _callback: Callback\n) => {\n  try {\n    // 1️⃣ Pull the SAML assertion (the \"badge\" the front desk gave us)\n    const samlAssertion = await getSamlAssertion();\n\n    // 2️⃣ Turn the badge into a temporary keycard (AssumeRoleWithSAML)\n    const tempCreds = await assumeRoleWithSaml(samlAssertion);\n\n    // 3️⃣ Build a Bedrock client that uses the temporary credentials\n    const bedrock = new BedrockRuntimeClient({\n      region: REGION,\n      credentials: {\n        accessKeyId: tempCreds.AccessKeyId!,\n        secretAccessKey: tempCreds.SecretAccessKey!,\n        sessionToken: tempCreds.SessionToken!,\n        expiration: tempCreds.Expiration,\n      },\n    });\n\n    // 4️⃣ Prepare a simple Claude request\n    const prompt = \"Explain the difference between TCP and UDP in two sentences.\";\n    const payload = {\n      // Bedrock expects a JSON string for Claude's body\n      body: JSON.stringify({ prompt, max_tokens_to_sample: 256, temperature: 0.7 }),\n      modelId: CLAUDE_MODEL_ID,\n      accept: \"application/json\",\n      contentType: \"application/json\",\n    };\n\n    // 5️⃣ Send the request\n    const response = await bedrock.send(new InvokeModelCommand(payload));\n\n    // 6️⃣ Return the model's answer (or handle streaming in real apps)\n    const result = Buffer.from(response.body!).toString(\"utf-8\");\n    console.log(\"Claude response:\", result);\n\n    return {\n      statusCode: 200,\n      body: result,\n    };\n  } catch (error: any) {\n    // The most common failure is an ExpiredTokenException when the SAML token\n    // is older than 1 hour. We re‑run the assume flow in that case.\n    if (error.name === \"ExpiredTokenException\") {\n      console.warn(\"SAML token expired – retrying assume flow\");\n      // In a real Lambda you might loop a couple of times or push to DLQ.\n      throw error;\n    }\n\n    console.error(\"Unhandled error:\", error);\n    return {\n      statusCode: 500,\n      body: JSON.stringify({ message: error.message }),\n    };\n  }\n};\n```\n\n**Helpful tip**: The AWS SDK for JavaScript does *not* automatically refresh SAML‑derived credentials. If you see `ExpiredTokenException`, catch it and run the `AssumeRoleWithSAML` flow again, as demonstrated in the `catch` block.\n\n`import` syntax exclusively; mixing `require` will cause silent failures.\nNow that we have a Bedrock client backed by temporary credentials, the actual model call is straightforward. The heavy lifting is in the request payload format, which differs per provider. Claude expects a JSON body with `prompt`, `max_tokens_to_sample`, and optional parameters like `temperature`.  \n\n**In plain English**: Think of the Bedrock client as a mailbox you hand a sealed letter to (the request). The envelope (HTTP headers) tells Bedrock what kind of letter it is (`application/json`). The temporary credentials are the postage stamp that proves you’re allowed to send mail to that address.\n\n``` js\n// Inside the Lambda handler – after creating `bedrock` client\nconst requestPayload = {\n  body: JSON.stringify({\n    prompt: \"Write a haiku about sunrise over the mountains.\",\n    max_tokens_to_sample: 64,\n    temperature: 0.5,\n  }),\n  modelId: CLAUDE_MODEL_ID,\n  accept: \"application/json\",\n  contentType: \"application/json\",\n};\n\nconst result = await bedrock.send(new InvokeModelCommand(requestPayload));\nconst modelOutput = Buffer.from(result.body!).toString(\"utf-8\");\nconsole.log(\"Claude says:\", modelOutput);\n```\n\n**Why we don’t use static keys**: The `bedrock` client receives credentials that expire after an hour. If the Lambda runs longer (e.g., a long‑running batch job), the next iteration will hit `ExpiredTokenException`. The catch block from the previous section restarts the assume flow, guaranteeing fresh credentials without any human touch.\n\nEarlier we stored the policy in `policy.ts`. Let’s expand that pattern to a **policy generation script** that could be run in CI/CD pipelines. The idea is to keep IAM definitions alongside your application code, ensuring they evolve together.\n\n``` js\n// scripts/build-policies.ts\nimport { writeFileSync } from \"fs\";\nimport { claudeAgentPolicy } from \"../src/policy\";\n\n// The `as const` assertion preserves literal types for maximum safety.\nconst policyJson = JSON.stringify(claudeAgentPolicy, null, 2) as const;\n\n// Write the policy to a file that can be fed to `aws iam create-policy`\nwriteFileSync(\"./dist/claude-agent-policy.json\", policyJson);\nconsole.log(\"Policy written to ./dist/claude-agent-policy.json\");\n```\n\nRunning `npm run build-policies` would generate a JSON file ready for the AWS CLI:\n\n```\naws iam create-policy \\\n  --policy-name ClaudeAgentInlinePolicy \\\n  --policy-document file://dist/claude-agent-policy.json\n```\n\n**Key takeaway**: By generating policies from TypeScript, you eliminate drift between code and permissions. The `satisfies` keyword guarantees the shape matches IAM’s expectations before you ever call the CLI.\n\n**Key points to remember** \n\n`ExpiredTokenException` yourself.\nBy following this pattern, you give your Claude‑powered agents the same security rigor you apply to human users, while staying free of hard‑coded keys and long‑lived secrets. The approach works for any LLM you hook up to Bedrock, making your AI tooling both safe and maintainable.\n\n**Transparency notice**\n\nThis article was written with the help of an AI system — [Groq](https://groq.com) (GPT OSS 120B).\n\n**Published:** 2026-10-19 · **Primary focus:** IAM\n\nAll code blocks are intended to be correct and runnable, but please verify them\n\nagainst the official docs for the tools mentioned before using in production.\n\n*Find an error? Drop a comment — corrections are always welcome.*", "url": "https://wpnews.pro/news/how-iam-identity-center-powers-secure-access-for-claude-ai-agents-in-node-js", "canonical_source": "https://dev.to/dineshgowtham/how-iam-identity-center-powers-secure-access-for-claude-ai-agents-in-nodejs-explained-simply-4mgm", "published_at": "2026-10-19 08:06:23+00:00", "updated_at": "2026-10-19 08:14:32.134863+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "ai-tools", "developer-tools"], "entities": ["AWS", "IAM Identity Center", "Claude", "Amazon Bedrock", "Node.js", "TypeScript", "AWS Lambda", "Anthropic"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-iam-identity-center-powers-secure-access-for-claude-ai-agents-in-node-js", "markdown": "https://wpnews.pro/news/how-iam-identity-center-powers-secure-access-for-claude-ai-agents-in-node-js.md", "text": "https://wpnews.pro/news/how-iam-identity-center-powers-secure-access-for-claude-ai-agents-in-node-js.txt", "jsonld": "https://wpnews.pro/news/how-iam-identity-center-powers-secure-access-for-claude-ai-agents-in-node-js.jsonld"}}