---
title: "Two Ways to Authorize an Agent Tool: IAM or OAuth on Amazon Bedrock AgentCore Gateway"
description: "When you expose a tool to agents through an AgentCore Gateway, the real decision is who is allowed to call it — SigV4/IAM for callers inside your AWS boundary, or a JWT authorizer for everyone else. Here are the three patterns and when each fits."
canonical_url: "https://artificialcuriositylabs.ai/posts/authorizing-agent-tools-iam-vs-oauth/"
md_url: "https://artificialcuriositylabs.ai/posts/authorizing-agent-tools-iam-vs-oauth.md"
published_at: "2026-07-11T00:00:00.000Z"
tags:
  - "agents"
  - "mcp"
  - "infrastructure"
---

When you expose a tool to agents, the interesting decision is not the tool. It is who is allowed to call it. Amazon Bedrock AgentCore Gateway turns that decision into a single field — `authorizerType` — and the value you pick determines whether the tool is a private capability for your own agents or a multi-tenant service anyone can reach with a token.

I run two gateways in front of AgentCore tools. One is authorized by IAM: my agents sign each request with an AWS profile, and there is nothing in the config to leak. The other is authorized by a JWT from an external identity provider: callers mint an OAuth token and send it as a bearer header. Same Gateway primitive, opposite trust models. This post is about how to choose between them — and the third option that sits in the middle.

## The Gateway is an authorizer with a tool behind it

AgentCore Gateway exposes tools over MCP. [WebSearch](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html), Lambda functions, and other connectors all attach as targets, and an agent discovers them with a standard `tools/list` call. That part is uniform. What differs per gateway is the gate in front of it.

A Gateway has exactly one `authorizerType`, set at creation, and it only accepts two values:

- **`AWS_IAM`** — the caller must present a SigV4-signed request from an AWS principal.
- **`CUSTOM_JWT`** — the caller must present a JWT the Gateway validates against an external identity provider (issuer, audience, expiry, scopes).

Everything else — the MCP protocol, the connector, the tool schema — is downstream of that choice. So "how do I wire web search into my agent" is really "which of these two gates do I want, and how does the caller get through it."

```mermaid
flowchart TD
    Q1{"Is the caller inside your AWS account?"}
    Q1 -->|yes| P1["Pattern 1: AWS_IAM + SigV4<br/>your own agents, Lambdas, tasks"]
    Q1 -->|another AWS account| P2["Pattern 2: AWS_IAM + AssumeRole<br/>AWS-to-AWS B2B"]
    Q1 -->|no AWS identity| P3["Pattern 3: CUSTOM_JWT + OAuth<br/>partners, SaaS, multi-tenant"]
    P1 --> G1["authorizerType = AWS_IAM"]
    P2 --> G1
    P3 --> G2["authorizerType = CUSTOM_JWT"]
```

## Pattern 1 — IAM + SigV4: the caller is you

With `AWS_IAM`, authorization collapses to a single question: does the caller hold valid AWS credentials for the account that owns the gateway? SigV4 answers it on every request. There is no token endpoint, no client secret, no refresh loop.

The caller resolves credentials from the standard AWS chain — environment variables, then a named profile, then an instance or container role — and signs the request. In code that is the only load-bearing part:

```python
import botocore.session, botocore.auth, botocore.awsrequest

creds = botocore.session.Session().get_credentials().get_frozen_credentials()
req = botocore.awsrequest.AWSRequest(method="POST", url=gateway_url, data=body, headers=headers)
botocore.auth.SigV4Auth(creds, "bedrock-agentcore", region).add_auth(req)
```

Notice what `get_credentials()` does not take: no API key, no secret, no token. On my machine the tool config sets a profile name and a region, and botocore does the rest. There is nothing in that config to leak, nothing to rotate, and nothing to expire.

The trade-off is the reach. "Auth" here means "has an IAM identity in my account." That makes it perfect for callers who already live inside your AWS boundary — your own laptop with a profile, a Lambda with an execution role, an ECS task with a task role. It makes it the wrong choice the moment you want to hand the tool to someone who is not in your account, because the only way to let them in is to give them an IAM principal, which is a far heavier grant than "here is a token."

## Pattern 2 — IAM + cross-account role assumption: the caller is another AWS account

If the caller is on AWS but in a *different* account — a partner, a separate org account — you can stay in IAM-land without adding OAuth. Keep the gateway on `AWS_IAM`, and have the external caller `sts:AssumeRole` into a role in your account that is permitted to invoke the gateway. They sign with the assumed role's temporary credentials; the Gateway sees a valid principal from your account.

This extends IAM auth across an account boundary without introducing an identity provider. You manage one trust policy; they manage one assume-role hop. Still no bearer tokens, still no secret distribution.

The cost is that both sides must be on AWS and comfortable with cross-account role plumbing. It does not help you reach a caller who has no AWS identity at all — a SaaS backend, a partner's non-AWS service, an individual you want to grant access to by handing them a credential.

## Pattern 3 — JWT + OAuth: the caller is everyone else

`CUSTOM_JWT` is the multi-tenant answer. The Gateway validates a JWT against an external identity provider — checking issuer, audience, expiry, and scopes — and the caller obtains that token however the IdP allows. For a machine-to-machine caller that is the `client_credentials` grant: exchange a client ID and secret for a short-lived access token, cache it, refresh on expiry, and send it as `Authorization: Bearer <token>`.

```python
# machine-to-machine token, then bearer header on every call
token = post(f"https://{idp_domain}/oauth/token", json={
    "grant_type": "client_credentials",
    "client_id": client_id,
    "client_secret": client_secret,
    "audience": audience,
    "scope": "mcp:invoke",
}).json()["access_token"]

forward(message, headers={"Authorization": f"Bearer {token}"})
```

Now the caller needs no AWS identity at all. Anyone you issue an IdP client to can reach the tool, and you can scope what they are allowed to do through the token's claims. This is the model to pick when the tool is meant to be shared — an internal platform tool, a service you expose to partners, anything with more than one tenant.

The cost is real, and it is everything IAM did not make you do. You now own an identity-provider relationship, client-secret issuance and storage, token lifecycle and refresh, and scope design. The bearer secret has to live somewhere on the caller's side and be protected. That is the "extra auth layer" — it buys you reach, and you pay for it in operational surface.

## The decision, in one table

| | IAM + SigV4 | IAM + cross-account role | JWT + OAuth |
|---|---|---|---|
| `authorizerType` | `AWS_IAM` | `AWS_IAM` | `CUSTOM_JWT` |
| Caller identity | AWS creds in your account | Assumed role from another AWS account | Token from your IdP |
| Secret in caller config | None | None | Client secret |
| Token lifecycle | None | STS handles it | You manage refresh |
| Who can call it | You and things in your AWS boundary | You + trusted AWS accounts | Anyone you issue a client to |
| You operate | Nothing extra | One trust policy | An IdP, secrets, scopes |
| Best for | Single-owner tools | AWS-to-AWS B2B | Multi-tenant / external |

The pattern is not "IAM is simpler, so use IAM." It is: **push authorization to the layer that already knows your caller.** If the caller is you or your own AWS workloads, that layer is IAM, and SigV4 gives you auth with nothing to rotate. If the caller is a separate AWS account, it is still IAM, through role assumption. If the caller is anyone else, that layer is an identity provider, and you take on the token machinery because reach is the thing you actually need.

## What's missing

Two gaps worth naming. The choice is made at gateway creation and is not a runtime toggle — a gateway is either IAM or JWT, so serving both an internal AWS caller and an external tenant means standing up two gateways in front of the same tool, not flipping a flag. And validating the token is not the same as scoping the caller: `CUSTOM_JWT` proves the token is genuine, but going from *this is a valid token* to *this caller may call this tool, with these arguments* is a separate layer. AgentCore does provide that layer — a [Cedar policy engine](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-understanding-cedar.html) you attach to the gateway, default-deny, where the principal is the caller's identity, the action is the tool name, and conditions can read the token's claims or even the tool's own arguments. What is on you is authoring those policies and issuing per-user tokens rather than a single machine credential; the enforcement mechanism is there, unwired until you write the rules.

The open thread I have not resolved is the seam between the two. When a tool needs to be reachable by both my own agents and an external caller, I run two gateways — but that means two authorizers, two configs, and two things to keep in sync for one underlying capability. I do not yet have a clean pattern for a single tool that is IAM-authorized for me and JWT-authorized for everyone else without duplicating the front door. If that seam has a tidy answer, I have not found it.

## So what

Before you wire a tool into your agents, decide who is allowed to call it, because that decision picks your authorizer and everything downstream follows. If the answer is "only me and my own workloads," IAM with SigV4 is auth with no secrets and nothing to rotate — take it. The moment the answer becomes "and other people," you are signing up for an identity provider and a token lifecycle, and that cost should be a deliberate choice, not something you discover after you have already shipped the tool with the wrong gate in front of it.

---

*Part of a series working through Amazon Bedrock AgentCore by building on it. Start with [The AgentCore Map](https://artificialcuriositylabs.ai/posts/the-agentcore-map) for the full picture. This post covered who may *reach* a tool; the next one, [Who May Call What](https://artificialcuriositylabs.ai/posts/per-user-authorization-cedar), covers what a caller may *do* once the token is valid — per-user authorization with Cedar. See also [Two Things I Almost Called AgentCore Gaps](https://artificialcuriositylabs.ai/posts/where-agentcore-sits).*
