AWS Builder Center
One AgentCore Gateway, Two Agents, Per-User Tools

One AgentCore Gateway, Two Agents, Per-User Tools

Point a server-hosted AgentCore agent and a local Claude Code agent at one OIDC gateway, then gate tools by the signed-in user's tier and profile.

One AgentCore Gateway, Two Agents, Per-User Tools

Say you've built a set of MCP tools behind an  Amazon Bedrock AgentCore Gateway , and two very different things need to call them:
  1. A server-hosted agent running in  AgentCore Runtime  — it answers requests inside your product, on behalf of whichever customer is signed in.
  1. A local  Claude Code  agent running on a developer's laptop — same tools, but the developer wants them in their IDE.
And you want two more things on top of that:
  • Tier gating. A  free  user sees a smaller tool set than an  enterprise  user. The gate has to be enforced server-side, not just hidden in a UI.
  • Per-profile calls. When any tool runs, it should act on that user's data — their records, their tenant — not a shared identity.
The instinct is to build two auth paths and two policy layers, one per client. You don't have to. Both clients speak the same OAuth/OIDC flow to the same gateway, and the gateway makes every decision off one thing: the JWT in the request. This post is about why that collapses into a single, clean design.

The one idea: identity is the token, not the client

An AgentCore Gateway configured for JWT auth is an  OAuth resource server . It validates an access token against your identity provider's  OIDC discovery document  — signature, issuer, audience, expiry — and then lets the request through or rejects it. It never issues tokens, and it does not care who handed it the token. A laptop, a Lambda, a container in Runtime — all identical, as long as the  Authorization: Bearer <token>  header holds a valid JWT.
That's the whole trick. The client is plumbing. The token carries the identity, and every downstream decision — which tier, whose data — reads off the token's claims. So instead of "two clients, two auth designs," you have "one token format, two ways of obtaining it."
This diagram shows how to authenticate with the gateway

Let me take the two clients one at a time, then the gate, then the per-profile part.

Client 1: Claude Code over PKCE

Claude Code is a public client — a CLI on a laptop with no secret to protect. It runs the OAuth 2.1 authorization-code flow with  PKCE (RFC 7636)  directly against your IdP: it discovers the authorization server from the gateway's  Protected Resource Metadata (RFC 9728) , opens a browser sign-in, and stores the resulting token in the OS keychain, refreshing it automatically.
Configuration is a small block in your MCP config — a public client ID and a registered callback port, no secret:
JSON
{
"mcpServers": {
"gateway": {
"type": "http",
"url": "https://<gateway-id>.gateway.bedrock-agentcore.<region>.amazonaws.com/mcp",
"oauth": { "clientId": "<public-app-client-id>", "callbackPort": 8080 }
}
}
}
Run  /mcp , pick the server, choose Authenticate, sign in once, and Claude Code is talking to a locked-down gateway with a token it manages for you. I walked through this leg in depth — the discovery chain, the  mcp-remote  comparison, where the token lives — in  OIDC Auth from Claude Code to an AgentCore Gateway . The important thing here: the token in that keychain is the developer's own identity, and it's the same shape of token the server-hosted agent will present.

Client 2: the Runtime-hosted agent — no browser to click

A server-hosted agent can't pop a browser. There's no human at the container to complete a sign-in. So it obtains the user's token a different way — and there are two patterns, depending on how much isolation you need.
Identity passthrough. The user is already signed in to your application; your app already holds their access token. When the app invokes the Runtime agent, it relays that token in the invocation payload, and the agent attaches it verbatim as the  Authorization: Bearer  header on its outbound MCP calls to the gateway. The agent never mints an identity of its own — it borrows the caller's. The gateway sees the same user it would have seen if that user had called directly. This is the simplest path and it's enough when your app and the gateway trust the same IdP and audience.
On-behalf-of (OBO) token exchange. For real multi-tenant isolation — where the agent needs a token scoped to a specific tenant and the downstream may trust a different authorization server — you exchange the inbound token for a fresh, narrowly-scoped one rather than passing it through. AWS documents this pattern end-to-end in  Implement on-behalf-of token exchange for multi-tenant agents with AgentCore Gateway , using  AgentCore Identity  and its token vault to broker the exchange. The gateway still just validates whatever JWT arrives — the exchange happens before the call.
Either way, the outbound leg looks like Claude Code's: an HTTP MCP connection to the same gateway endpoint, carrying a per-user bearer token.
jsonc
// what the Runtime agent hands its MCP client for the gateway
"gateway": {
"type": "http",
"url": "https://<gateway-id>.gateway.bedrock-agentcore.<region>.amazonaws.com/mcp",
"headers": { "Authorization": "Bearer <the signed-in user's access token>" }
}
Send the access token, not the ID token. A JWT-authorized gateway expects the OAuth access token as the bearer. Presenting an ID token is a common mistake — it's meant for the client, not for calling a resource server — and the gateway will reject it. Claude Code sends the access token by default; make sure your Runtime agent relays the access token too, not the ID token.

The gate: turning a claim into a tier

Now both clients present a valid, per-user JWT. Gating tools by tier means reading a claim off that token and letting it decide which tools are callable. Model each subscription tier as a group in your IdP ( free ,  pro ,  enterprise ), put each user in the group matching their plan, and the tier rides in every token automatically as a groups claim — no client-side configuration, and neither client can forge it because the IdP signs the token.
AgentCore gives you a few places to enforce it, from simplest to most granular:
  • Required custom claims at the authorizer. The  JWT authorizer  can require specific claim values ( CustomClaimValidationType ) before any call reaches the gateway. Good for a coarse "must be a paying tier" gate.
  • A gateway interceptor. For per-tool decisions, a  gateway interceptor  inspects the inbound JWT on each  CallTool , checks the caller's groups/scopes against the specific tool being invoked, and allows or denies — while still letting tool discovery ( ListTools ) stay open. This is where "the  enterprise  tier can call  RunForecast , the  free  tier can't" lives.
  • An attached policy engine. For declarative, auditable rules you can offload authorization to a policy engine such as  Cedar , keyed off the JWT claims. The gateway supports this  offloaded authorization  model, and it's the same "centralized Cedar enforcement" pattern AWS calls out for  gateway-fronted resources . Rules read like  permit(... when principal has "enterprise" in groups) , DENY beats ALLOW, and anything without a matching permit is denied by default.
The point that ties it back to the top: this gate is written once and is client-agnostic. The developer in Claude Code and the customer being served by the Runtime agent hit the exact same rule set, because the gate reads the token, and both of them presented a token in the same format. A tool the caller's tier isn't granted simply isn't callable — the gateway returns a policy-denied error before the target ever runs, not a soft UI hide.

Per-profile calls: the claims travel all the way down

Tier gating decides whether a call is allowed. The second half — making the call act on the right user's data — falls out of the same token. When the gateway validates a JWT it makes the caller's claims available as authorization context, so both the policy layer and the downstream target can see who's calling: the subject ( sub ), email, tenant, groups.
That means a tool doesn't need — and shouldn't take — a  userId  argument from the model. A  ListMyRecords  tool reads the  sub  (or a tenant claim) from the request's identity context and scopes its query to that user. Two different people calling the identical tool through the identical gateway get their own data, because the identity was proven by the IdP and carried in the token, not asserted by the agent. It also closes an injection hole: the model can't ask for someone else's records by passing a different ID, because the ID it would pass is ignored in favor of the verified claim.
So a single tool definition serves everyone, and its behavior is personalized by the token — the same way its availability is gated by the token.

Why this shape holds up

  • One validation point. Every client — Claude Code, a Runtime agent, a CI job — presents a JWT and the gateway checks it the same way. Your tool backend never sees an unauthenticated or unattributed request.
  • No secrets on the edge. The laptop uses PKCE (nothing to leak from the config file); the server relays or exchanges a token it already holds. Neither stores a long-lived client secret.
  • The tier can't be spoofed. It's a signed claim from your IdP. A user can't promote themselves by editing a config or a tool argument.
  • You reuse the IdP you already run. The gateway is IdP-agnostic — Cognito, Okta, Auth0, Entra ID — anything exposing an OIDC discovery endpoint validates the same way.

Takeaways

  • An AgentCore Gateway is a resource server: it validates JWTs and gates on their claims. The client that obtained the token is irrelevant.
  • A local Claude Code agent gets its token via browser PKCE; a Runtime-hosted agent gets the user's token via identity passthrough or OBO exchange. Both arrive as a per-user bearer token at the same endpoint.
  • Model tiers as IdP groups, then enforce per-tool with a custom-claim rule, an interceptor, or an attached policy engine — written once, applied to both clients.
  • Personalize by claim, not by argument. Read  sub /tenant from the verified identity context so each call acts on the caller's own profile — and send the access token, not the ID token.

Related reading

Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article