DEV Community

Amit
Amit

Posted on Originally published at artificialcuriositylabs.ai

Two Ways to Authorize an Agent Tool: IAM or OAuth on Amazon Bedrock AgentCore Gateway

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, 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."

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"]
Enter fullscreen mode Exit fullscreen mode

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:

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)
Enter fullscreen mode Exit fullscreen mode

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>.

# 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}"})
Enter fullscreen mode Exit fullscreen mode

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 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 for the full picture. This post covered who may *reach a tool; the next one, Who May Call What, 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.*

Top comments (2)

Collapse
 
reidmarlow profile image
Reid Marlow

The boundary I care about is who the tool is allowed to represent. IAM is cleaner when the caller is really an AWS workload and the audit trail should stay on principals and roles. OAuth is the better default once the tool is acting for a human or outside org, because the token can carry the user-shaped claims you need for policy. AssumeRole is a nice middle step, but I would be careful not to let it become a fake user model with nicer plumbing.

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

Strong framing: authenticate at the layer that already knows the caller, then authorize the actual tool invocation separately.

One operational guardrail I would add for the two-gateway pattern is a shared, versioned authorization contract. Keep the tool/argument policy, capability IDs, and test fixtures in one source, compile them into each gateway's enforcement layer, and emit the contract version with every decision. Then CI can replay the same allow/deny matrix through IAM and JWT entrances—including cross-tenant IDs, stale scopes, confused-deputy resource substitutions, and revoked principals.

That turns “two front doors” from duplicated policy into two adapters around one reviewed contract. Drift becomes observable instead of something discovered after one path permits what the other rejects.