Agentic AI AcademyAgentic AI Academy

Inbound Auth

SigV4 vs. JWT/OIDC, and invoking a protected agent

Intermediate 13 minBuilder
What you'll be able to do
  • 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
At a glance

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.

  1. 1Two doors, one version: SigV4 or JWT — never both
  2. 2IAM SigV4: the default door
  3. 3JWT Bearer: configuring customJWTAuthorizer
  4. 4The big gotcha: boto3 cannot invoke a JWT-protected agent
  5. 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 Authorization header. A customJWTAuthorizer you 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:

python
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 *:

json
{
  "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.

JWT Bearer: configuring customJWTAuthorizer

To accept OAuth/OIDC bearer tokens instead, you attach a customJWTAuthorizer in the authorizerConfiguration when you call create_agent_runtime on the control-plane client (bedrock-agentcore-control):

python
import boto3
client = boto3.client("bedrock-agentcore-control", region_name="us-east-1")

client.create_agent_runtime(
    agentRuntimeName="HelloAgent",
    agentRuntimeArtifact={
        "containerConfiguration": {"containerUri": "...ecr.../my-agent:latest"}
    },
    authorizerConfiguration={"customJWTAuthorizer": {
        "discoveryUrl": "COGNITO_DISCOVERY_URL",
        "allowedClients": ["COGNITO_CLIENT_ID"]
    }},
    networkConfiguration={"networkMode": "PUBLIC"},
    roleArn="arn:aws:iam::111122223333:role/AgentRuntimeRole",
    lifecycleConfiguration={"idleRuntimeSessionTimeout": 300, "maxLifetime": 1800},
)

The authorizer validates incoming JWTs against these fields:

FieldToken claim validatedPurpose
discoveryUrliss (issuer)The OIDC discovery document URL; must match the regex ^.+/\.well-known/openid-configuration$. Maps to and validates the token's issuer.
allowedClientsclient_idThe set of OAuth client IDs permitted to invoke.
allowedAudienceaudThe set of allowed audiences the token must target.
allowedScopesscopeRequired OAuth scopes the token must carry.

You typically provide either allowedClients or allowedAudience (plus optionally allowedScopes and any required custom claims), depending on how your IdP populates the token. The discoveryUrl is always required — that is how AgentCore discovers the issuer's signing keys and validates the iss.

Any standards-compliant OIDC provider works as the inbound IdP: Cognito, Microsoft Entra ID, Okta, Auth0, Keycloak, or PingFederate. The dossier example uses Cognito; the config shape is identical for the others — you just point discoveryUrl at that provider's .well-known/openid-configuration.

Watch out

Cognito has NO Dynamic Client Registration

AgentCore-as-resource-server does not auto-register clients, and Cognito does not support Dynamic Client Registration (RFC 7591). You must PRE-REGISTER the OAuth app client in your Cognito user pool, capture its client_id, and put that value in allowedClients. There is no on-the-fly client creation — a client that isn't pre-registered simply can't get a valid token your authorizer will accept.

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:

  1. The agent runtime ARN is URL-encoded into the request path (not passed as a parameter).
  2. You pass the OAuth token in the standard Authorization: Bearer <jwt> header — no AWS signing at all.
python
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:

text
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:

json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "bedrock-agentcore:InvokeAgentRuntimeForUser",
    "Resource": "arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/HelloAgent-abc123"
  }]
}
python
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:InvokeAgentRuntimeForUser only to specific, trusted principals and only on specific runtimes. Explicitly Deny it 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.)

  1. Deploy a SigV4 version (default). Create an agent runtime with no authorizerConfiguration so it defaults to SigV4. From a role granted only bedrock-agentcore:InvokeAgentRuntime on that runtime ARN, invoke it with boto3's client('bedrock-agentcore').invoke_agent_runtime(...) using a runtimeSessionId of at least 33 characters. Confirm a success response.

  2. 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.

  3. Pre-register a Cognito client. In a Cognito user pool, create an OAuth app client (remember: no Dynamic Client Registration) and note its client_id and the pool's .well-known/openid-configuration discovery URL.

  4. 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>]}}.

  5. 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 POST to https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{urllib.parse.quote(arn, safe='')}/invocations?qualifier=DEFAULT with Authorization: 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.

  6. Harden a delegation path. On the SigV4 version, add an IAM policy granting bedrock-agentcore:InvokeAgentRuntimeForUser to ONE trusted backend role on that specific runtime ARN, and an explicit Deny for everyone else. Invoke with runtimeUserId derived from a (mock) authenticated session — never from raw request input. Then check CloudTrail and confirm the runtimeUserId was recorded.

  7. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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