Govern AI agent access with Okta Cross App Access and Amazon Bedrock AgentCore – Part 1

An AI agent that acts for a user raises an identity question that per-app OAuth and service accounts leave open: which agent may act for which user, and how you stop it. In this first post of two, you build a task agent on Amazon Bedrock AgentCore that governs access through Okta Cross App Access and AgentCore Identity, and enforces each user's authority.

Tác giả
Hammad Ausaf
Đăng bởi
AWS
Ngày đăng
Độ dài
3,660 từ · 17 phút
Govern AI agent access with Okta Cross App Access and Amazon Bedrock AgentCore – Part 1

An AI agent that updates a ticket or reads a customer record acts for a person, inside applications that person can reach. Your identity team then has to answer a question that per-app OAuth consent and shared service accounts leave open: which agent might act for which user in which application, and how you stop it. Okta Cross App Access (XAA) answers it in your identity provider (IdP). You register the agent in Okta, an administrator approves the applications it might reach, and Okta evaluates policy each time the agent asks to act for a user. In our tests, after an administrator removed a user’s assignment to the agent, Okta refused the agent’s new requests for that user within about 1 second, and tokens already issued expired within 5 minutes.

Amazon Bedrock AgentCore is a platform to build, connect, and optimize agents at scale, with any framework or model. This solution uses two of its capabilities to carry out Okta’s decision on AWS. Amazon Bedrock AgentCore runtime, a capability of Amazon Bedrock AgentCore, validates Okta’s grant. Amazon Bedrock AgentCore Identity, a capability of Amazon Bedrock AgentCore, redeems it for a short-lived access token with credentials your agent code doesn’t see, and keeps that token in its token vault. In our tests, AgentCore Identity added about 0.6 seconds to a request that took about 30 seconds end to end.

In this post, the first of two, you build a task agent on AgentCore runtime that acts for signed-in users through XAA and AgentCore Identity. You configure the AgentCore Identity parts it needs, enforce each user’s authority in the tools, and see what we measured: what each check refuses, the practices we settled on, and how fast Okta’s controls take effect. Part 2 then puts the same governance behind Amazon Bedrock AgentCore Gateway, a capability of Amazon Bedrock AgentCore, with Cedar policies, a second agent on Amazon Elastic Kubernetes Service (Amazon EKS), and an audit trail in Amazon CloudWatch.

Solution overview

Okta is the governance plane: it decides which agent might act for which user, against which resource. AgentCore Identity is the execution plane: it holds the agent’s credentials, turns Okta’s decision into a token the resource accepts, and keeps both identities in that token. In Figure 1, the agent and the two Model Context Protocol (MCP) servers it calls all run on AgentCore runtime.

Figure 1: Solution architecture, with numbers matching the steps that follow

Each request carries three tokens, and each records a different decision: the ID token says who the user is, the Identity Assertion JWT Authorization Grant (ID-JAG) is Okta’s decision, and the access token is what the resource accepts. One request flows like this:

  1. The user signs in to the web front end, a backend for frontend (BFF), with OpenID Connect through the agent’s linked Okta app.
  2. For each request, the BFF exchanges the user’s ID token for an ID-JAG at Okta’s org authorization server, authenticated as the agent (RFC 8693 token exchange). Okta checks that the agent is active, that the user is assigned to it, and that an administrator connected it to the target authorization server for the requested scopes. The ID-JAG is single-use and expires in 5 minutes.
  3. The BFF invokes the agent on AgentCore runtime with the ID-JAG as its bearer token. The runtime’s JWT authorizer accepts only ID-JAGs that Okta issued to this agent.
  4. The runtime gets a workload access token from AgentCore Identity that binds the agent’s workload identity to the user, and passes it to the agent code. This workload access token is an AWS-internal binding token: neither Okta nor the resource sees it, so it isn’t one of the three tokens.
  5. The agent asks AgentCore Identity for an access token. AgentCore Identity redeems the ID-JAG at the resource’s Okta custom authorization server with the JWT authorization grant (RFC 7523), authenticated as the agent. Okta returns a 5-minute access token that names both the user and the agent and carries the user’s groups.
  6. The agent calls the MCP servers with that token. The task manager checks that a registered agent obtained it, and that the user’s group and scope allow the tool.

Figure 2 shows the same request as a sequence, with the check that each party makes.

Figure 2: The three-token chain and the check each party makes, with the step 4 workload access token staying inside AWS

Prerequisites

To follow along, you need the following:

PrerequisiteWhat we used
An AWS account with access to Amazon Bedrock AgentCore and an Anthropic Claude model in Amazon BedrockUS West (Oregon)
An Okta org with Cross App Access for AI agents and API Access Management (custom authorization servers)An Okta Integrator Free Plan org
Python, the AWS Command Line Interface (AWS CLI), boto3, the AgentCore CLI, and the AgentCore Python SDK, a capability of Amazon Bedrock AgentCorePython 3.11, AWS CLI version 2, boto3 1.43, AgentCore CLI 0.31.1, and AgentCore Python SDK 1.24.1
AWS Identity and Access Management (IAM) permissionsPermission to create AgentCore resources and IAM roles

The walkthrough takes about 1 hour. AgentCore charges for what you use. See Amazon Bedrock AgentCore pricing.

The base for this solution is the AWS sample 08-okta-xaa-delegation-chain in the AgentCore samples repository on GitHub, and you can build the solution in this post on top of it. Beyond that sample, the snippets and configurations in this post show the pattern conceptually. They aren’t a complete, deployable solution. The code is sample code for non-production use, so work with your security and legal teams to meet your organization’s security, regulatory, and compliance requirements before you deploy it.

Register the agent in Okta

Okta needs an authorization server for the resource, the agent, a connection between them, and a policy that admits the agent’s grant. Do all of these steps in the Okta Admin Console: registering the agent creates its linked app, so you don’t need an app integration wizard.

  1. Create a custom authorization server for the resource application with the audience api://task-manager and the custom scopes xaa:read and xaa:write. Request only the resource’s custom scopes: Okta rejects an ID-JAG request that includes OpenID Connect scopes such as openid or profile (invalid_scope), so a request needs at least one custom scope and no OpenID Connect scopes.
  2. Add a groups claim to its access tokens, filtered to groups that start with xaa-. This claim carries each user’s authority to the tools.
  3. In the Admin Console, go to Directory > AI Agents, choose Register AI agent and then Register manually, optionally assign an owner, and create a linked OpenID Connect app. On Client registration, choose Client secret, generate a secret, and then activate the agent. The agent and the app share one client ID, and that app must sign your users in: an ID token from another app fails the exchange with “the client application is not registered for delegation to this agent.”
  4. On the agent’s Resource connections tab, connect it to the authorization server, limited to the scopes it needs, as Figure 3 shows.
  5. Add an access policy rule to the custom authorization server that allows the agent’s JWT Bearer grant, with a 5-minute access token lifetime. Without the grant type, the second exchange fails with no_matching_policy, and the lifetime bounds how long a revoked delegation keeps working.
  6. Assign the users or groups who might act through the agent. They sign in through the linked app, so give it an app sign-in policy that admits them with the authenticators they’ve enrolled. In our sandbox org, the linked app had its own policy with a rule for the test group. For real users, apply your organization’s policy, including multifactor authentication.

Figure 3: The task agent’s resource connection in Okta, limited to the xaa:read and xaa:write scopes

Okta’s Cross App Access guide covers the console steps, and the AWS sample 08-okta-xaa-delegation-chain scripts the authorization server and its JWT bearer policy. The BFF then requests the ID-JAG, authenticated as the agent:

POST /oauth2/v1/token HTTP/1.1
Host: YOUR-ORG.okta.com
Authorization: Basic <the agent's client ID and secret>
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&subject_token=<the user's ID token>
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&audience=https://YOUR-ORG.okta.com/oauth2/YOUR-CUSTOM-AS-ID
&scope=xaa:read xaa:write

The AWS sample 06-okta-xaa sends the same request in Python (agent/xaa_client.py). The BFF authenticates as the agent, so store the agent’s client secret in AWS Secrets Manager rather than in code or configuration files.

Set up AgentCore Identity

AgentCore Identity holds three things for this solution, and your agent code doesn’t see the client secret or call Okta’s token endpoint:

Part of AgentCore IdentityWhat it holdsWho creates it
Workload identityThe agent’s identity on AWS, bound to the signed-in user for each requestAgentCore runtime, for each agent runtime
OAuth2 credential providerThe agent’s Okta client ID and secret, and the grant to useYou, once per Okta agent
Token vaultThe access tokens the provider obtainsAgentCore Identity

Create the credential provider

The credential provider performs the second exchange. Point it at the custom authorization server, authenticate it as the AI agent, and set the on-behalf-of grant to JWT_AUTHORIZATION_GRANT, which AgentCore Identity sends as jwt-bearer with the ID-JAG as the assertion:

control = boto3.client("bedrock-agentcore-control")
provider = control.create_oauth2_credential_provider(
    name="okta-xaa-obo",
    credentialProviderVendor="CustomOauth2",
    oauth2ProviderConfigInput={"customOauth2ProviderConfig": {
        "oauthDiscovery": {"discoveryUrl": f"{OKTA_ORG}/oauth2/{CUSTOM_AS_ID}/.well-known/openid-configuration"},
        "clientId": AGENT_CLIENT_ID,          # the AI agent's Okta client ID
        "clientSecret": AGENT_CLIENT_SECRET,  # AgentCore Identity stores it in AWS Secrets Manager
        "clientAuthenticationMethod": "CLIENT_SECRET_BASIC",
        "onBehalfOfTokenExchangeConfig": {"grantType": "JWT_AUTHORIZATION_GRANT"},
    }},
)
provider_arn = provider["credentialProviderArn"]

Okta binds both exchanges to the client named in the ID-JAG, so each Okta agent needs its own provider: when we redeemed one agent’s ID-JAG as another, Okta refused with “The ‘client_id’ in the JWT Bearer Grant must match the ‘client_id’ used to authenticate the client.” AgentCore Identity keeps the secret in a service-managed AWS Secrets Manager secret, which you rotate with update-oauth2-credential-provider. If your team already manages the client secret in Secrets Manager, you can reference that secret’s ARN instead. AgentCore Identity reads the client secret from it, so you can rotate the secret without updating the provider (see Reference your own AWS Secrets Manager secrets in Amazon Bedrock AgentCore Identity). To avoid a shared secret, authenticate with a private key instead. See Authenticate with Private Key JWT using Amazon Bedrock AgentCore Identity. Figure 4 shows the provider in the AgentCore console.

Figure 4: The credential provider authenticating as the agent with the JWT authorization grant

Use the workload identity that AgentCore runtime manages

AgentCore runtime creates and manages a workload identity for each agent runtime. When a request passes the runtime’s JWT authorizer, the runtime gets a workload access token for that workload identity and the inbound token’s issuer and subject, and passes it to your code. AgentCore Identity then uses the ID-JAG behind it as the assertion for the exchange.

The AgentCore Python SDK keeps the workload access token in BedrockAgentCoreContext, not in the request headers that your entrypoint receives. Our first version looked for it in the headers, found nothing, and fell back to its own GetWorkloadAccessTokenForJWT call with a second workload identity, on all 21 requests we checked. Read it from the context instead:

from bedrock_agentcore.runtime import BedrockAgentCoreContext

identity = boto3.client("bedrock-agentcore")

def okta_access_token() -> str:
    workload_token = BedrockAgentCoreContext.get_workload_access_token()  # minted by the runtime
    return identity.get_resource_oauth2_token(
        workloadIdentityToken=workload_token,
        resourceCredentialProviderName="okta-xaa-obo",
        scopes=["xaa:read", "xaa:write"],  # must equal the ID-JAG's scopes
        oauth2Flow="ON_BEHALF_OF_TOKEN_EXCHANGE",
    )["accessToken"]

AgentCore Identity keeps the access token in its token vault for the rest of the request. Across 10 requests, the agent’s first call redeemed the ID-JAG in about half a second (see the latency table in Validate the results), and the second, for the other MCP server, returned the same token in a median of 58 milliseconds without redeeming the single-use ID-JAG again. A new ID-JAG for the same user got a new token, so each request reflects Okta’s latest decision. The sample 08-okta-xaa-delegation-chain reads the workload access token the same way (agent/agent.py).

Scope the agent’s role to its workload identity and provider

AgentCore Identity doesn’t bind workload identities to credential providers in the same account. The agent’s IAM role does, so scope it to the agent’s own workload identity and provider, and to that provider’s secret:

[
  {
    "Sid": "OktaOnBehalfOf",
    "Effect": "Allow",
    "Action": "bedrock-agentcore:GetResourceOauth2Token",
    "Resource": [
      "arn:aws:bedrock-agentcore:us-west-2:123456789012:workload-identity-directory/default",
      "arn:aws:bedrock-agentcore:us-west-2:123456789012:workload-identity-directory/default/workload-identity/RUNTIME-WORKLOAD-IDENTITY",
      "arn:aws:bedrock-agentcore:us-west-2:123456789012:token-vault/default",
      "arn:aws:bedrock-agentcore:us-west-2:123456789012:token-vault/default/oauth2credentialprovider/okta-xaa-obo"
    ]
  },
  {
    "Sid": "ReadProviderSecret",
    "Effect": "Allow",
    "Action": "secretsmanager:GetSecretValue",
    "Resource": "arn:aws:secretsmanager:us-west-2:123456789012:secret:bedrock-agentcore-identity!default/oauth2/okta-xaa-obo-*"
  }
]

Take the workload identity’s name from workloadIdentityDetails in GetAgentRuntime, and the provider’s ARN from credentialProviderArn: the segment is oauth2credentialprovider, and with the hyphenated form every call failed with AccessDeniedException. The role needs no GetWorkloadAccessToken permission, because the runtime gets that token, not the agent. Our tests passed with only these two statements.

Deploy the agent on AgentCore runtime

Configure the agent runtime’s inbound authorizer to accept only ID-JAGs that Okta issued to this agent. Its issuer is Okta’s org authorization server, and its audience is the resource’s custom authorization server. In agentcore.json:

"authorizerType": "CUSTOM_JWT",
"authorizerConfiguration": {
  "customJwtAuthorizer": {
    "discoveryUrl": "https://YOUR-ORG.okta.com/.well-known/openid-configuration",
    "allowedAudience": ["https://YOUR-ORG.okta.com/oauth2/YOUR-CUSTOM-AS-ID"],
    "allowedClients": ["YOUR_AGENT_CLIENT_ID"],
    "allowedScopes": ["xaa:read", "xaa:write"]
  }
}

allowedClients matches the client_id claim that an ID-JAG carries. Without it, the runtime admitted another agent’s ID-JAG with HTTP 200 and the request failed later, at the exchange. With it, the runtime refused that token with HTTP 401.

Besides the policy from the previous section, the execution role needs bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream. It needs no bedrock-agentcore:InvokeAgentRuntime permission for the MCP servers: each runs on AgentCore runtime behind its own JWT authorizer, with the custom authorization server as issuer and api://task-manager as audience, so the agent calls it with the Okta access token as a bearer token, not with AWS Signature Version 4 (SigV4).

Enforce each user’s authority in the MCP server

The resource connection sets the scopes for every user of an agent, so all three test users received xaa:read xaa:write, and per-user authority travels in the groups claim instead: Alice’s token carried xaa-readers, Bob’s xaa-writers, and Carol’s had none.

The task manager checks two things: it accepts only tokens a registered agent obtained, and it allows a write only when the group and the scope are both present:

REGISTERED_AGENTS = {"YOUR_AGENT_CLIENT_ID"}

def _require_agent() -> None:
    claims = _decode_claims()  # the runtime's JWT authorizer already validated the token
    agent = (claims.get("act") or {}).get("sub")
    if agent in REGISTERED_AGENTS and claims.get("cid") == agent:
        return
    raise ToolError("insufficient_privileges: this server accepts only tokens that a registered "
                    "AI agent obtained through Okta Cross App Access")

def _require_write() -> None:
    _require_agent()
    if WRITERS_GROUP in _groups() and "xaa:write" in _scopes():
        return
    raise ToolError(f"insufficient_privileges: this tool requires membership in '{WRITERS_GROUP}'")

Both checks close gaps we found: a write check that allowed tokens without a groups claim let Carol, who belongs to neither group, create tasks, and before _require_agent a plain token-exchange token, issued to a service app with no act claim, created a task for Bob. The server can read the claims without verifying the token again only because the runtime’s JWT authorizer already verified it. A server without that authorizer in front of it must check the signature, issuer, audience, and expiry itself, as the Part 2 agent on Amazon EKS does.

Validate the results

We ran every check against the deployed solution. Alice, Bob, and Carol each asked the agent to create a task and then list the project’s tasks:

Callercreate_tasklist_tasks
AliceDenied: requires membership in xaa-writersAllowed
BobCreatedAllowed
CarolDenied: no groups claimAllowed
Bob, then Alice right after himCreated for Bob, then denied for Alice: no authority carried overAllowed

These requests stopped before tools ran:

RequestStopped byResponse
Agent B’s ID-JAG sent to the task agentThe runtime’s JWT authorizer401: “Claim ‘client_id’ value mismatch with configuration.”
An ID-JAG held for 245 secondsThe runtime’s JWT authorizer401: “Ineffectual token, will expire within the next minute”
A malformed bearer tokenThe runtime’s JWT authorizer403: “Failed to parse token”
One ID-JAG redeemed twiceOkta’s custom authorization server400: “The id-jag was already used.”
A narrower scope at the second exchangeOkta’s custom authorization server400: “The scope parameter must exactly match the scopes in the id-jag.”
A scope outside the connectionOkta’s org authorization server400 invalid_scope
A plain token exchange token at the MCP serverThe MCP serverDenied: only tokens that a registered AI agent obtained

The token that AgentCore Identity returned matched one we redeemed directly at Okta, claim for claim: sub was the user, cid and act named the agent, scp held the connection’s scopes, groups held the user’s groups, and it lived 300 seconds.

AgentCore Identity adds about 0.6 seconds to a request that takes about 30 seconds end to end, most of it model and tool calls. Medians of five runs from our test client:

CallMedian
Sign-in, scripted (authorization code flow)1,205 ms
ID token to ID-JAG at Okta’s org authorization server216 ms
GetWorkloadAccessTokenForJWT135 ms
GetResourceOauth2Token, including Okta’s JWT bearer grant508 ms

Govern agents in operation

Okta evaluates the agent, the user, and the connection when it mints an ID-JAG. We measured what each administrator action does to the next request and to issued tokens:

Admin action in OktaNew requestsTokens already issued
Delete the agent’s resource connectionRefused immediately (invalid_target)Work until they expire, up to 5 minutes
Remove the user’s assignmentRefused within about 1 second, even with an ID token the user already heldUp to 5 minutes
Deactivate the agentRefused after Okta runs the operation, which our test org queued for 86 minutesUp to 5 minutes

Actions on one agent left the other working. Okta doesn’t re-check the user when an ID-JAG is redeemed: its custom authorization server issued an access token 3 seconds after we removed a user, for an ID-JAG minted before the removal. Okta’s System Log records each grant with the agent as the actor, and Part 2 streams those events into CloudWatch.

Best practices for the delegation chain

Six practices came out of these measurements, each one a property of the chain rather than a preference:

PracticeWhat we measured
Mint a fresh ID-JAG for each requestAn ID-JAG is single-use and lives 300 seconds. The runtime refuses one with under 60 seconds left, so 225 seconds worked, 245 didn’t
Carry per-user authority in the groups claim, and request one scope set per requestOkta sets scopes from the resource connection, so all three users got the same scopes, and the second exchange must request exactly the ID-JAG’s
Pin the agent at the runtime, and check act and cid in the toolallowedClients matches the client_id claim an ID-JAG carries. Okta access tokens carry cid instead
Reproduce a failed exchange directly against OktaThrough AgentCore Identity, a scope mismatch and a wrong client both surface as “Token exchange failed with HTTP status 400”
Grant the agent’s role read access to the provider’s secretGetResourceOauth2Token failed until the role could read it
Keep lifetimes short, and use the resource connection as your kill switchDeleting the connection took effect immediately. Deactivating the agent sat in Okta’s beta AI agent API queue for 86 minutes, with no cancel

Cross App Access compared with plain token exchange

Plain RFC 8693 token exchange also carries the user’s identity downstream. We compared it with XAA against the same Okta authorization server and user:

Behavior we observedPlain token exchangeCross App Access
Agent identity in the tokenNo act claimact names the agent
ScopesWidened: Alice’s own token had xaa:read, and the exchange returned xaa:read xaa:writeThe scopes the administrator approved
Exchange rule limited to xaa-writersAlice still received a tokenAlice denied
User unassigned, earlier token reusedStill exchangedRefused
What administrators seeA service app with a grant typeThe agent, its connections, and the agent in each log event

These results reflect our Okta configuration. Okta’s guide shows an act claim for exchanged tokens, which we didn’t receive. Plain token exchange carried the user’s identity, but not the user’s authority.

Where this pattern applies

We work with media and entertainment companies, and this is where we met the problem first. A studio or streaming or games organization runs agents against production tracking, title and asset metadata, rights and localization, and viewer or player support. Each system has its own authorization model and its own auditors, and a user’s authority differs by title, territory, or release window, so “which agent might act for which user in which system” has to be answered before an agent ships.

Nothing in the chain is specific to that industry: the agent is an identity in your IdP, the target is an OAuth 2.0 authorization server, and the user’s authority rides in a claim. Financial services, healthcare, and public sector teams put different resources behind the same controls.

Clean up

To avoid ongoing charges, delete the resources you created:

  1. Delete the AWS CloudFormation stacks that the AgentCore CLI created for the agent and the two MCP servers.
  2. Delete the credential provider with aws bedrock-agentcore-control delete-oauth2-credential-provider --name okta-xaa-obo, and any workload identity you created yourself with delete-workload-identity.
  3. Delete the IAM policies you added to the execution roles.
  4. In Okta, deactivate and delete the AI agent and its linked app, and remove the resource connection, the authorization server’s policy rule, scopes, and claim, and the test users and groups.

Conclusion

In this post, you used Okta Cross App Access to decide which agent might act for which user, and AgentCore Identity to carry out each decision. The runtime admitted only the agent’s own ID-JAGs, the credential provider redeemed each one with a secret the agent didn’t hold, the token vault reused the result within a request, and the MCP server enforced each user’s authority from the groups claim.

This design keeps the exchange in the agent’s role and repeats the authorization check in every MCP server. See Part 2 of this series to learn why its design moves both behind one AgentCore Gateway with Cedar policies, adds a second agent on Amazon EKS, and joins the Okta System Log with AgentCore traces in CloudWatch.

To get started, explore the AgentCore samples and Okta’s Cross App Access documentation. For the API details, see On-behalf-of token exchange in the AgentCore Developer Guide.

Bài viết này từ đâu mà có

Bài viết này do Hammad Ausaf đưa tin và được AWS đăng lần đầu vào 6 tháng 11, 2026. HUE Legacy Ventures không phải tác giả của bài viết.

Carried in full with attribution and a link to the original. Rights remain with the publisher, who may request removal at any time.

Đọc tại aws.amazon.com →