On this page
Secure Amazon Bedrock Classic Agents with Okta
This guide shows you how to secure an Amazon Bedrock Classic Agent with Okta authentication by building a Python calling app that acts as a secure orchestrator. Your app performs Okta's two-step token exchange internally, and then invokes the Bedrock Classic Agent while securely passing the resulting access token and user identity as session attributes.
Note: To enable AI agent token exchange, you must first subscribe to Okta for AI Agents. Contact your Okta account team to enable the feature.
Learning outcomes
- Understand what a calling app must do to authenticate as a signed-in user with Okta.
- Add a token exchange module to your app.
- Set up an Amazon Bedrock Classic Agent with an action group Lambda that can call Okta-protected APIs.
- Invoke the Bedrock Classic Agent with the user's Okta identity passed as session attributes.
- Verify and test the end-to-end flow with a real Okta ID token.
What you need
- An Identity Engine org with the Okta for AI Agents feature enabled
- An AWS account with Amazon Bedrock available in your region and foundation model access approved
- The AI Agent registered in your org. See Configure AWS Identity and Access Management for AI agent imports (opens new window).
- The configured AWS CLI (opens new window) (
aws configure ssoor an access key and secret) - Python (opens new window) 3.10 or later
Overview
An AI agent has no inherent knowledge of an Okta user. To let it act for a specific user without sharing long-lived credentials, the calling app exchanges the user's identity for a short-lived, narrowly scoped access token. It then uses that token to call protected resources.
The integration has two parts:
Okta authentication. Your app performs a two-step token exchange:
- Exchange the user's
id_tokenfor an Identity Assertion JWT authorization grant (ID-JAG) at the org authorization server. - Exchange the ID-JAG for a scoped
access_tokenat a custom authorization server.
This logic is identical for any agent. You add it once as a reusable module. See Add Okta authentication to your agent.
- Exchange the user's
Platform integration (Amazon-specific). Your app invokes the Bedrock Classic Agent, passing the access token and the user's claims as session attributes. An action group Lambda function reads those session attributes and forwards the token as a bearer credential to an Okta-protected API. See Invoke the Bedrock Classic Agent with the access token.
There's no gateway or interceptor in this pattern. Your calling app owns the full token exchange, and the Bedrock Classic Agent receives a ready-to-use access_token in its session attributes.
For the conceptual background on AI agent token exchange, see Set up AI agent token exchange.
Before you begin
The token exchange depends on Okta objects that you configure once per org. Confirm that the following are in place before you add any integration code. For detailed steps, see Set up third-party AI Agent token exchange.
An OIDC web app integration that signs users in and issues the
id_tokenyour app exchanges. Use the Authorization Code grant type and theopenid profile emailscopes. Theid_tokenmust have anaudclaim equal to this app's client ID.A custom authorization server. Use the built-in
defaultserver or create one.A custom scope on the custom authorization server, such as
xaa:read. Okta strips system scopes (openid,profile,email) during the ID-JAG exchange and can cause aninvalid_scopeerror, so you must request a custom scope instead.Your agent imported into Okta as an AI Agent identity that uses
private_key_jwtclient authentication, with its public key (JWK) registered. Link the OIDC web app, set the custom authorization server, include your custom scope, and activate the agent.Note: Okta doesn't retain the agent's private key. Store it in a secrets manager when it's generated, because it's shown only once.
An access policy rule on the custom authorization server that enables the JWT bearer grant type (
urn:ietf:params:oauth:grant-type:jwt-bearer), adds the AI Agent as an allowed client, and includes the audience, the custom scope, and a user or group condition.
Collect your configuration values
Your app reads these values as environment variables. The token exchange module uses the first group. The second group is specific to Amazon Bedrock.
Okta values (used by the token exchange):
| Environment variable | Description | Where to find it |
|---|---|---|
OKTA_DOMAIN | Okta org domain, for example example.okta.com (no https:// prefix) | Admin Console > Settings > Account |
OKTA_CUSTOM_AS_ID | Custom authorization server ID, for example default | Security > API |
OKTA_SCOPE | The custom scope that the agent requests | Custom AS > Scopes |
AGENT_CLIENT_ID | Client ID of the imported third-party AI Agent, for example wlp9k6... | Directory > AI Agents > (yourAgent) |
AGENT_KEY_ID | kid of the public JWK registered on the third-party AI agent | Directory > AI Agents > (yourAgent) > Credentials |
AGENT_PRIVATE_KEY_JWK | The third-party agent's private JWK (single-line JSON) | Output of Generate credentials. Store the value in a secrets manager |
Amazon values (used by the platform integration):
| Environment variable | Description | Where to find it |
|---|---|---|
BEDROCK_AGENT_ID | The Bedrock Classic Agent to invoke | AWS Console > Bedrock > Agents |
BEDROCK_AGENT_ALIAS_ID | The alias of the Bedrock Classic Agent | AWS Console > Bedrock > Agents > Aliases |
AWS_REGION, AWS_DEFAULT_REGION | The AWS region where the Bedrock Classic Agent runs | AWS Console |
Note: Set both
AWS_REGIONandAWS_DEFAULT_REGION. Your code passesAWS_REGIONasregion_nameto the boto3 client. The boto3 internals readAWS_DEFAULT_REGIONwhen refreshing SSO credentials. Omitting it causes aNoRegionError.
Add Okta authentication to your agent
The following example token_exchange.py module that you create here has no dependency on AWS or Amazon Bedrock.
Install the token exchange dependencies
The module needs only a JWT library and an HTTP client. Add these to your project's requirements.txt:
PyJWT[crypto]>=2.8.0
requests>=2.31.0 Create the token exchange module
Create a file named token_exchange.py. It reads the Okta values from the environment, signs the client assertion, and exposes two functions, get_id_jag and get_access_token, that your agent calls in order.
"""Okta token exchange for AI agents.
Turns a signed-in user's id_token into a scoped access_token:
id_token -> ID-JAG (org AS) -> access_token (custom AS)
Exposes get_id_jag() and get_access_token(). No platform dependencies.
"""
import json, os, time, uuid
import jwt
import requests
from jwt.algorithms import RSAAlgorithm
# --- Okta configuration (from environment) ---
OKTA_DOMAIN = os.environ["OKTA_DOMAIN"] # for example, example.okta.com
CUSTOM_AS_ID = os.environ.get("OKTA_CUSTOM_AS_ID", "default")
REQUESTED_SCOPE = os.environ.get("OKTA_SCOPE", "xaa:read")
AGENT_CLIENT_ID = os.environ["AGENT_CLIENT_ID"]
AGENT_KEY_ID = os.environ["AGENT_KEY_ID"]
AGENT_PRIVATE_KEY_JWK = json.loads(os.environ["AGENT_PRIVATE_KEY_JWK"])
ORG_TOKEN_URL = f"https://{OKTA_DOMAIN}/oauth2/v1/token"
CUSTOM_AS_TOKEN_URL = f"https://{OKTA_DOMAIN}/oauth2/{CUSTOM_AS_ID}/v1/token"
CUSTOM_AS_AUDIENCE = f"https://{OKTA_DOMAIN}/oauth2/{CUSTOM_AS_ID}"
def build_client_assertion(audience: str) -> str:
"""Sign a short-lived client assertion JWT for the given token endpoint."""
private_key = RSAAlgorithm.from_jwk(json.dumps(AGENT_PRIVATE_KEY_JWK))
now = int(time.time())
return jwt.encode(
{
"iss": AGENT_CLIENT_ID,
"sub": AGENT_CLIENT_ID,
"aud": audience, # must match the endpoint this assertion is sent to
"iat": now,
"exp": now + 300, # valid for 5 minutes
"jti": str(uuid.uuid4()),
},
private_key,
algorithm="RS256",
headers={"kid": AGENT_KEY_ID},
)
def get_id_jag(id_token: str) -> str:
"""Step 1: exchange the user's id_token for an ID-JAG at the org AS."""
r = requests.post(ORG_TOKEN_URL, data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": build_client_assertion(ORG_TOKEN_URL),
"subject_token": id_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
"requested_token_type": "urn:ietf:params:oauth:token-type:id-jag",
"scope": REQUESTED_SCOPE,
"audience": CUSTOM_AS_AUDIENCE,
}, timeout=10)
r.raise_for_status()
return r.json()["access_token"] # the ID-JAG
def get_access_token(id_jag: str) -> str:
"""Step 2: exchange the ID-JAG for a scoped access token at the custom AS."""
r = requests.post(CUSTOM_AS_TOKEN_URL, data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": build_client_assertion(CUSTOM_AS_TOKEN_URL),
"assertion": id_jag,
}, timeout=10)
r.raise_for_status()
return r.json()["access_token"] # scoped access token for the resource A few details that this module encodes:
-
The client assertion function is invoked twice.
build_client_assertionis called once per step, each time with theaudset to the token endpoint it targets: the org token URL for Step 1, and the custom authorization server token URL for Step 2. Thekidheader must match the public JWK registered on the agent. -
The
audienceparameter in Step 1 is the custom authorization server's issuer URL (https://{yourOktaDomain}/oauth2/{custom-as-id}), not its token endpoint. - Step 1 requires the Okta imported AI Agent client. An OIDC app client can't perform this exchange.
Note: For production workloads, cache the ID-JAG and access token in process until their
expclaim expires. This avoids a fresh two-step exchange on every user request.
Set up your Amazon Bedrock Classic Agent
Enable model access
Foundation models aren't enabled by default. If your model shows as unavailable:
- In the AWS console, go to Amazon Bedrock > Model access.
- Click Request access for the model you want, for example
amazon.titan-text-lite-v1.
Note: There's no CLI command for requesting model access. Use the AWS console. To verify access through CLI after approval:
aws bedrock get-foundation-model \ --model-identifier amazon.titan-text-lite-v1 \ --region us-east-1
Create the agent
- In the AWS console, open the Amazon Bedrock console (opens new window). Confirm that you're in a Region that supports Amazon Bedrock Classic agents (opens new window).
- In the navigation pane, under Builder tools, choose Agents, then choose Create agent.
- Enter a name for your agent (for example,
MyBedrockAgent), and then choose Create. The Agent builder pane opens. - In the Agent details section:
- For Agent resource role, select Create and use a new service role.
- For Select model, choose a foundation model, for example, Claude 3 Haiku.
- Add your Instructions for the Agent. This field can't be empty. Describe what the agent does.
- Choose Save.
- (Optional) Add an action group so the agent can call Okta-protected APIs through a Lambda function:
Choose the Action groups tab, then choose Add.
Enter a name for the action group. For Action group type, select Define with API schemas. For Action group invocation, choose Select an existing Lambda function, then select your Lambda function.
Provide an OpenAPI schema that describes the Lambda's endpoint, review your configuration, and choose Create.
Inside the Lambda, retrieve the Okta token from session attributes to call Okta-protected APIs:
def lambda_handler(event, context): access_token = event.get("sessionAttributes", {}).get("okta_access_token") user_email = event.get("sessionAttributes", {}).get("user_email") resp = requests.get( "https://yourOrg.okta.com/api/v1/users/me", headers={"Authorization": f"Bearer {access_token}"}, ) # ...Note: Ensure that your Lambda function has outbound internet access (through a NAT Gateway or secure route) to reach your Okta organization's API endpoints.
In the IAM console, open the agent's service role (linked from Agent overview > Permissions) and add an inline policy granting
lambda:InvokeFunctionon your Lambda's ARN, so the agent is authorized to invoke it.
- Choose Save, then choose Prepare to prepare the agent.
- Choose Save and exit.
- Note the Agent ID and create an Alias. Note the Alias ID.
Important: Scope the Lambda's execution role to least privilege: grant it only the specific downstream APIs it needs to call, run it in a VPC if it needs network isolation, and monitor it with CloudWatch and CloudTrail. See AWS IAM Best Practices (opens new window) and Implementing least privilege access for Amazon Bedrock (opens new window).
Import your Bedrock Classic Agent into Okta
Importing the agent lets it appear in Directory > AI Agents for visibility and governance, such as access certifications. This is separate from the AI Agent identity that you registered in Before you begin, which is the credential your app uses to perform the token exchange.
In AWS, create an IAM user dedicated to the import, for example
okta-ai-agent-import.Attach an inline policy that grants only read access to list and describe agents:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "OktaAIAgentImport", "Effect": "Allow", "Action": [ "sts:GetCallerIdentity", "bedrock:GetAgent", "bedrock:ListAgents" ], "Resource": "*" } ] }Generate an access key for the IAM user and store it in a secrets manager.
In the Admin Console, configure the AI agent import with the access key, the AWS regions where your agents run, and AWS Bedrock Classic Agents as the platform. Test the connection and save.
Configure your app
Project structure
bedrock-agent-app/
├── main.py # Token exchange + Bedrock Classic Agent invocation
├── token_exchange.py # Okta token exchange module
├── requirements.txt
├── .env # Secrets (gitignored)
└── .env.example # Template
Add the Bedrock dependencies
Add these to the same requirements.txt, alongside the token exchange dependencies:
boto3>=1.34.0
botocore[crt]
python-dotenv>=1.0.0
Note: Your project requires
botocore[crt]when your AWS credentials use the SSO login credential provider. Without it, the runtime fails at startup withModuleNotFoundError: awscrt.
Install the complete set of dependencies:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Invoke the Bedrock Classic Agent with the access token
After the token exchange, invoke the agent and pass the access_token and the user's claims as session attributes. A Lambda action group on the agent reads those attributes and forwards the token as Authorization: Bearer <access_token> to an Okta-protected resource.
import os
import uuid
import boto3
BEDROCK_AGENT_ID = os.environ["BEDROCK_AGENT_ID"]
BEDROCK_AGENT_ALIAS_ID = os.environ["BEDROCK_AGENT_ALIAS_ID"]
AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")
def invoke_bedrock_agent(prompt: str, user_claims: dict, access_token: str) -> str:
client = boto3.client("bedrock-agent-runtime", region_name=AWS_REGION)
response = client.invoke_agent(
agentId=BEDROCK_AGENT_ID,
agentAliasId=BEDROCK_AGENT_ALIAS_ID,
sessionId=str(uuid.uuid4()), # reuse across turns for multi-turn sessions
inputText=prompt,
sessionState={
"sessionAttributes": {
"okta_access_token": access_token,
"user_name": user_claims.get("name", ""),
"user_email": user_claims.get("email", ""),
"user_sub": user_claims.get("sub", ""),
}
},
)
# The response is a streaming EventStream.
chunks = [
event["chunk"]["bytes"].decode()
for event in response["completion"]
if "chunk" in event
]
return "".join(chunks)
Note: The IAM identity running this code needs the
bedrock:InvokeAgentpermission on the target agent. This is a separate, narrower permission than the read-only import policy in Import your Bedrock Classic Agent into Okta.
Wire it into an entry point
In your app's entry point, call the two token exchange functions in order, decode the user's identity claims from the id_token, and then invoke the Bedrock Classic Agent. The following example main.py imports the reusable token exchange module and adds only the AWS-specific wiring:
import json
import sys
import jwt
from token_exchange import get_id_jag, get_access_token
# invoke_bedrock_agent from the previous step
def main():
payload = json.loads(sys.argv[1])
id_token = payload["id_token"]
prompt = payload["prompt"]
# Okta authentication
id_jag = get_id_jag(id_token)
access_token = get_access_token(id_jag)
# The id_token was already verified by the org authorization server in
# Step 1. Decoding it here only reads display claims for the session
# attributes. It isn't used to make an authorization decision.
user_claims = jwt.decode(id_token, options={"verify_signature": False})
# Platform integration (Amazon Bedrock)
answer = invoke_bedrock_agent(prompt, user_claims, access_token)
print(json.dumps({
"ok": True,
"user": user_claims.get("name"),
"access_token_prefix": access_token[:10],
"answer": answer,
}))
if __name__ == "__main__":
main()
Verify the configuration
After you add the code, verify the Okta-side configuration:
- Go to Directory > AI Agents and confirm that the agent appears with Status: Active and the expected owners, connections, and user app.
- (Optional) Go to Identity Governance > Access Certifications to confirm that the agent's user sign-on app is visible for future certification campaigns.
Obtain a test ID token
To exercise the flow, you need an ID token from the OIDC app linked to the agent. Complete an OIDC sign-in against that app to obtain one. For an example Authorization Code with PKCE sign-in helper, see Create an app to obtain a test ID token.
Note: Add the helper's callback URL (for example,
http://localhost:8765/callback) to the linked OIDC app's Sign-in redirect URIs before you run it, and remove it after verification is complete.
Run an end-to-end invocation
Run main.py locally, passing the test ID token to confirm the full id_token → ID-JAG → access_token round trip:
source venv/bin/activate
python main.py "{\"id_token\": \"$ID_TOKEN\", \"prompt\": \"Who am I?\"}"
A successful response appears as follows and confirms the full round trip:
{
"ok": true,
"user": "Jessie Smith",
"access_token_prefix": "eyJraWQiOiI...",
"answer": "Hello! How can I help you today?"
}
Useful AWS CLI commands
# List your Bedrock Classic Agents
aws bedrock list-agents --region us-east-1
# Find an Agent's Alias IDs
aws bedrock list-agent-aliases \
--agent-id <BEDROCK_AGENT_ID> \
--region us-east-1
# Invoke the agent directly, bypassing main.py
aws bedrock-agent-runtime invoke-agent \
--agent-id <BEDROCK_AGENT_ID> \
--agent-alias-id <BEDROCK_AGENT_ALIAS_ID> \
--session-id test-session-1 \
--input-text "Hello" \
--region us-east-1 \
outfile.json
# Check Lambda logs for action groups
aws logs tail /aws/lambda/<action-group-function-name> \
--follow \
--region us-east-1
Troubleshooting
The following errors are specific to the Amazon Bedrock integration:
| Error | Root cause | Fix |
|---|---|---|
NoRegionError: You must specify a region | The boto3 SSO credential refresher needs AWS_DEFAULT_REGION | Set both AWS_REGION and AWS_DEFAULT_REGION in the environment |
ModuleNotFoundError: awscrt at startup | Missing the CRT extension required by the SSO credential provider | Run pip install botocore[crt] |
ThrottlingException on InvokeAgent | Bedrock model invocation quota exceeded (often 0 on new accounts) | Check Service Quotas. A quota of 0 means that the model is disabled for the account |
Agent Instruction cannot be null | The Bedrock Classic Agent has no instructions | In the AWS console, edit the agent to add an instruction, then choose Prepare |
ResourceNotFoundException on InvokeAgent | Wrong agent ID or alias ID | Verify BEDROCK_AGENT_ID and BEDROCK_AGENT_ALIAS_ID in the AWS console |
The following errors come from the Okta token exchange and are covered in Set up third-party AI Agent token exchange: Troubleshooting:
invalid_scope: openid not allowedinvalid_client: JWKSet not configuredinvalid_client: kid is invalidaccess_denied: no_matching_policyOnly service apps can use client_credentials
Next steps
Your Bedrock Classic Agent can now authenticate as a user and call Okta-protected APIs on their behalf. To define which resources and scopes the agent is permitted to reach, see Set up AI agent token exchange and the Okta for AI Agents documentation on governing access to AI agents.