Inbound Auth
SigV4 vs. JWT/OIDC, and invoking a protected agent
- Explain why a single runtime version supports EITHER SigV4 OR JWT inbound auth, never both, and how versions let you offer both
- Configure IAM SigV4 (default) inbound auth and govern callers with the bedrock-agentcore:InvokeAgentRuntime action
- Configure JWT Bearer auth via customJWTAuthorizer with discoveryUrl, allowedClients, allowedAudience, and allowedScopes
- Invoke a JWT-protected agent with a raw HTTPS Bearer request (because boto3 cannot) and read the 401 + WWW-Authenticate PRM hint
- Pre-register an OAuth client in Cognito (no Dynamic Client Registration) and wire its discovery URL into the authorizer
- Use the X-Amzn-Bedrock-AgentCore-Runtime-User-Id delegation header safely with InvokeAgentRuntimeForUser and the right IAM guardrails
Inbound auth answers one question: who is allowed to invoke your agent runtime? AgentCore gives you two mutually exclusive mechanisms per runtime version — IAM SigV4 (the default, callers sign with AWS credentials) and JWT Bearer (OAuth 2.0/OIDC validated by a customJWTAuthorizer). This lesson shows the create_agent_runtime authorizer config for each, the hard gotcha that boto3 cannot invoke a JWT-protected agent (you make a raw HTTPS request with an Authorization: Bearer header), and how to use — and safely lock down — the X-Amzn-Bedrock-AgentCore-Runtime-User-Id delegation header.
- 1Two doors, one version: SigV4 or JWT — never both
- 2IAM SigV4: the default door
- 3JWT Bearer: configuring customJWTAuthorizer
- 4The big gotcha: boto3 cannot invoke a JWT-protected agent
- 5The user-id delegation header — powerful, and easy to misuse
Two doors, one version: SigV4 or JWT — never both
Inbound auth is the bouncer at the door: it authenticates and authorizes the caller invoking your agent runtime. AgentCore Identity gives you exactly two mechanisms, and a single runtime version can wear only one of them:
- IAM SigV4 (the default). Callers sign the request with AWS credentials. Authorization is governed by the IAM action
bedrock-agentcore:InvokeAgentRuntime. This is the right call for service-to-service traffic inside AWS, where the caller already has a role. - JWT Bearer (OAuth 2.0 / OIDC). Callers present a bearer token in an
Authorizationheader. AcustomJWTAuthorizeryou attach at create time validates the token against your identity provider. This is the right call when end users (web/mobile apps, third-party clients) need to reach the agent directly.
The rule that trips everyone up: a runtime version supports either SigV4 OR JWT, not both. If a version has a customJWTAuthorizer, it is JWT-only; if it has none, it is SigV4-only. There is no "accept either" mode on one version.
The escape hatch is the runtime's version/endpoint model. CreateAgentRuntime auto-creates Version 1 and a DEFAULT endpoint; each update creates a new immutable version. So if you genuinely need both auth styles — say, internal SigV4 callers and external OAuth users hitting the same agent code — you deploy two versions (or two runtimes) of the same container, one configured for SigV4 and one for JWT, and route callers to the appropriate ARN/qualifier.
Key insight
Inbound vs outbound — don't conflate them
This lesson is about inbound auth (who may invoke the agent). Outbound auth — the agent calling Google Drive, GitHub, Slack, or internal APIs on a user's behalf via the token vault — is a separate concern handled by AgentCore Identity's credential providers and the @requires_access_token decorator. A request first passes the inbound bouncer, then (optionally) the agent fetches downstream tokens for the outbound leg.
IAM SigV4: the default door
If you say nothing about authorization at create time, your runtime is SigV4-protected. Callers must sign their InvokeAgentRuntime requests with AWS credentials, and whether the call is allowed comes down to one IAM action: bedrock-agentcore:InvokeAgentRuntime.
Invoking a SigV4 runtime is exactly the boto3 path you'd expect — the data-plane client is bedrock-agentcore, and SigV4 signing happens transparently:
import boto3, json
# Data-plane client: bedrock-agentcore.{region}.amazonaws.com
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
resp = client.invoke_agent_runtime(
agentRuntimeArn="arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/HelloAgent-abc123",
runtimeSessionId="a-unique-session-id-at-least-33-characters-long", # >= 33 chars
payload=json.dumps({"prompt": "Hello"}).encode("utf-8"),
qualifier="DEFAULT",
)
# The response field is an iterable of byte chunks; join + decode them.
print("".join(c.decode("utf-8") for c in resp.get("response", [])))Least-privilege IAM for the caller (the principal that invokes the agent) looks like this — note you scope it to the specific runtime ARN, not *:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntime",
"Resource": "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/HelloAgent-abc123"
}]
}How a SigV4 auth failure looks on the wire matters for debugging. A SigV4 agent that rejects a caller returns HTTP 403 with ACCESS_DENIED and — importantly — no WWW-Authenticate header. That absence is the quick tell that you're on a SigV4 endpoint, not a JWT one (JWT failures behave differently, as we'll see next).
Tip
SigV4 = IAM does the authorization
With SigV4 there is no token validation config to attach. Authorization is pure IAM: grant or deny bedrock-agentcore:InvokeAgentRuntime on the runtime ARN to the calling principal. This is why SigV4 is the natural fit for backend services that already assume an IAM role.
The big gotcha: boto3 cannot invoke a JWT-protected agent
Here is the single most surprising thing about JWT inbound auth: the boto3 bedrock-agentcore client cannot invoke a JWT/bearer-protected agent. boto3 always SigV4-signs the request; it has no notion of attaching an Authorization: Bearer <jwt> header. So you must drop down to a plain HTTPS client and build the request by hand.
Two things make the raw request unusual:
- The agent runtime ARN is URL-encoded into the request path (not passed as a parameter).
- You pass the OAuth token in the standard
Authorization: Bearer <jwt>header — no AWS signing at all.
import requests, urllib.parse, json
region = "us-east-1"
agent_arn = "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/HelloAgent-abc123"
escaped_arn = urllib.parse.quote(agent_arn, safe="") # ARN URL-encoded into the path
url = (
f"https://bedrock-agentcore.{region}.amazonaws.com"
f"/runtimes/{escaped_arn}/invocations?qualifier=DEFAULT"
)
resp = requests.post(
url,
headers={
"Authorization": f"Bearer {bearer_token}", # the OAuth/OIDC JWT
"Content-Type": "application/json",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": session_id, # >= 33 chars
},
data=json.dumps({"prompt": "Hello"}),
)
print(resp.status_code, resp.text)Reading an auth failure. A JWT/OAuth agent that rejects you returns HTTP 401 with a WWW-Authenticate: Bearer header that points at the agent's Protected Resource Metadata (PRM) endpoint:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://.../.well-known/oauth-protected-resource?qualifier={QUALIFIER}"That PRM endpoint (.well-known/oauth-protected-resource, exposed by the control API GetRuntimeProtectedResourceMetadata) tells a client which authorization server to go get a token from. Contrast this with SigV4, which fails with 403 ACCESS_DENIED and no WWW-Authenticate header — a one-glance way to tell which kind of door you hit.
Example
The decision tree for clients
Invoking with AWS creds? Use boto3 invoke_agent_runtime — it SigV4-signs for you, and the agent must be a SigV4 version. Invoking with an OAuth token? You CANNOT use boto3 — build a raw HTTPS POST to /runtimes/{url-encoded-arn}/invocations with Authorization: Bearer, and the agent must be a JWT version. Mixing them up produces either a 403 (sent a signed request to... it still won't carry the bearer) or a 401 with a PRM pointer.
The user-id delegation header — powerful, and easy to misuse
Sometimes you want per-user behavior (per-user memory, per-user outbound tokens) but the caller is a trusted backend authenticating with SigV4, not the end user with a JWT. For that, AgentCore offers the X-Amzn-Bedrock-AgentCore-Runtime-User-Id header: the SigV4 caller asserts which user this request is for, and AgentCore derives a workload identity for that user via GetWorkloadAccessTokenForUserId.
This is real delegation, so it is gated by an extra IAM action beyond the normal invoke permission:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntimeForUser",
"Resource": "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/HelloAgent-abc123"
}]
}resp = client.invoke_agent_runtime(
agentRuntimeArn=agent_arn,
runtimeSessionId=session_id,
payload=payload,
# The asserted user id — opaque and UNVERIFIED by AgentCore.
# Must be derived from YOUR authenticated principal, never raw client input.
runtimeUserId="user-7f3c", # surfaces as X-Amzn-Bedrock-AgentCore-Runtime-User-Id
)Why this is dangerous if you're sloppy. The user-id value is an opaque, unverified identifier — AgentCore does not check that the caller "really is" that user; it trusts the asserting principal. If you let untrusted input flow into this header, a caller can impersonate any user. Treat it with the discipline its power demands:
- Restrict the action. Grant
bedrock-agentcore:InvokeAgentRuntimeForUseronly to specific, trusted principals and only on specific runtimes. ExplicitlyDenyit everywhere delegation isn't needed. - Derive the user-id from the authenticated principal in your backend — never pass a value supplied directly by an end client.
- Audit it. CloudTrail records the
runtimeUserId, so you can review who delegated as whom. - Prefer JWT in production. When the end user can authenticate directly, a validated JWT (with a real, signed
sub) is strictly safer than a backend asserting an unverified id.
Watch out
Unverified means unverified
AgentCore takes the user-id header at face value. The entire security of delegation rests on (a) tightly scoping who can call InvokeAgentRuntimeForUser and (b) your backend setting the id from an authenticated session, not from a request body or query param. If both aren't true, you've built an impersonation API.
Try it: Stand up both doors: invoke one agent two ways and lock down delegation
Goal: deploy the same agent container behind both inbound mechanisms, prove that boto3 can't reach the JWT door, and harden a delegation path. (Costs apply; tear down when done. Pricing/limits are volatile — check the live pricing page.)
-
Deploy a SigV4 version (default). Create an agent runtime with no authorizerConfiguration so it defaults to SigV4. From a role granted only
bedrock-agentcore:InvokeAgentRuntimeon that runtime ARN, invoke it with boto3'sclient('bedrock-agentcore').invoke_agent_runtime(...)using aruntimeSessionIdof at least 33 characters. Confirm a success response. -
Force a SigV4 failure and read it. Strip the IAM permission (or invoke from a principal that lacks it) and confirm you get HTTP 403 ACCESS_DENIED with no WWW-Authenticate header. Note that signature — it's how you recognize a SigV4 door.
-
Pre-register a Cognito client. In a Cognito user pool, create an OAuth app client (remember: no Dynamic Client Registration) and note its
client_idand the pool's.well-known/openid-configurationdiscovery URL. -
Deploy a JWT version. Create a second runtime (or version) of the same container, this time passing
authorizerConfiguration={'customJWTAuthorizer': {'discoveryUrl': <cognito_discovery_url>, 'allowedClients': [<client_id>]}}. -
Prove boto3 fails, then succeed with raw HTTPS. Try invoking the JWT version with boto3 and observe it does not work (no bearer support). Then obtain a token from Cognito and
POSTtohttps://bedrock-agentcore.{region}.amazonaws.com/runtimes/{urllib.parse.quote(arn, safe='')}/invocations?qualifier=DEFAULTwithAuthorization: Bearer <jwt>and a session-id header. Confirm success. Send a bad/expired token and capture the 401 + WWW-Authenticate: Bearer resource_metadata=... pointing at the PRM endpoint. -
Harden a delegation path. On the SigV4 version, add an IAM policy granting
bedrock-agentcore:InvokeAgentRuntimeForUserto ONE trusted backend role on that specific runtime ARN, and an explicitDenyfor everyone else. Invoke withruntimeUserIdderived from a (mock) authenticated session — never from raw request input. Then check CloudTrail and confirm theruntimeUserIdwas recorded. -
Reflect (3-4 sentences). When would you choose SigV4 vs JWT for a given caller? Why can't one version serve both? And what specifically would let a misconfigured user-id header become an impersonation hole — and which of your guardrails closes it?
Key takeaways
- 1A runtime VERSION supports exactly one inbound mechanism: IAM SigV4 (default) OR JWT Bearer — never both. To offer both, deploy separate versions/runtimes of the same container and route callers by ARN/qualifier.
- 2SigV4 callers sign with AWS credentials; authorization is the IAM action bedrock-agentcore:InvokeAgentRuntime scoped to the runtime ARN. boto3's invoke_agent_runtime signs transparently. Auth failure = 403 ACCESS_DENIED with NO WWW-Authenticate header.
- 3JWT auth is configured with authorizerConfiguration={'customJWTAuthorizer': {...}}, validating discoveryUrl (iss), allowedClients (client_id), allowedAudience (aud), and allowedScopes (scope). discoveryUrl must end in /.well-known/openid-configuration.
- 4boto3 CANNOT invoke a JWT-protected agent — it always SigV4-signs. Make a raw HTTPS POST to /runtimes/{url-encoded-arn}/invocations with an Authorization: Bearer <jwt> header. A 401 + WWW-Authenticate: Bearer points to the Protected Resource Metadata (PRM) endpoint.
- 5Cognito has no Dynamic Client Registration (RFC 7591) — pre-register the OAuth client and put its client_id in allowedClients. Any OIDC provider (Cognito, Entra, Okta, Auth0, Keycloak, PingFederate) works as the inbound IdP.
- 6The X-Amzn-Bedrock-AgentCore-Runtime-User-Id header enables user delegation from a SigV4 caller; it needs the extra IAM action bedrock-agentcore:InvokeAgentRuntimeForUser and uses GetWorkloadAccessTokenForUserId under the hood.
- 7That user-id is OPAQUE and UNVERIFIED: restrict the action to trusted principals + specific runtimes, derive the id from the authenticated principal (never raw client input), audit via CloudTrail (runtimeUserId), Deny it where unused, and prefer JWT in production.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.You configured a runtime version with a customJWTAuthorizer and want internal services that authenticate with IAM roles to also call it via SigV4 against the same version. What's the correct understanding?
2.Which authorizerConfiguration field validates the token's issuer, and what is the constraint on its value?
3.You wrote a Python client using boto3's client('bedrock-agentcore').invoke_agent_runtime(...) to call a JWT-protected agent, passing the JWT somewhere in the request. It keeps failing. Why, and what's the fix?
4.A teammate's backend reads a user_id straight from the incoming HTTP request body and passes it as X-Amzn-Bedrock-AgentCore-Runtime-User-Id when calling InvokeAgentRuntimeForUser. What is the most important problem?
Go deeper
Hand-picked sources to keep learning
The canonical page for inbound auth: SigV4 vs JWT, customJWTAuthorizer fields, the user-id delegation header, and the workload access token flow.
Where the 401 + WWW-Authenticate (JWT) vs 403 ACCESS_DENIED (SigV4) wire behavior and the PRM endpoint are specified.
IAM actions for callers and execution roles, including InvokeAgentRuntime and InvokeAgentRuntimeForUser. Verify the current action list here.
Deep dive on inbound/outbound auth and the delegation security model behind the user-id header.
Identity providers (Cognito, Entra, Okta, Auth0, Keycloak, PingFederate) and how inbound JWT validation fits the wider Identity service.
Identity is no extra charge when used through Runtime/Gateway — confirm current pricing and any free-tier terms on the live page.