On this page

Secure Amazon Bedrock Classic Agents with Okta

Identity Engine

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


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:

    1. Exchange the user's id_token for an Identity Assertion JWT authorization grant (ID-JAG) at the org authorization server.
    2. Exchange the ID-JAG for a scoped access_token at 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.

  • 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_token your app exchanges. Use the Authorization Code grant type and the openid profile email scopes. The id_token must have an aud claim equal to this app's client ID.

  • A custom authorization server. Use the built-in default server 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 an invalid_scope error, so you must request a custom scope instead.

  • Your agent imported into Okta as an AI Agent identity that uses private_key_jwt client 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_REGION and AWS_DEFAULT_REGION. Your code passes AWS_REGION as region_name to the boto3 client. The boto3 internals read AWS_DEFAULT_REGION when refreshing SSO credentials. Omitting it causes a NoRegionError.

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_assertion is called once per step, each time with the aud set to the token endpoint it targets: the org token URL for Step 1, and the custom authorization server token URL for Step 2. The kid header must match the public JWK registered on the agent.
  • The audience parameter 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 exp claim 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:

  1. In the AWS console, go to Amazon Bedrock > Model access.
  2. 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

  1. 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).
  2. In the navigation pane, under Builder tools, choose Agents, then choose Create agent.
  3. Enter a name for your agent (for example, MyBedrockAgent), and then choose Create. The Agent builder pane opens.
  4. 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.
  5. Choose Save.
  6. (Optional) Add an action group so the agent can call Okta-protected APIs through a Lambda function:
    1. Choose the Action groups tab, then choose Add.

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

    3. Provide an OpenAPI schema that describes the Lambda's endpoint, review your configuration, and choose Create.

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

    5. In the IAM console, open the agent's service role (linked from Agent overview > Permissions) and add an inline policy granting lambda:InvokeFunction on your Lambda's ARN, so the agent is authorized to invoke it.

  7. Choose Save, then choose Prepare to prepare the agent.
  8. Choose Save and exit.
  9. 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.

  1. In AWS, create an IAM user dedicated to the import, for example okta-ai-agent-import.

  2. 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": "*"
        }
      ]
    }
    
  3. Generate an access key for the IAM user and store it in a secrets manager.

  4. 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 with ModuleNotFoundError: 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:InvokeAgent permission 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:

  1. Go to Directory > AI Agents and confirm that the agent appears with Status: Active and the expected owners, connections, and user app.
  2. (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 allowed
  • invalid_client: JWKSet not configured
  • invalid_client: kid is invalid
  • access_denied: no_matching_policy
  • Only 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.

See also