Exam Room · Advanced GenAI

Giving an Agent Credentials Without a Standing Key

August 16, 2026 · 34 min read

Generative AI Development · part of The Exam Room

The situation

The subscriber help desk agent has to reach four things. An internal billing API, to check whether a charge was correct. An internal delivery API, to read the schedule for a postcode. A third-party routing service, which authenticates with an API key. And, for the subscribers who opted into it, their calendar, so the assistant can suggest delivery windows that miss their meetings.

Right now three of those are one long-lived key each, read from the agent’s environment. It works, and it has the property nobody wants to say out loud: every request the agent makes to the billing API carries the same credential, whether it is acting for the subscriber who asked or for one it was talked into acting for. The calendar is not wired up at all, because nobody could see how to do it without asking subscribers to hand over a password.

The team wants to fix both problems at once. The agent should be able to reach what it needs, each call should carry the identity of the subscriber it is acting for rather than a shared identity, and no credential should live in the agent’s environment. What they have to work out is which mechanism covers which of the four, because the four are not the same shape.

What actually matters

The first thing to separate is two questions that get conflated. Who is asking? is settled before your agent code runs, by validating the token the caller presented. What can the agent call downstream, and as whom? is a different question with different machinery. Conflating them produces the specific bug where an agent trusts a subscriber id because it arrived in the same request as a valid token, without anything having checked that the token actually says that subscriber.

The second is that the identity of the caller has to be established by something that verifies, not something that stores. A credential store is a place to keep secrets safely; it does not tell you whose secret to fetch. That comes from validating the inbound token against the issuer’s published keys and checking the claims that say who it was minted for and which application obtained it. Everything downstream inherits its trustworthiness from that check, so it is the part to get exactly right.

The third is that the four calls genuinely differ in whose authority they need. Reading a postcode’s delivery schedule is the same for everyone and needs no user at all. Reading a subscriber’s calendar needs that subscriber’s explicit permission, given once, to a third party that has never heard of your agent. Checking a charge on a subscriber’s account needs their identity to travel with the call, but not their consent, because they are already talking to you about it. Treating all three as the same problem is what produces one shared key.

The fourth is expiry and refresh, which is where hand-rolled versions rot. A user-delegated token expires, and the difference between a system that survives that and one that starts failing at three in the morning is whether refresh is somebody’s code or somebody’s service. The same applies to the consent itself: a subscriber should be asked once, not on every request, which means the token has to be stored somewhere that survives the session.

Underneath all of it: the blast radius question. If the agent misbehaves, whether through a bug or through a prompt-injection attempt in a tool call, what can it reach? A shared standing key means the answer is everything that key opens, for every subscriber. A per-subscriber credential means the answer is bounded by whoever the request was actually for.

What we’ll filter on

  1. Whose identity does the downstream call carry: the agent’s, the subscriber’s, or both?
  2. Does the subscriber have to consent, and are they asked once or repeatedly?
  3. Where does the credential live, and who is allowed to retrieve it?
  4. Does the mechanism work for the target you actually have?
  5. What happens when the token expires: whose code refreshes it?
  6. What is reachable if the agent is manipulated into acting for the wrong person?

The landscape

A standing key in the environment

One long-lived credential per downstream service, read from an environment variable or a secret at start-up, used for every request. It is the shape the team already has and the one to design away from.

Its failure is that the credential carries no information about who the request is for. Every call to the billing API looks identical whether the agent is serving the subscriber who asked or one an injected instruction named, so the downstream service cannot make an authorisation decision and the audit trail records the agent rather than the person. Rotation is manual, revocation is all-or-nothing, and the blast radius of any mistake is the full scope of the key.

The inbound JWT authorizer

Not a credential mechanism at all, and the prerequisite for every one that follows. Configured on the runtime or the gateway, it validates the token the caller presents before your code sees the request. It fetches the issuer’s public keys from an OIDC discovery URL, so it works with any OAuth 2.0 provider without onboarding each one, and then checks what you tell it to check.

The checks available are aud, so a token minted for a different API cannot be replayed at yours; client_id, so only registered applications get in; scopes, where at least one must match; and required custom claims, matched with EQUALS, CONTAINS, or CONTAINS_ANY, which is how a rule like “group must equal Developer” is expressed. At least one of these must be configured, and where several are, all are verified.

This is what makes a subscriber id trustworthy. Everything below inherits from it.

Workload identity and the token vault

The agent gets its own identity rather than borrowing a user’s. Agent identities are workload identities in a directory that works like a Cognito user pool, each with an ARN of the form arn:aws:bedrock-agentcore:region:account:workload-identity/directory/default/workload-identity/agent-name, so policies can be applied across a group of agents rather than one at a time. The agent authenticates as itself and carries user context alongside, which is delegation rather than impersonation.

The token vault is where credentials live: OAuth tokens, OAuth client secrets, and API keys, encrypted at rest and in transit with a customer-managed or service-managed KMS key. Its access rule is the part worth memorising. A credential is retrievable only by an agent that presents verifiable proof of its workload identity, and only for the agent and user combination that obtained it. Every retrieval is validated independently, including from callers inside the same trust domain, which is the protection against agent code that has gone wrong rather than against an outside attacker.

Two-legged OAuth, for machine-to-machine calls

The client credentials grant. The agent authenticates as itself against the resource server, no user involved, and gets a token scoped to what the agent is allowed to do. Right for the delivery API, where a postcode’s schedule is the same regardless of who asked.

Nothing about it is per-subscriber, which is exactly why it suits the calls that are not.

Three-legged OAuth, for user-delegated access

The authorization code grant, and the answer to the calendar. The subscriber consents once, in a browser, to your agent reaching their calendar, and the resulting token is vaulted against that agent-and-subscriber pair. Later requests for that subscriber use the stored token without asking again.

In the SDK this is a decorator rather than a flow you implement: @requires_access_token with auth_flow='USER_FEDERATION'. It checks the vault for a live token, and where there is not one it generates an authorisation URL and hands it to your application through an on_auth_url callback, which is how your front end knows to put the consent screen in front of the subscriber. The code exchange and the vaulting happen for you, as does refresh when the token ages out. Built-in providers ship for Google, GitHub, Slack, Salesforce, and Atlassian with the endpoints pre-filled; anything else is a custom provider you configure once.

On-behalf-of token exchange

For the calls where the subscriber’s identity has to travel but their consent is not the question, because they are already in a session with you. The inbound user token is exchanged for a new, scoped token addressed to a specific downstream service, and that token carries both the subscriber’s identity and the agent’s.

No consent screen appears, because no new permission is being granted; an existing authenticated session is being narrowed and passed along. The far-end service can then authorise on both identities at once, which is what lets the billing API answer “is this agent allowed to do this, and is it allowed to do it for this person” as one decision.

API key credential providers

Some services have no OAuth at all. An API key credential provider stores the key in the vault, records where it belongs (header or query parameter, and any prefix such as Bearer), and hands it over at call time, with @requires_api_key as the SDK equivalent of the token decorator. The key stops living in the agent’s environment, which is most of the benefit, though it stays a shared credential and carries no user identity.

Where the gateway constrains the choice

The mechanism you can use is limited by what kind of target the tool is, and this catches people out. A Lambda gateway target is always invoked with the gateway service role: no OAuth, no API key, no forwarding of the caller’s token. Three-legged OAuth and on-behalf-of exchange are available to OpenAPI and MCP-server targets, and caller IAM credentials and token passthrough only to AgentCore Runtime targets.

Where a tool must act as the subscriber and its target cannot carry a user credential, the remaining option is a REQUEST interceptor that reads the validated claim and writes the subscriber id into the tool arguments before the call is forwarded. That is injected context rather than a credential, so the target is trusting the gateway rather than verifying for itself.

Evaluation

Side by side

Mechanism Carries user identity Consent needed Credential location Refresh Blast radius if misused
Standing key in the environment The agent’s process Manual Everything the key opens, for everyone
2LO client credentials Token vault Managed The agent’s own scope
3LO authorization code ✓ (once) Token vault, per agent+user Managed One subscriber’s account at that provider
On-behalf-of exchange ✓ (plus the agent’s) Derived per request Re-exchanged One subscriber, one downstream audience
API key provider Token vault Manual rotation Everything the key opens
Interceptor-injected id ✓ (as data) n/a n/a Bounded by the target’s own checks

Reading it for the help desk: no single row covers all four calls, which is the finding rather than a failure of the table. The delivery API wants the row with no user in it, the calendar wants the one with consent, the billing API wants the exchange, and the routing service wants the key out of the environment and nothing more. What every row except the first has in common is that the credential is not in the agent’s process.

The solution

Configure the inbound authorizer first, then pick a credential mechanism per call rather than one for the agent. The four downstream services differ in whose authority they need, and any answer that treats them alike is the standing key again with extra steps.

Start with the authorizer, because everything else is worthless without it. Point it at the identity provider’s discovery URL and configure the checks: the audience your gateway expects, the client ids of the front ends allowed to call it, and the scope that means “may use the help desk”. Now the subscriber id in a validated token is a fact rather than a claim, and it is the fact every downstream decision rests on.

The delivery API has no subscriber in the question at all: a postcode’s schedule is a postcode’s schedule, so two-legged client credentials fit. Giving the call a user identity it does not need only widens what a mistake could reach. The credential lives in the vault, refresh is handled, and nothing sits in the environment.

The calendar is the opposite case and wants three-legged OAuth. Use the built-in Google provider so the endpoints come pre-filled, and decorate the tool with @requires_access_token, auth_flow='USER_FEDERATION', and an on_auth_url callback that hands the URL to the chat front end. The first time a subscriber asks about delivery windows they see a consent screen; afterwards they do not, because the token is vaulted against that agent-and-subscriber pair and refreshed for you. A subscriber who never opts in simply has no token in the vault, and the tool fails closed for them rather than falling back to something shared.

Billing goes through an on-behalf-of exchange. The subscriber is already authenticated to you, so a consent screen would be asking permission for something they have just requested. The exchange produces a token scoped to the billing service that carries both identities, and the billing service authorises on both at once. This is also the call where the difference matters most: a manipulated tool call that names another subscriber’s order fails at the billing service, because the token accompanying it says who the session is actually for.

That leaves the routing service, which uses an API key provider, the weakest of the four. It moves the key out of the agent’s environment and into the vault, which is worth doing, and it remains a shared credential carrying no user identity. Scope it as tightly as the vendor allows and rotate it on a schedule, because nothing about the mechanism will tell you when it has leaked.

Then check the target types before you commit, because the gateway will silently narrow your options. A Lambda target gets the gateway service role and nothing else, so any tool that must act as the subscriber belongs behind an OpenAPI or MCP-server target. Where that is not possible, a REQUEST interceptor writing the validated subscriber id into the arguments is the fallback, and it should be recognised as a weaker guarantee: the target is trusting the gateway rather than verifying a token itself.

Why not one mechanism for everything. It is the instinct that produced the current state. Making every call three-legged means asking subscribers to consent to things they are not being asked about; making every call two-legged throws away the user identity that makes downstream authorisation possible.

Why not keep the keys and add checks in the agent. Checks in the agent are checks the agent can be talked out of. The reason to move identity into the credential is that it stops being something the model could get wrong.

Worked example

A subscriber asks: “I was charged after I paused, and can you move Thursday’s box to a day I am not in meetings?”

The request arrives with a bearer token from the chat front end. The inbound authorizer fetches the provider’s keys from the discovery URL, verifies the signature, checks aud against the gateway, checks client_id against the registered front end, and confirms the required scope is present. The subscriber id in that token is now trustworthy. Nothing of the agent’s has run yet.

The billing half uses the exchange. The agent’s tool call to check the charge triggers an on-behalf-of exchange of the inbound token for one addressed to the billing service, carrying both the subscriber’s identity and the agent’s. The billing service reads the charge for that subscriber and confirms it should be reversed. Had an injected instruction named a different subscriber’s order, the call would have arrived with a token saying who the session was for, and the billing service would have refused.

The calendar half uses the vault. The tool is decorated with @requires_access_token, so before it runs the SDK looks for a live Google token for this agent-and-subscriber pair. This subscriber connected their calendar last month, so one is there, refreshed without anybody noticing. The tool reads Thursday and Friday, finds Thursday morning blocked and Friday clear.

The delivery half needs no subscriber at all. Checking which days the van serves that postcode uses the two-legged credential, because the answer is the same for every subscriber on that route.

The agent composes a reply: the charge is being reversed, and Friday is available. Four calls, four different credentials, none of them in the agent’s environment, and three of the four carrying an identity that a manipulated tool call could not have forged.

What’s worth remembering

  1. Establishing who is asking and obtaining something to call downstream with are separate problems; the inbound JWT authorizer answers the first, and it validates signature, audience, client id, scopes, and custom claims against the issuer’s published keys.
  2. The token vault stores credentials, it does not establish identity: its guarantee is that a credential is retrievable only by the agent and user combination that obtained it, and only against verifiable proof of workload identity.
  3. Match the flow to whose authority the call needs: two-legged where no user is involved, three-legged where a third party needs the user’s consent, and on-behalf-of exchange where the user’s identity must travel but their consent is not in question.
  4. On-behalf-of tokens carry both the user’s identity and the agent’s, so the downstream service can authorise on both at once rather than trusting a value in a payload.
  5. The gateway target type constrains the choice: a Lambda target is always invoked with the gateway service role, so a tool that must act as the subscriber belongs behind an OpenAPI or MCP-server target.
  6. A standing key in the environment carries no identity, cannot be revoked for one user, and makes the blast radius of any mistake the full scope of the key.

The help desk ends up with nothing in its environment, one credential mechanism per downstream call chosen on whose authority that call needs, and an inbound check that turns a subscriber id from something the request asserts into something the token proves.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.