Hightower's AI Harness Engineering

Hightower's AI Harness Engineering

AgentCore IX: Per-User Agent Auth: The Model Cannot Leak a Token It Never Held

Part 9: AgentCore Identity keeps OAuth tokens out of the model by injecting them into your tool function at call time; the real failure mode is wiring an unverified user-id header from the browser.

Rick Hightower's avatar
Rick Hightower
Jul 31, 2026
∙ Paid

Per-User Agent Auth: The Model Cannot Leak a Token It Never Held

AgentCore Identity keeps OAuth tokens out of the model by injecting them into your tool function at call time; the real failure mode is wiring an unverified user-id header from the browser.

Cover image for “Per-User Agent Auth: The Model Cannot Leak a Token It Never Held” by Rick Hightower

Your agent still authenticates as one shared service principal, so entitlements and audit collapse into “the agent did it.” The model cannot leak a token it never held. AgentCore Identity injects credentials into your tool function at call time. Secrets stay out of the context window if you assemble the plumbing correctly.

In this article: You will learn how AgentCore Identity separates workload identity from user identity, how requires_access_token injects OAuth credentials into plain Python without exposing them to the model, how the 3LO wire flow and token vault work, and which three gotchas (unverified user-id header, local vs deployed context, stale vault tokens) break production auth. By the end, you will know how to give each analyst their own entitlements without putting a secret where a prompt injection can reach it.


AgentCore Part 11

Share


AgentCore Identity injects credentials into your tool function at call time. Secrets stay out of the context window if you assemble the plumbing correctly.

Your multi-user agent still hits the market-data provider as one service principal: same token, same entitlements, every session is “the agent.”

Entitlements become “we trust the agent.” Audit becomes “the agent did it.” Asked to prove Dana could not see Sam’s licensed data, you shrug.

This is plumbing. It still holds the cleanest sentence in the series:

The model cannot leak a token it never held.

Secrets must never enter the context window. The agent needs an authenticated API, and you do not trust the agent. Naive solutions put the credential in the prompt, a tool argument, or a tool result. Whatever the model can see, a crafted page can ask it to repeat.

AgentCore Identity injects the credential into your Python function at call time as a keyword argument. The model asks for the tool; your code gets the token; the two never meet.

Three primitives

AgentCore Identity rests on workload identity, a token vault keyed by agent-plus-user, and a clean inbound versus outbound auth split.

Workload identity. Not an IAM role or Cognito user: a separate identity with its own ARN for “who is the agent,” independent of “who is the user.” Auto-provisioned with AgentCore Runtime so credentials can bind to the pair (this agent, this user).

Token vault. OAuth tokens keyed by that pair. Default: AWS-managed KMS. If you need key ownership (financial services, healthcare, public sector), bind a customer-managed key at runtime creation time.

Inbound vs outbound. Inbound who may invoke: IAM SigV4 (service-to-service default) or JWT via OIDC (human path). Mutually exclusive per runtime; need both, use different runtime versions. Outbound what the agent may reach: 2LO (machine credentials as the agent) or 3LO (on behalf of a human).

Seat-licensed market data: JWT inbound, 3LO outbound. Dana gets Dana’s entitlements.

The decorator

This listing configures 3LO OAuth for a market-data provider and injects the access token into a plain async function as a keyword argument at call time.

from bedrock_agentcore.identity import requires_access_token

@requires_access_token(
    provider_name="market-data-provider",
    scopes=["quotes.read", "filings.read"],
    into="access_token",              # injected as a kwarg  ①
    auth_flow="USER_FEDERATION",      # 3LO  ②
    on_auth_url=lambda url: auth_url_holder.update({"url": url, "needs_auth": True}),  # ③
    callback_url="https://your-app.example.com/oauth/callback",
    force_authentication=False,       # cache the token  ④
)
async def _fetch_quotes(symbol: str, access_token: str) -> str:  # ⑤
    async with httpx.AsyncClient() as http:
        r = await http.get(
            f"https://api.marketdata.example.com/v1/quotes/{symbol}",
            headers={"Authorization": f"Bearer {access_token}"},  # ⑥
        )
        return r.text

① into names the keyword argument the decorator injects; no caller ever passes it.

② USER_FEDERATION selects the 3LO flow so the agent acts on behalf of a specific human.

③ On first consent, the vault returns an auth URL; this callback surfaces it to the analyst.

④ With force authentication off, the vault reuses a cached token until it expires.

⑤ The signature advertises access_token, but only the decorator supplies it.

⑥ The token is used only inside the HTTP call and is never returned into the model context.

Note: The full extracted listing at code/agent-core/part-09-identity-per-user-auth/listings/01-requires-access-token.py shows the imports and auth_url_holder stand-in elided here.

_fetch_quotes takes an access_token no caller ever passes. No secret in the tool schema or model context. Nothing for prompt injection to extract.


If you are a paid subscriber, thank you. Your support makes this work possible.

If you are a free subscriber and find these articles useful, please consider upgrading. A paid subscription is $80 per year or $8 per month.

Free subscribers typically receive access to the full versions of paid articles after one to two months.

Share Hightower's AI Harness Engineering

User's avatar

Continue reading this post for free, courtesy of Rick Hightower.

Or purchase a paid subscription.
© 2026 Rick Hightower · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture