Outbound Auth & the Token Vault
The workload-access-token flow, 2LO vs. 3LO, refresh tokens
- Trace the Workload Access Token flow from inbound authorize through token exchange, vault call, user consent, and cached storage
- Distinguish 2LO (Client Credentials / M2M) from 3LO (Authorization Code / user-delegated) and pick the right `auth_flow`
- Configure refresh tokens correctly per provider (Google, Microsoft, GitHub, Slack) and recover from a failed token with `force_authentication`
- Explain how the token vault isolates credentials per-(agent identity + user id) and encrypts them with KMS
- Apply the right IAM for the workload token across the pre- and post-Oct-13-2025 service-linked-role boundary
Outbound auth is how an AgentCore agent calls Google Drive, GitHub, Slack, or an internal API on a user's behalf without ever putting a secret in code, config, or the LLM context. This lesson walks the full Workload Access Token flow end to end, distinguishes 2LO (M2M) from 3LO (user-delegated) and their per-provider refresh-token gotchas, and explains how the token vault enforces strict per-(agent + user) isolation with KMS encryption.
- 1The problem: an agent that calls other people's APIs
- 2The Workload Access Token flow, end to end
- 32LO vs 3LO: as itself, or on behalf of a user
- 4Per-provider refresh-token gotchas
- 5The token vault: what it holds and how it isolates
- 6IAM for the workload token: the Oct-13-2025 boundary
The problem: an agent that calls other people's APIs
Your agent needs to read the user's Google Drive, open a GitHub issue, or post to a Slack channel. The naive approaches all fail in production:
- Hard-code an API key. Now every user shares one identity, the secret lives in your image, and it leaks into logs and the LLM context the moment something gets printed.
- Pass the user's OAuth token in the prompt. The token is now in plaintext in the model's context window — exfiltratable by prompt injection, logged by observability, and impossible to scope or revoke per call.
- Ask the user to paste credentials. Consent fatigue, no refresh, and you are storing other people's tokens yourself.
Outbound Auth (the egress side of AgentCore Identity) solves this. The agent never holds a long-lived secret. Instead it proves who it is acting for, and AgentCore's token vault hands back a short-lived downstream token — fetched, refreshed, and isolated by the service. The raw token is injected as a function argument, not into the model.
Teaching framing from the dossier: Inbound = the bouncer at the door. Outbound = the valet who fetches the user's keys from a locked vault. Identity makes agentic auth delegated, not impersonated-with-a-shared-secret.
Two problems get solved at once: the agent reaches downstream resources (Google Drive, GitHub, Slack, internal APIs, other MCP servers) on behalf of the user (3LO) or as itself (2LO/M2M), and it does so without any secret in code or in LLM context.
Key insight
Delegated, not impersonated
The whole design goal is that the agent acts with the user's delegated authority and a short-lived token, instead of as the user with a shared secret. That is what lets you scope, audit (CloudTrail), revoke, and isolate per user — none of which a baked-in API key gives you.
The Workload Access Token flow, end to end
A Workload Identity is auto-created with every runtime (ARN shape arn:aws:bedrock-agentcore:<region>:<account>:workload-identity/directory/default/workload-identity/<agent-name>). It is the agent's first-class identity — the thing the vault keys credentials against. The end-to-end outbound flow has five steps:
- Inbound: the Runtime authorizes the inbound token (a JWT, or SigV4 for IAM callers). This is the upstream step covered in the inbound-auth lesson.
- Exchange: the Runtime exchanges that inbound token for a Workload Access Token via
bedrock-agentcore:GetWorkloadAccessTokenForJWT(JWT path — it derives user identity from theISS/SUBclaims). The token is delivered in the payload headerWorkloadAccessToken. (SigV4 callers useGetWorkloadAccessToken; the opaque user-id header path usesGetWorkloadAccessTokenForUserId.) - Outbound: the agent uses that Workload Access Token to call the token vault via
bedrock-agentcore:GetResourceOauth2Token. For 3LO, the vault generates the provider's authorization URL. - Consent: the agent surfaces the URL (via the
on_auth_urlcallback); the user grants consent in their browser. - Storage: Identity caches the downstream token in the vault, keyed by (agent workload identity + user id), so future calls skip the consent step until the token expires.
Under the hood the decorator orchestrates three APIs: CreateWorkloadIdentity, GetWorkloadAccessToken, and GetResourceOauth2Token. You rarely call them directly — the SDK decorator runs the whole sequence and injects the resulting token as a kwarg:
from bedrock_agentcore.identity.auth import requires_access_token
@requires_access_token(
provider_name="google-provider",
scopes=["https://www.googleapis.com/auth/drive.metadata.readonly"],
auth_flow="USER_FEDERATION", # 3LO — user-delegated
on_auth_url=lambda url: print("Authorize:\n" + url),
force_authentication=False,
callback_url="<oauth2_callback_url>",
)
async def read_from_google_drive(*, access_token: str):
# access_token is injected here — it never enters the model's context
...The key property: the raw token is delivered straight into your business logic as the access_token argument and never touches the LLM context.
Note
Step 2 picks the API by how the caller authenticated
JWT inbound -> GetWorkloadAccessTokenForJWT (user identity from ISS/SUB). SigV4 inbound -> GetWorkloadAccessToken. The opaque X-Amzn-Bedrock-AgentCore-Runtime-User-Id header path -> GetWorkloadAccessTokenForUserId. They all produce the same WorkloadAccessToken that step 3 uses against the vault.
Tip
Don't print the consent URL in production
The example uses on_auth_url=lambda url: print(...) for clarity. In a real runtime you should stream the consent URL back to the calling client (e.g. as part of your response payload), not write it to stdout where no user will see it.
2LO vs 3LO: as itself, or on behalf of a user
Outbound auth comes in exactly two legged-OAuth shapes, and the only knob you flip is the decorator's auth_flow:
| 2LO | 3LO | |
|---|---|---|
| Name | Client Credentials / M2M | Authorization Code |
| Who the agent acts as | Itself | On behalf of a consenting user |
| SDK value | auth_flow="M2M" | auth_flow="USER_FEDERATION" |
| User consent URL? | No | Yes (via on_auth_url) |
| Typical use | Service-to-service, an internal API the agent owns | Google Drive / GitHub / Slack as the end user |
2LO (Client Credentials / M2M). The agent authenticates as itself — there is no end user in the loop. Pick this for machine-to-machine calls where the downstream resource trusts the agent's own identity.
@requires_access_token(
provider_name="internal-api-provider",
scopes=["orders:read"],
auth_flow="M2M", # 2LO — agent acts as itself, no consent URL
)
async def fetch_orders(*, access_token: str):
...3LO (Authorization Code). The agent acts on behalf of a user who consents via an authorization URL. This is the flow for user-scoped data in Google, GitHub, Slack, and similar. Crucially, AgentCore stores both the access token and the refresh token and auto-refreshes them, which is what minimizes consent fatigue — the user grants access once and subsequent calls reuse the cached, refreshed token until it can no longer be renewed.
The selection is per-decorated-function, so one agent can do M2M to an internal API and USER_FEDERATION to Google within the same codebase.
Key insight
Refresh is the whole point of 3LO
Because AgentCore stores the refresh token and renews silently, the user sees the consent screen once, not on every call. That only works if the provider actually issued a refresh token in the first place — which is exactly the per-provider configuration trap covered next.
Per-provider refresh-token gotchas
3LO only minimizes consent fatigue if the provider issues a refresh token. Most OAuth providers do not hand one out by default — you have to opt in, and the switch is different for every vendor. These settings are volatile (providers change their consoles); treat the list as a starting point and verify against the live provider docs and the AgentCore resource-providers page.
| Provider | What to set (as of writing — verify live) |
|---|---|
"access_type": "offline" in customParameters | |
| Microsoft Entra | request the offline_access scope |
| Salesforce | request the refresh_token scope |
| Atlassian / Jira | request the offline_access scope |
| GitHub | enable "User-to-server token expiration" in the app |
| Slack | enable "token rotation" |
| enable refresh in the app config |
For Google, the offline access-type rides along in the decorator's custom_parameters:
@requires_access_token(
provider_name="google-provider",
scopes=["https://www.googleapis.com/auth/drive.metadata.readonly"],
auth_flow="USER_FEDERATION",
custom_parameters={"access_type": "offline"}, # Google must be told to issue a refresh token
on_auth_url=lambda url: stream_to_client(url),
)
async def read_from_google_drive(*, access_token: str):
...Tokens are not guaranteed to stay valid. A provider can revoke a token (user changed password, admin pulled the app, rotation kicked in). When a cached token fails, force a fresh consent by setting force_authentication=True, which bypasses the cache and re-runs the authorization flow:
@requires_access_token(
provider_name="google-provider",
scopes=["https://www.googleapis.com/auth/drive.metadata.readonly"],
auth_flow="USER_FEDERATION",
force_authentication=True, # bypass the cached/expired token, re-prompt for consent
on_auth_url=lambda url: stream_to_client(url),
)
async def read_from_google_drive(*, access_token: str):
...Watch out
These switches change — verify the live page
Provider OAuth consoles and AgentCore's built-in vendor configs move frequently. The names above are accurate as of writing, but always confirm against the provider's own docs and the AWS resource-providers page before shipping. Treat 'no refresh token issued' as the most common cause of repeated consent prompts.
Example
Symptom -> cause
Users complain the agent re-asks for Google consent every session. Almost always: you didn't pass access_type=offline, so Google never issued a refresh token, so AgentCore has nothing to refresh and falls back to a fresh authorization each time.
The token vault: what it holds and how it isolates
The token vault is the secure credential store at the center of Outbound Auth. It holds:
- OAuth access tokens and refresh tokens
- OAuth client credentials
- API keys (static keys for services like OpenAI, fetched with the
@requires_api_keydecorator)
Three properties make it production-grade:
- Encryption everywhere. Credentials are encrypted at rest and in transit using AWS KMS — either a service-managed key or your own customer-managed key (CMK) if you need control over the key policy and rotation.
- Strict per-(agent identity + user id) isolation. Every credential is keyed to a specific (agent workload identity, user id) pair, giving zero token sharing across agents or across users. User A's Google token is unreachable by user B, and your billing agent's tokens are unreachable by your support agent.
- Independent validation. Every access request to the vault is validated on its own — there is no ambient "already authenticated" trust.
And the cardinal rule that ties it all together: tokens are never embedded in code or config, and never exposed to the LLM context. The decorator pulls the credential from the vault and injects it directly into your function as the access_token (or api_key) argument; it does not flow through the prompt.
Agent --GetResourceOauth2Token--> Token Vault (KMS-encrypted)
| keyed by (agent identity + user id)
<--access_token injected as kwarg----+ zero token sharingBecause isolation is per-(agent + user), the same physical vault safely serves a multi-tenant agent: each user's downstream tokens are partitioned, and revoking or rotating one user's grant never touches another's.
Tip
Use a CMK when you need key governance
Service-managed KMS is the zero-config default. Choose a customer-managed key (CMK) when compliance requires that you own the key policy, control rotation, or be able to disable the key to cut off access to all stored credentials at once.
IAM for the workload token: the Oct-13-2025 boundary
How the agent's execution role is allowed to fetch the Workload Access Token changed at GA, and this is a volatile fact tied to a specific date — verify the live runtime-oauth page before relying on it.
Agents created before Oct 13 2025 need the workload-token actions granted explicitly in the execution role:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetWorkloadAccessToken",
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
],
"Resource": [
"arn:aws:bedrock-agentcore:<region>:<account>:workload-identity-directory/default",
"arn:aws:bedrock-agentcore:<region>:<account>:workload-identity-directory/default/workload-identity/agentname-*"
]
}]
}Agents created on or after Oct 13 2025 rely instead on a Service-Linked Role (SLR) named AWSServiceRoleForBedrockAgentCoreRuntimeIdentity (trusted principal runtime-identity.bedrock-agentcore.amazonaws.com). You no longer hand-write the GetWorkloadAccessToken* grants — the SLR carries them. The caller creating the runtime via the control API just needs iam:CreateServiceLinkedRole for that service so the role can be provisioned.
Keep the two planes straight: IAM governs AWS-level access — the control/data-plane APIs, credential-provider creation, KMS, and the execution role (SigV4 is one inbound option). Identity governs the agent/workload identity — the directory, the token vault, and OAuth 2LO/3LO to non-AWS services. The key IAM actions you will see across outbound auth are GetWorkloadAccessToken* (the exchange) and GetResourceOauth2Token (the vault call); InvokeAgentRuntime / InvokeAgentRuntimeForUser sit on the inbound side.
Watch out
This is date- and version-sensitive — re-verify
The pre/post Oct-13-2025 split and the SLR name are accurate as of writing but are exactly the kind of fact AWS updates. Confirm against the live runtime-oauth and runtime-permissions docs before granting (or removing) the explicit GetWorkloadAccessToken* actions.
Try it: Wire a 3LO Google Drive call through the token vault
Goal: build an agent function that reads a user's Google Drive via 3LO, proves the token never enters the LLM context, and demonstrates the refresh and recovery switches.
- Register an outbound credential provider. Using
bedrock-agentcore-control, create a Google OAuth2 provider and capture the returnedcallbackUrl:Register the returnedbashaws bedrock-agentcore-control create-oauth2-credential-provider --region us-east-1 \ --name "google-provider" --credential-provider-vendor "GoogleOauth2" \ --oauth2-provider-config-input '{"googleOauth2ProviderConfig":{"clientId":"...","clientSecret":"..."}}'https://bedrock-agentcore.<region>.amazonaws.com/identities/oauth2/callback/<uuid>URL in Google's Authorized redirect URIs (forgetting this breaks the 3LO redirect). - Decorate a function for 3LO. Add a
@requires_access_token(...)withauth_flow="USER_FEDERATION", a Drive read-only scope,custom_parameters={"access_type": "offline"}, and anon_auth_urlthat streams the URL back to your client (notprint). Make the function take*, access_token: str. - Run the consent once. Invoke the agent, open the surfaced authorization URL, grant consent, and confirm the Drive call succeeds. Then invoke a second time and confirm there is no consent prompt — the cached, refreshed token is reused.
- Prove the isolation contract. Add a log line inside your function that prints the length of
access_token(never the value). Confirm the raw token appears nowhere in the prompt/response payload — only inside your business logic. - Break and recover. Revoke the app's access in your Google account, invoke again, and observe the failure. Flip the decorator to
force_authentication=True, re-invoke, and confirm it re-runs consent and recovers. - Reflect (3 sentences). Where would you switch this same agent to
auth_flow="M2M"instead? What changes in IAM if this agent was created after Oct 13 2025 versus before? And why does omittingaccess_type=offlinecause repeated consent prompts?
Key takeaways
- 1Outbound Auth lets an agent call downstream APIs (Google, GitHub, Slack, internal services, other MCP servers) with no secret in code, config, or the LLM context — the token is injected as a function kwarg.
- 2The Workload Access Token flow is: inbound authorize -> exchange via `GetWorkloadAccessTokenForJWT` (delivered in the `WorkloadAccessToken` header) -> call the vault with `GetResourceOauth2Token` -> user consents via `on_auth_url` -> token cached keyed by (agent identity + user id).
- 32LO = Client Credentials = M2M (`auth_flow="M2M"`, agent acts as itself, no consent URL); 3LO = Authorization Code (`auth_flow="USER_FEDERATION"`, user-delegated). AgentCore stores and auto-refreshes 3LO tokens.
- 4Refresh tokens are opt-in per provider and the setting is volatile: Google `access_type=offline`, Microsoft `offline_access` scope, GitHub user-to-server token expiration, Slack token rotation. When a cached token fails, set `force_authentication=True`.
- 5The token vault holds OAuth tokens, client credentials, and API keys; encrypts them at rest and in transit with KMS (service-managed or CMK); and enforces strict per-(agent + user) isolation = zero token sharing.
- 6Workload-token IAM is date-sensitive (volatile): agents before Oct 13 2025 need `GetWorkloadAccessToken*` in the execution role; on/after that date they use the SLR `AWSServiceRoleForBedrockAgentCoreRuntimeIdentity`.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.In the Workload Access Token flow, what does the agent do immediately after the Runtime exchanges the inbound JWT for a Workload Access Token?
2.Your agent needs to read a user's Google Drive on that user's behalf. Which decorator configuration is correct?
3.Which statement about the token vault is accurate?
4.You create a brand-new AgentCore agent after Oct 13 2025 and it gets a permission error fetching its workload token. What is the most likely fix?
Go deeper
Hand-picked sources to keep learning
The Workload Access Token flow, 2LO/3LO, and the pre/post-Oct-13-2025 workload-token IAM and service-linked-role details. Verify the volatile IAM facts here.
Built-in OAuth vendors and the per-provider refresh-token configuration (Google offline, Microsoft offline_access, GitHub, Slack). Check here before shipping a 3LO integration.
AWS Security Blog deep dive on the token vault, KMS encryption, per-(agent + user) isolation, and delegated vs impersonated access.
Execution-role policies and the GetWorkloadAccessToken* / GetResourceOauth2Token actions referenced in the IAM section.
Source for `@requires_access_token` / `@requires_api_key` (in `bedrock_agentcore.identity.auth`) and the lower-level `IdentityClient`. Pin to a verified version.
Identity is no additional charge through Runtime/Gateway; otherwise per OAuth-token / API-key request. Pricing is volatile — confirm on the live page.