Agentic AI AcademyAgentic AI Academy

Credential Providers & SDK Decorators

@requires_access_token, built-in vendors, and the callback gotcha

Intermediate 13 minBuilder
What you'll be able to do
  • Distinguish an inbound identity provider (whose JWTs gate invocation) from an outbound credential provider (config that fetches downstream tokens) and explain why conflating them is the #1 mistake
  • Choose between the built-in OAuth2 vendors (Google, GitHub, Slack, Salesforce, Microsoft, Auth0, CustomOauth2) and API-key providers for static keys like OpenAI
  • Create a credential provider with `aws bedrock-agentcore-control create-oauth2-credential-provider` and register the returned `callbackUrl` to avoid breaking the 3LO redirect
  • Apply `@requires_access_token` and `@requires_api_key` to inject a credential as a kwarg so the raw token never touches business logic or the LLM context
  • Stream the consent URL back to the caller via `on_auth_url` in production instead of printing it, and explain how Identity backs Gateway outbound auth
At a glance

Outbound auth is where agentic security gets practical: how does your agent get a Google Drive or GitHub token without secrets in code or in LLM context? This lesson untangles the two things people constantly conflate — the inbound IdP that gates who can invoke your agent versus the outbound credential provider that fetches downstream tokens — then walks the built-in OAuth2 vendors, the `CreateOauth2CredentialProvider` / `CreateApiKeyCredentialProvider` control APIs, the callback-URL gotcha that silently breaks 3LO, and the `@requires_access_token` / `@requires_api_key` SDK decorators that run the whole flow and inject the credential as a kwarg.

  1. 1Two sides of one coin: inbound IdP vs outbound credential provider
  2. 2Built-in OAuth2 vendors and API-key providers
  3. 3Creating a provider with bedrock-agentcore-control
  4. 4The callback gotcha: register the returned callbackUrl
  5. 5The SDK decorators: @requires_access_token and @requires_api_key
  6. 6Production: stream the consent URL, and the Identity↔Gateway link

Two sides of one coin: inbound IdP vs outbound credential provider

The single most common confusion in AgentCore Identity is treating "who can call my agent" and "how my agent calls Google" as the same setting. They are two completely separate mechanisms that happen to both involve OAuth, and wiring one where you needed the other produces a 401 you will stare at for an hour.

  • Inbound identity provider (IdP) — the OIDC provider whose JWTs gate who can invoke your agent, tool, Runtime, or Gateway. This is the bouncer at the door. Examples: Cognito, Entra ID, Okta, Auth0, Keycloak, PingFederate. You configure it as a customJWTAuthorizer (discovery URL, allowed clients, allowed audience/scopes). It answers: is this caller allowed in?
  • Outbound credential provider — configuration that lets your agent get a token for a downstream resource (Google Drive, GitHub, Slack, an internal API). This is the valet who fetches the user's keys from a locked vault. You create it with CreateOauth2CredentialProvider or CreateApiKeyCredentialProvider. It answers: how does the agent authenticate to the thing it's about to call?

The same vendor name can appear on both sides and still mean different things. Auth0 as an inbound IdP validates the JWT that lets a user invoke your agent. Auth0Oauth2 as an outbound credential provider is how your agent obtains a token to call an Auth0-protected downstream API. Same brand, opposite direction of trust.

text
   Caller ──(JWT from inbound IdP: Cognito/Entra/Okta)──▶  [ Your Agent ]
                                                               │ outbound credential provider
                                              Google Drive / GitHub / Slack / internal API

Keep the mental split sharp: inbound = authenticate the caller; outbound = authenticate the agent to a downstream service. Everything else in this lesson is the outbound side.

Key insight

Bouncer vs valet

Inbound IdP = the bouncer who checks the caller's ID at the door. Outbound credential provider = the valet who walks to a locked vault and fetches the user's car keys on demand. AgentCore Identity makes agentic auth delegated (the user consents, the agent acts on their behalf) rather than impersonated-with-a-shared-secret. If you ever find yourself pasting a long-lived API key into agent code, you've skipped the valet.

Built-in OAuth2 vendors and API-key providers

AgentCore Identity ships first-class config blocks for the common OAuth2 providers so you don't hand-roll discovery URLs and endpoints. You pick a credentialProviderVendor and supply the matching provider-config block.

credentialProviderVendorConfig blockUse it for
GoogleOauth2googleOauth2ProviderConfigGoogle APIs (Drive, Gmail, Calendar)
GithubOauth2githubOauth2ProviderConfigGitHub APIs
SlackOauth2(Slack config)Slack APIs
SalesforceOauth2(Salesforce config)Salesforce APIs
MicrosoftOauth2(Microsoft/Entra config)Microsoft Graph / Entra-protected APIs
Auth0Oauth2includedOauth2ProviderConfigAuth0-protected APIs
CustomOauth2customOauth2ProviderConfigAny OIDC server (provide the discovery URL)

For providers without a named built-in, CustomOauth2 accepts any OIDC-compliant authorization server via its discovery URL — that's your escape hatch for self-hosted IdPs like Keycloak or PingFederate, which are supported for both inbound JWT and outbound credential providers.

Not everything is OAuth. Some services authenticate with a single static API key — OpenAI is the canonical example. For those, you create an API-key credential provider with CreateApiKeyCredentialProvider; the key lives in the token vault (encrypted with KMS) and is fetched at call time with @requires_api_key. Use OAuth providers when the downstream service does delegated user consent; use API-key providers when it's just a bearer secret.

Watch out

The exact vendor list is VOLATILE

The set of named built-in vendors moves as AWS adds providers. As of writing the named built-ins are Google, GitHub, Slack, Salesforce, Atlassian/Jira, plus Microsoft/Entra and Auth0 — verify the current list on the Resource Providers doc before you teach or build against it. When in doubt, CustomOauth2 with a discovery URL covers any OIDC server.

Note

Where do the keys actually live?

OAuth client credentials, access tokens, refresh tokens, and API keys all sit in the AgentCore token vault — encrypted at rest and in transit via AWS KMS (service-managed or your CMK), isolated per (agent identity + user id). Tokens are never embedded in code/config or exposed to the LLM context. The credential provider is the config; the vault is where the resulting secrets are stored.

Creating a provider with bedrock-agentcore-control

Credential providers are control-plane resources. You create them once (per provider, per account/region) on the bedrock-agentcore-control API — via the AWS CLI, boto3, or the lower-level SDK IdentityClient.

OAuth2 provider (CLI):

bash
aws 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":"..."}}'
# IMPORTANT: register the returned callbackUrl in the provider's "Authorized redirect URIs"

Lower-level SDK (IdentityClient):

python
from bedrock_agentcore.services.identity import IdentityClient

identity = IdentityClient(region="us-east-1")

# OAuth2 (delegated, 3LO-capable)
identity.create_oauth2_credential_provider(...)

# Static API key (e.g. OpenAI)
identity.create_api_key_credential_provider(...)

The clientId / clientSecret you pass are the OAuth app credentials you registered with the provider (Google Cloud Console, a GitHub OAuth App, etc.). AgentCore stores them in the vault and uses them to drive the authorization-code (3LO) or client-credentials (2LO) flow.

The call returns a callbackUrl — and that return value is the single most-missed step in the whole feature, covered next.

Tip

Provider config carries refresh-token quirks

Long-lived 3LO requires a refresh token, and each provider asks for it differently. Google wants "access_type":"offline" in customParameters; Microsoft Entra wants the offline_access scope; Salesforce wants the refresh_token scope; GitHub needs "User-to-server token expiration" enabled in the app; Slack needs "token rotation" enabled. These are VOLATILE per-provider details — check the live docs and set them when you create the provider, or your tokens expire and the agent re-prompts for consent every time.

The callback gotcha: register the returned callbackUrl

When you create an OAuth2 credential provider, AgentCore returns a callback URL of this shape:

text
https://bedrock-agentcore.<region>.amazonaws.com/identities/oauth2/callback/<uuid>

This is the redirect URI the provider (Google, GitHub, Slack...) will send the user back to after they grant consent. You must register that exact URL in the provider's "Authorized redirect URIs" list. If you don't, the 3LO authorization-code flow breaks: the user authenticates, the provider tries to redirect to a URI it doesn't recognize, and the flow dies with a redirect-mismatch error before AgentCore ever sees the code.

Concretely, for a Google provider in us-east-1:

text
# 1. create-oauth2-credential-provider returns something like:
https://bedrock-agentcore.us-east-1.amazonaws.com/identities/oauth2/callback/3f2c1a9e-...-d4

# 2. In Google Cloud Console → your OAuth client → Authorized redirect URIs:
#    paste that exact URL (region + uuid must match)

This only bites 3LO (user-delegated, authorization-code) flows — pure 2LO/M2M client-credentials flows have no browser redirect, so there's no callback to register. But the failure mode is sneaky precisely because everything looks configured: the provider exists, the decorator runs, the consent page even renders. The break happens at the redirect hop, which is exactly the step people forget to wire up.

Watch out

Region and UUID must match exactly

The callback URL embeds both the region and a per-provider UUID. Copying the URL from a tutorial, a different region, or a previously-deleted provider will fail with a redirect-URI mismatch. Always paste the URL that this create-oauth2-credential-provider call returned, and re-paste it if you recreate the provider.

The SDK decorators: @requires_access_token and @requires_api_key

Once the provider exists, you almost never call the vault APIs by hand. The Python SDK gives you two decorators in bedrock_agentcore.identity.auth that run the entire workload-identity → vault → consent flow and inject the resolved credential as a keyword argument — so your function body, and the LLM driving it, never see the raw token.

OAuth access token (3LO / user-delegated):

python
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; "M2M" for client-credentials/2LO
    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 by the decorator — never passed by the LLM,
    # never stored in code. Use it to call the downstream API.
    ...

The full @requires_access_token signature: provider_name, scopes, auth_flow ("M2M" | "USER_FEDERATION"), into="access_token" (the kwarg name to inject into), on_auth_url, callback_url, force_authentication=False, token_poller, custom_state, custom_parameters. Set into="..." if you want the token under a different parameter name.

Static API key (e.g. OpenAI):

python
from bedrock_agentcore.identity.auth import requires_api_key

@requires_api_key(provider_name="openai-provider")
async def call_openai(*, api_key: str):
    # api_key fetched from the vault and injected as a kwarg
    ...

Why the kwarg injection matters: the credential is resolved at call time from the vault and handed to the function as a parameter. It is not embedded in the prompt, not returned to the model, and not logged in your business logic. That's the whole security pitch — the agent reasons about what to do, the decorator handles how to authenticate, and the secret stays out of context.

Token caching & refresh: under 3LO, AgentCore caches the access + refresh tokens in the vault keyed by (agent workload identity + user id), so subsequent calls skip consent until expiry and auto-refresh. If a provider revokes a token, the call fails — set force_authentication=True to force a fresh consent.

Lower-level escape hatch: if you need finer control, IdentityClient exposes create_oauth2_credential_provider, create_api_key_credential_provider, create_workload_identity, get_workload_access_token(...), async get_token(...), async get_api_key(...), and complete_resource_token_auth(session_uri, user_identifier).

Example

auth_flow picks 2LO vs 3LO

auth_flow="USER_FEDERATION" runs 3LO (authorization-code, user-delegated) — the user consents via the auth URL and the agent acts on their behalf. auth_flow="M2M" runs 2LO (client-credentials) — the agent authenticates as itself, no human in the loop, no consent URL, no callback to register. Pick M2M for backend service-to-service calls and USER_FEDERATION whenever the action is on a specific user's behalf.

Production: stream the consent URL, and the Identity↔Gateway link

The on_auth_url=lambda url: print(...) in the examples is a demo affordance. In production there is no terminal to print to — the agent is a serverless Runtime handling a request. Printing the consent URL means the user never sees it and the 3LO flow stalls forever.

The production pattern is to stream the consent URL back to the caller so your front end can render it as a clickable "Connect your Google account" link:

python
async def entrypoint(payload, *, stream):
    async def surface_consent(url: str):
        # send the URL to the caller instead of printing it
        await stream.send({"type": "auth_required", "consent_url": url})

    @requires_access_token(
        provider_name="google-provider",
        scopes=["https://www.googleapis.com/auth/drive.metadata.readonly"],
        auth_flow="USER_FEDERATION",
        on_auth_url=surface_consent,        # stream it, do not print it
        callback_url="<oauth2_callback_url>")
    async def read_drive(*, access_token: str):
        ...
    return await read_drive()

How this relates to Gateway. AgentCore Gateway aggregates downstream tools (Lambda, OpenAPI, MCP servers, Smithy, API Gateway, Runtime) behind one MCP endpoint, and its outbound (egress) auth is powered by the same AgentCore Identity machinery you just learned. When a Gateway target uses OAuth, the gateway's service role calls into Identity exactly as the decorators do — its role needs bedrock-agentcore:GetWorkloadAccessToken, bedrock-agentcore:GetResourceOauth2Token, and secretsmanager:GetSecretValue (swap GetResourceOauth2Tokenbedrock-agentcore:GetResourceApiKey for API-key targets). So the credential providers you create here are reused whether the agent calls a downstream API directly via a decorator or through a Gateway target. Identity is the credential layer; Gateway is one consumer of it.

Key insight

Same vault, two front doors

The credential provider you register with bedrock-agentcore-control is consumed two ways: directly by an agent function via @requires_access_token / @requires_api_key, or by a Gateway target's outbound-auth config. Both resolve credentials from the same per-(identity + user) token vault. Create the provider once; reuse it across direct calls and gateways.

Note

Pricing is VOLATILE

As of writing, Identity carries no additional charge when used through Runtime or Gateway; otherwise it's billed per OAuth-token / API-key request. Treat that as a concept, not a fixed fact — verify the live AgentCore pricing page before you size a workload.

Try it: Wire up a Google credential provider and read Drive without secrets in code

Goal: create an outbound OAuth2 credential provider, survive the callback gotcha, and have your agent call Google Drive via @requires_access_token so no token ever appears in your code.

Prereqs: an AWS account with Bedrock AgentCore access in a supported region, a Google Cloud project with an OAuth 2.0 Client (Web application), and bedrock-agentcore installed.

  1. Create the provider. Run the control-plane call (fill in your own Google client id/secret — do NOT paste secrets into chat or commit them):
    bash
    aws bedrock-agentcore-control create-oauth2-credential-provider --region us-east-1 \
      --name "google-provider" --credential-provider-vendor "GoogleOauth2" \
      --oauth2-provider-config-input '{"googleOauth2ProviderConfig":{"clientId":"$GOOGLE_CLIENT_ID","clientSecret":"$GOOGLE_CLIENT_SECRET"}}'
    Capture the returned callbackUrl.
  2. Register the callback (the gotcha). Copy the exact https://bedrock-agentcore.us-east-1.amazonaws.com/identities/oauth2/callback/<uuid> URL into your Google OAuth client's Authorized redirect URIs. Confirm the region and UUID match.
  3. Add the refresh-token quirk. For Google, ensure "access_type":"offline" is set via customParameters so you get a refresh token; note in a comment which providers need which setting (Google offline, Entra offline_access, Salesforce refresh_token scope, etc.).
  4. Write the decorated function. Create an async function decorated with @requires_access_token(provider_name="google-provider", scopes=["https://www.googleapis.com/auth/drive.metadata.readonly"], auth_flow="USER_FEDERATION", on_auth_url=..., callback_url="<your callback url>") that accepts *, access_token: str and lists Drive file metadata. Confirm access_token is never referenced anywhere except inside this function.
  5. Run 3LO locally. For the first run, set on_auth_url=lambda url: print(url), open the printed URL, grant consent, and watch the call succeed. Then run it a second time and confirm consent is skipped (cached token).
  6. Make it production-shaped. Replace the print on_auth_url with a handler that streams the consent URL back to the caller (e.g. over your Runtime response) instead of printing. Write two sentences on why printing fails in a serverless Runtime.
  7. Bonus — API key. Create an API-key credential provider for OpenAI with CreateApiKeyCredentialProvider and a second function using @requires_api_key(provider_name="openai-provider") with a *, api_key: str parameter. Note the difference: no callback, no consent screen.
  8. Reflect. Where would this same provider get reused if you fronted the downstream call with an AgentCore Gateway target instead of a direct decorator? Name the two Identity IAM actions the gateway service role would need.

Key takeaways

  1. 1Don't conflate the two sides: an inbound IdP (Cognito/Entra/Okta/Auth0) gates *who can invoke* your agent via JWTs; an outbound credential provider gets *tokens for a downstream resource*. Same vendor name can appear on both sides meaning opposite things.
  2. 2Built-in OAuth2 vendors are `GoogleOauth2`, `GithubOauth2`, `SlackOauth2`, `SalesforceOauth2`, `MicrosoftOauth2`, `Auth0Oauth2`, and `CustomOauth2` (any OIDC server via discovery URL, including self-hosted Keycloak/PingFederate). For static keys like OpenAI, use an API-key provider instead.
  3. 3Create providers on the `bedrock-agentcore-control` API with `CreateOauth2CredentialProvider` / `CreateApiKeyCredentialProvider` (CLI, boto3, or `IdentityClient`); the OAuth call returns a `callbackUrl`.
  4. 4Callback gotcha: register the returned `https://bedrock-agentcore.<region>.amazonaws.com/identities/oauth2/callback/<uuid>` in the provider's Authorized redirect URIs, with exact region + UUID, or the 3LO redirect silently breaks. (2LO/M2M has no callback.)
  5. 5`@requires_access_token(provider_name, scopes, auth_flow, on_auth_url, callback_url, ...)` and `@requires_api_key` from `bedrock_agentcore.identity.auth` run the full workload-identity → vault → consent flow and inject the credential as a kwarg — the raw token never touches business logic or the LLM context.
  6. 6`auth_flow="USER_FEDERATION"` is 3LO (user consent), `"M2M"` is 2LO (agent-as-itself). In production, stream the consent URL back to the caller via `on_auth_url` instead of printing it.
  7. 7AgentCore Gateway's outbound auth reuses the same Identity credential providers and token vault — create a provider once and use it from direct decorators or from Gateway targets.

Quiz

Lock in what you learned

Check your understanding

0 / 4 answered

1.Your agent must let only employees with a valid Okta token invoke it, and once invoked must call the Google Drive API on the user's behalf. Which two mechanisms do you configure?

2.You created a `GoogleOauth2` credential provider, the decorator runs, and the user even sees a Google consent screen — but after they click Allow the flow fails with a redirect error. What did you most likely skip?

3.Why do `@requires_access_token` and `@requires_api_key` inject the credential as a keyword argument instead of returning it or putting it in the prompt?

4.Your agent runs in a serverless Runtime with no terminal. Which `@requires_access_token` setup correctly handles a 3LO consent flow in production?

Go deeper

Hand-picked sources to keep learning