diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-amazon-bedrock/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-amazon-bedrock/index.md new file mode 100644 index 00000000000..dfdaa883f60 --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-amazon-bedrock/index.md @@ -0,0 +1,7 @@ +--- +title: Secure an Amazon Bedrock AgentCore agent +excerpt: This guide discusses how to secure an Amazon Bedrock AgentCore agent +layout: Guides +sections: + - main +--- \ No newline at end of file diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-amazon-bedrock/main/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-amazon-bedrock/main/index.md new file mode 100644 index 00000000000..d80d7645f76 --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-amazon-bedrock/main/index.md @@ -0,0 +1,349 @@ +--- +title: Secure an Amazon Bedrock AgentCore agent +excerpt: Learn how to add Okta authentication to an existing Amazon Bedrock AgentCore agent +layout: Guides +--- + + +This guide explains how to add Okta authentication to an Amazon Bedrock AgentCore agent that you've already built. It assumes that you have a working agent and can edit its code. The focus is on the Okta authentication that you add so that the agent can act on a signed-in user's behalf. + +The Okta authentication is a two-step token exchange that's the same for any AI agent, regardless of the platform it runs on. This guide first introduces what the integration needs to do and provides sample code functions that implements the authentication. It then shows the Amazon Bedrock-specific code and configuration that consume it. + +> **Note**: To enable AI agent token exchange, you must first subscribe to Okta for AI Agents. See your Okta account team to enable the feature. + +--- + +#### Learning outcomes + +* Understand what a third-party AI agent must do to authenticate as a signed-in user with Okta. +* Add a token exchange module to your agent. +* Wire the token exchange into an Amazon Bedrock AgentCore agent and call a downstream Bedrock agent with the resulting access token. +* Verify and test the end-to-end flow with a real Okta ID token. + +#### What you need + +* An [Identity Engine](/docs/concepts/oie-intro/) org with the Okta for AI Agents feature enabled +* An existing Amazon Bedrock AgentCore agent that you can edit and deploy +* The Amazon Bedrock AgentCore agent imported into Okta +* [Python](https://www.python.org/) 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 agent exchanges the user's identity for a short-lived, narrowly scoped access token, and then uses that token to call protected resources. + +The integration has two parts: + +* Okta authentication. The agent performs a two-step token exchange: + 1. Sign a client assertion with the agent's private key. + 1. Exchange the user's `id_token` for an Identity Assertion JWT authorization grant (ID-JAG) at the org authorization server. + 1. 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](#add-okta-authentication-to-your-agent). + +* Platform integration (Amazon Bedrock-specific). Your agent calls the token exchange and then attaches the access token to its downstream calls. For AgentCore, this means passing the token to a Bedrock agent as a session attribute. See [Integrate the token exchange into your AgentCore agent](#integrate-the-token-exchange-into-your-agentcore-agent). + +```text +User + { "prompt": "...", "id_token": "" } + | + v +Okta authentication (token_exchange.py) + Step 1: id_token -> ID-JAG (Org AS: /oauth2/v1/token) + Step 2: ID-JAG -> access_token (Custom AS: /oauth2/{custom-as-id}/v1/token) + | + v +Platform integration (Amazon Bedrock AgentCore: agent.py) + invoke_agent(..., sessionAttributes={ "oktaAccessToken": access_token }) + | + v +Downstream resource (Bedrock agent, MCP server, or Okta-protected API) + Authorization: Bearer +``` + +For the conceptual background on AI agent token exchange, see [Set up AI agent token exchange](/docs/guides/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](/docs/guides/ai-agent-third-party-token-exchange/). + +* An OIDC web app integration that signs users in and issues the `id_token` your agent 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`. +* The Bedrock AgentCore 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 Amazon Bedrock AgentCore agent code reads these values as environment variables. The first group is consumed by the token exchange module. 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 the agent requests | Custom AS > **Scopes** | +| `AGENT_CLIENT_ID` | Client ID of the imported third-party AI Agent | **Directory** > **AI Agents** > *(agent)* | +| `AGENT_KEY_ID` | `kid` of the public JWK registered on the third-party AI agent | **Directory** > **AI Agents** > *(agent)* > **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 Bedrock values (used by the platform integration):** + +| Environment variable | Description | Where to find it | +| --- | --- | --- | +| `BEDROCK_AGENT_ID` | The downstream Bedrock agent to invoke | **AWS Console** > **Bedrock** > **Agents** | +| `BEDROCK_AGENT_ALIAS_ID` | The alias of the downstream Bedrock agent | **AWS Console** > **Bedrock** > **Agents** > **Aliases** | +| `AWS_REGION`, `AWS_DEFAULT_REGION` | The AWS Region where the Bedrock agent runs | **AWS Console** | + +> **Note:** Set both `AWS_REGION` and `AWS_DEFAULT_REGION`. The `botocore[crt]` credential refresher requires `AWS_DEFAULT_REGION`. Omitting this value causes a `NoRegionError`. + +## Add Okta authentication to your agent + +The following example `token_exchange.py` module that you create here has no dependency on Amazon Bedrock or AWS. + +### Install the token exchange dependencies + +The module needs only a JWT library and an HTTP client. Add these to your project's `requirements.txt`: + +```text +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. + +```python +"""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. + +## Integrate the token exchange into your AgentCore agent + +This section is specific to Amazon Bedrock AgentCore. Here you call `get_id_jag` and `get_access_token` from your agent and attach the resulting access token to the downstream Bedrock call. + +### Add the Bedrock dependencies + +Your AgentCore agent needs the following. Add these to the same `requirements.txt`, alongside the token exchange dependencies: + +```text +bedrock-agentcore +boto3 +botocore[crt] +``` + +> **Note:** `botocore[crt]` is required 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: + +```bash +pip install -r requirements.txt +``` + +### Call the downstream Bedrock agent with the access token + +Pass the scoped access token to the downstream Bedrock agent as a session attribute (`oktaAccessToken`). A Lambda action group on the Bedrock agent reads that attribute and forwards it as `Authorization: Bearer ` to an Okta-protected resource, such as an MCP server. + +```python +import os +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, access_token: str, session_id: 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=session_id, + inputText=prompt, + sessionState={"sessionAttributes": {"oktaAccessToken": access_token}}, + ) + chunks = [] + for event in response["completion"]: + if "chunk" in event: + chunks.append(event["chunk"]["bytes"].decode("utf-8")) + return "".join(chunks) +``` + +Two AWS runtime requirements apply: + +* The IAM identity running the AgentCore runtime must have the `bedrock:InvokeAgent` permission on the target Bedrock agent. +* Both `AWS_REGION` and `AWS_DEFAULT_REGION` must be set, because the `botocore[crt]` credential refresher requires `AWS_DEFAULT_REGION`. + +### Wire it into the AgentCore entry point + +In your agent's entry point, call the two token exchange functions in order, and then invoke the downstream Bedrock agent with the access token. The following `agent.py` imports the reusable module and adds only the AgentCore-specific wiring: + +```python +import uuid +from bedrock_agentcore.runtime import BedrockAgentCoreApp + +from token_exchange import get_id_jag, get_access_token +# invoke_bedrock_agent from the previous step + +app = BedrockAgentCoreApp() + + +@app.entrypoint +def handler(payload: dict) -> dict: + """Expected payload: {"prompt": "...", "id_token": ""}.""" + id_token = payload["id_token"] + prompt = payload["prompt"] + session_id = payload.get("session_id") or str(uuid.uuid4()) + + # Okta authentication + id_jag = get_id_jag(id_token) + access_token = get_access_token(id_jag) + + # Platform integration (Amazon Bedrock) + answer = invoke_bedrock_agent(prompt, access_token, session_id) + return {"answer": answer, "session_id": session_id} + + +if __name__ == "__main__": + app.run() +``` + +## 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 application. +1. (Optional) Go to **Identity Governance** > **Access Certifications** to confirm that the agent's user sign-on application is visible for future certification campaigns. + +## Obtain a test ID token + +To exercise the flow, you need an ID token from the OIDC application linked to the agent. Complete an OIDC sign-in against that application to obtain one. For a ready-to-run Authorization Code with PKCE sign-in helper, see [Create an app to obtain a test ID token](/docs/guides/ai-agent-third-party-token-exchange/main/#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 application's **Sign-in redirect URIs** before you run it, and remove it after verification is complete. + +## Run an end-to-end invocation + +Run the AgentCore runtime locally, then deploy it, passing the test ID token to confirm the full `id_token` → ID-JAG → `access_token` round trip: + +```bash +# Start the runtime locally. While this runs, `invoke` hits the local instance. +agentcore dev +agentcore invoke "{\"prompt\": \"Who am I?\", \"id_token\": \"$ID_TOKEN\"}" + +# Stop `agentcore dev`, deploy to AWS, then `invoke` hits the deployed instance. +agentcore deploy +agentcore invoke "{\"prompt\": \"Who am I?\", \"id_token\": \"$ID_TOKEN\"}" +``` + +A successful response is shaped as follows and confirms the full round trip. Reuse the returned `session_id` on any follow-up invocation to keep the Bedrock conversation state: + +```json +{ + "answer": "You are signed in as jessie.smith@example.com.", + "session_id": "3e4f1b9a-7c26-4d88-9e42-1a0b5c9d2f3e" +} +``` + +## Troubleshooting + +The following errors are specific to the Amazon Bedrock integration: + +| Error | Root cause | Fix | +| --- | --- | --- | +| `ResourceNotFoundException` on `InvokeAgent` | Wrong agent ID or alias ID | Verify `BEDROCK_AGENT_ID` and `BEDROCK_AGENT_ALIAS_ID` in the AWS Console | +| `ThrottlingException` on `InvokeAgent` | Bedrock model invocation quota exceeded (often `0` on new accounts) | Check **Service Quotas**; a quota of `0` means the model is disabled for the account | +| `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 agent runtime environment | +| `ModuleNotFoundError: awscrt` at startup | Missing the CRT extension required by the SSO credential provider | Run `pip install botocore[crt]` | +| `Agent Instruction cannot be null` | The Bedrock agent was created without instructions | In the AWS Console, edit the agent to add an instruction, then choose **Prepare** | + +## Next steps + +Your agent can now authenticate as a user and call Okta-protected resources on their behalf. To define which resources and scopes the agent is permitted to reach, see [Set up AI agent token exchange](/docs/guides/ai-agent-token-exchange/) and the Okta for AI Agents documentation on governing access to AI agents. + +## See also + +* [Set up AI agent token exchange](/docs/guides/ai-agent-token-exchange/) +* [Set up third-party AI Agent token exchange](/docs/guides/ai-agent-third-party-token-exchange/) +* [Amazon Bedrock AgentCore documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html) diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-aws-bedrock/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-aws-bedrock/index.md new file mode 100644 index 00000000000..6753fdc8008 --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-aws-bedrock/index.md @@ -0,0 +1,7 @@ +--- +title: Secure AWS Bedrock Agents with Okta +excerpt: This guide discusses how to secure AWS Bedrock Agents with Okta +layout: Guides +sections: + - main +--- \ No newline at end of file diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-aws-bedrock/main/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-aws-bedrock/main/index.md new file mode 100644 index 00000000000..a29c67e4be2 --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-aws-bedrock/main/index.md @@ -0,0 +1,28 @@ +--- +title: Secure AWS Bedrock Agents with Okta +excerpt: Learn how to secure AWS Bedrock Agents with Okta +layout: Guides +--- + + +To come, opening blurb. + +> **Note**: To enable AI agent token exchange, you must first subscribe to Okta for AI Agents. See your Okta account team to enable the feature. + +--- + +#### Learning outcomes + +- TBC + +#### What you need + +- TBC +- TBC + +--- + +## Overview + + + diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-azure/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-azure/index.md new file mode 100644 index 00000000000..31f20dc6bd1 --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-azure/index.md @@ -0,0 +1,7 @@ +--- +title: Secure Azure AI Foundry agents with Okta +excerpt: This guide discusses how to secure Azure AI Foundry agents with Okta +layout: Guides +sections: + - main +--- \ No newline at end of file diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-azure/main/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-azure/main/index.md new file mode 100644 index 00000000000..02f931f56cd --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-azure/main/index.md @@ -0,0 +1,25 @@ +--- +title: Secure Azure AI Foundry agents with Okta +excerpt: Learn how to secure Azure AI Foundry agents with Okta +layout: Guides +--- + + +To come, opening blurb. + +> **Note**: To enable AI agent token exchange, you must first subscribe to Okta for AI Agents. See your Okta account team to enable the feature. + +--- + +#### Learning outcomes + +- TBC + +#### What you need + +- TBC +- TBC + +--- + +## Overview diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-third-party/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-third-party/index.md new file mode 100644 index 00000000000..9a9d7458bd7 --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-third-party/index.md @@ -0,0 +1,7 @@ +--- +title: Secure third-party AI agents +excerpt: This guide discusses how to secure third-party AI agents +layout: Guides +sections: + - main +--- \ No newline at end of file diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-third-party/main/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-third-party/main/index.md new file mode 100644 index 00000000000..18c7054a8eb --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-secure-third-party/main/index.md @@ -0,0 +1,105 @@ +--- +title: Secure third-party AI agents +excerpt: Learn how to secure third-party and imported AI agents with Okta +layout: Guides +--- + + +Okta's AI Agents feature secures third-party and imported AI agents with delegated user identity. When a user signs in with Okta, your agent exchanges that user's identity for a short-lived, scoped access token, and then uses it to call protected resources on the user's behalf. + +This guide is the platform-agnostic overview of that integration. It explains what you do from an Okta perspective and, at a high level, what an agent developer does in code. The specifics of wiring the access token into a running agent depend on your agent platform, and you implement them using your platform's own tools and documentation. + +> **Note**: To enable AI agent token exchange, you must first subscribe to Okta for AI Agents. See your Okta account team to enable the feature. + +--- + +#### Learning outcomes + +* Understand the two parts of securing an AI agent: Okta authentication (the same for every platform) and platform integration (specific to your agent's runtime). +* Know what to configure in Okta to enable the token exchange. +* Add the platform-agnostic token exchange module to your agent. +* Know where the platform-specific work begins and which guide to follow. + +#### What you need + +* An [Identity Engine](/docs/concepts/oie-intro/) org with the Okta for AI Agents feature enabled +* A third-party AI agent that you can edit and deploy +* [Python](https://www.python.org/) 3.10 or later for the token exchange module + +--- + +## 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 agent exchanges the user's identity for a short-lived, narrowly scoped access token, and then uses that token to call protected resources. + +Securing any agent breaks into two parts: + +* **Okta authentication (platform-agnostic).** The agent performs a two-step token exchange — it turns the user's `id_token` into an Identity Assertion JWT authorization grant (ID-JAG) at the org authorization server, then turns the ID-JAG into a scoped `access_token` at a custom authorization server. This logic is identical for every agent, so you add it once as a reusable module. + +* **Platform integration (platform-specific).** Your agent calls the token exchange and then attaches the resulting access token to the calls it makes — as a session attribute, a request header, or whatever the platform expects. This part differs per platform, so you implement it using your agent platform's own tools and documentation. + +```text +User + signs in with Okta -> receives id_token + | + v +Okta authentication (platform-agnostic) + Step 1: id_token -> ID-JAG (Org AS: /oauth2/v1/token) + Step 2: ID-JAG -> access_token (Custom AS: /oauth2/{custom-as-id}/v1/token) + | + v +Platform integration (platform-specific) + attach access_token to the agent's downstream calls + | + v +Okta-protected resource (API, MCP server, or another agent) + Authorization: Bearer +``` + +For the underlying concepts and the token exchange API details, see [Set up AI agent token exchange](/docs/guides/ai-agent-token-exchange/). + +## How securing an agent works + +At a high level, securing any third-party or imported AI agent involves three stages: + +1. **Configure Okta.** Set up the Okta objects that make the token exchange possible. See [Configure Okta](#configure-okta). +1. **Add Okta authentication to your agent.** Drop the reusable token exchange module into your agent's code. See [Add Okta authentication to your agent](#add-okta-authentication-to-your-agent). +1. **Integrate with your platform.** Call the token exchange and attach the access token to your agent's downstream calls. The specifics depend on your platform. See [Integrate with your platform](#integrate-with-your-platform). + +## Configure Okta + +This is the work you do from an Okta perspective. It's the same regardless of platform. Configure the following objects, then collect their identifiers for your agent's environment. For step-by-step instructions, see [Set up third-party AI Agent token exchange](/docs/guides/ai-agent-third-party-token-exchange/). + +* **An OIDC web app integration** that signs users in and issues the `id_token` your agent exchanges. The `id_token`'s `aud` claim must equal this app's client ID. +* **A custom authorization server** with a **custom scope** (such as `xaa:read`) that the agent requests. + + > **Note:** System scopes (`openid`, `profile`, `email`) are stripped during the ID-JAG exchange and cause an `invalid_scope` error. Request only a non-system custom scope. + +* **An AI Agent identity in Okta** — the machine identity whose key signs the token exchange requests. For a third-party agent that runs on a cloud provider, you import the agent into Okta; you can also register one manually for testing. Register its public key, link the OIDC web app, set the custom authorization server, and activate it. +* **An access policy rule** on the custom authorization server that enables the JWT Bearer grant type, allows the AI Agent identity as a client, and includes the audience, the custom scope, and a user or group condition. + +When you finish, you have the values your agent reads as environment variables: `OKTA_DOMAIN`, `OKTA_CUSTOM_AS_ID`, `OKTA_SCOPE`, `AGENT_CLIENT_ID`, `AGENT_KEY_ID`, and `AGENT_PRIVATE_KEY_JWK`. + +## Add Okta authentication to your agent + +This is the platform-agnostic half of the agent code. The following example `token_exchange.py` module has no dependency on any agent platform. You can add it once and reuse it unchanged in any Python agent. + + + +## Integrate with your platform + +This is the platform-specific half. Regardless of platform, your agent code does two things: + +1. **Obtain the access token.** Call `get_id_jag()` and then `get_access_token()` from the module above, passing the user's `id_token`. +1. **Attach the access token to the agent's downstream calls.** How you pass the token depends on the platform. For example, as a session attribute on an agent invocation, or as an `Authorization: Bearer` header on an outbound request. An action or tool the agent runs then uses that token to call the Okta-protected resource. + +The exact entry point, SDK calls, and configuration differ per platform. Implement this step using your agent platform's own tools and documentation. + +## Next steps + +After your agent can authenticate as a user and call protected resources, define which resources and scopes it's allowed to reach. See [Set up AI agent token exchange](/docs/guides/ai-agent-token-exchange/) and the Okta for AI Agents documentation on governing access to AI agents. + +## See also + +* [Set up AI agent token exchange](/docs/guides/ai-agent-token-exchange/) +* [Set up third-party AI Agent token exchange](/docs/guides/ai-agent-third-party-token-exchange/) diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-third-party-token-exchange/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-third-party-token-exchange/index.md new file mode 100644 index 00000000000..d166a4a21cf --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-third-party-token-exchange/index.md @@ -0,0 +1,7 @@ +--- +title: Set up third-party AI Agent token exchange +excerpt: This guide discusses how to set up third-party AI Agent token exchange +layout: Guides +sections: + - main +--- \ No newline at end of file diff --git a/packages/@okta/vuepress-site/docs/guides/ai-agent-third-party-token-exchange/main/index.md b/packages/@okta/vuepress-site/docs/guides/ai-agent-third-party-token-exchange/main/index.md new file mode 100644 index 00000000000..0de7484faa0 --- /dev/null +++ b/packages/@okta/vuepress-site/docs/guides/ai-agent-third-party-token-exchange/main/index.md @@ -0,0 +1,766 @@ +--- +title: Set up third-party AI Agent token exchange +excerpt: Learn how to configure token exchange for third-party AI agents to securely access protected resources. +layout: Guides +--- + + +Okta's AI Agents feature secures third-party AI agents with delegated user identity. When a user authenticates with Okta, your app exchanges the user's identity token for a scoped access token. The AI agent can then call Okta-protected APIs on the user's behalf. + +In this guide, learn how to configure token exchange for third-party AI agents. + +> **Note**: To enable AI agent token exchange, you must first subscribe to Okta for AI Agents. See your Okta account team to enable the feature. + +--- + +#### Learning outcomes + +- Understand Okta's two-step cross app access (XAA) token exchange flow for AI agents. +- Understand how to set up the token exchange flow. +- Test the third-party token exchange flow. + +#### What you need + +- An Okta org that's subscribed to Okta for AI Agents. +- An Okta user account with the super admin role. + +--- + +## Overview + +Okta's token exchange uses two API calls. The user's ID token is exchanged for an ID-JAG at the org authorization server. The ID-JAG is then exchanged for a scoped access token at the custom authorization server. The calling app passes that token to the AI agent. + + + +```bash +Diagram? From Vicky? + +User + authenticates via Okta OIDC web app → receives id_token + ↓ +Application + Step 1: id_token → ID-JAG (Org AS — POST /oauth2/v1/token) + Step 2: ID-JAG → access_token (Custom AS — POST /oauth2/{as-id}/v1/token) + ↓ +Third-party AI agent + receives: access_token + user claims (name, email, sub) + ↓ +Okta-protected API + Authorization: Bearer +``` + +>**Note:** No gateway or proxy is involved. The calling app owns the full token exchange. The AI agent receives a ready-to-use access token. + +The machine identity that signs token exchange requests is the third-party AI Agent imported in the Admin Console. The AI Agent authenticates both steps of the exchange using a private key JWT. + +### Supported platforms + +The following third-party AI Agent platforms are supported: + +| Provider | Platform | Guide | +| --- | --- | --- | +| Amazon Web Services | AWS Bedrock Agents | AWS Bedrock Agents guide | +| Amazon Web Services | AWS Bedrock AgentCore | AWS Bedrock AgentCore guide | +| Microsoft | Azure AI Foundry | Azure AI Foundry guide | + +## Setting up the third-party token flow + +To configure token exchange for third-party AI agents, you must complete the following configurations: + +- Create an Okta OIDC web app integration to handle user sign-on and issue ID tokens. +- Add a custom scope for your custom authorization server. +- Import a third-party AI Agent with RSA key-pair authentication. +- Configure the access policy to allow the JWT Bearer grant type. +- Complete the token exchange flow with Okta APIs. + +After these configurations, you can create a test app to demonstrate this flow, see [Create an app to test the token exchange flow](#create-an-app-to-test-the-token-exchange-flow). + +### Create an OIDC web app integration + +An app integration represents your app in your Okta org. Use it to configure how your app connects with Okta services. + +1. Open the Admin Console for your org. +1. Go to **Applications** > **Applications** to view the current app integrations. +1. Click **Create App Integration**. +1. Select **OIDC - OpenID Connect** as the **Sign-in method**. +1. Select **Web Application** as the **Application type**, then click **Next**. +1. Enter an **App integration name**. For example, "AI third-party token exchange." +1. Set the following values. + 1. **Grant types**: Authorization Code + [[style="list-style-type:lower-alpha"]] + 1. **Sign-in redirect URIs**: Enter `http://localhost:5000/callback` + +1. Select **Allow everyone in your organization to access** for **Controlled access**. +1. Click **Save** to create the app integration. + +The configuration page for the new app integration appears. + +Make a note of the client ID and client secret. Both are in the configuration pane for the app integration that you've created: + +- **Client ID**: Found on the **General** tab in the **Client Credentials** section. + +- **Client Secret**: Found on the **General** tab in the **Client Credentials** section. + +> **Note:** For a complete guide to all the options not explained in this guide, see [Create OIDC app integrations](/docs/guides/create-an-app-integration/openidconnect/main/). + +### Add a custom scope for your custom authorization server + +Your custom authorization server requires a custom scope for the third-party AI Agent token exchange. You can use the default custom authorization server or create your own. See [Create an authorization server](/docs/guides/customize-authz-server/main/#about-the-custom-authorization-server). + +>**Note**: System scopes (`openid`, `profile`, `email`) are stripped during the ID-JAG exchange and cause an `invalid_scope` error. Use a custom scope. + +1. In the Admin Console, go to **Security** > **API**. +1. On the **Authorization Servers** tab, select the name of your authorization server, and then select **Scopes**. +1. Select **Scopes** and then **Add Scope**. +1. Enter a **Name**, for example, `xaa:read`. +1. Optional. Enter a **Display phrase**, for example, "Cross App Access (XAA read-only scope)." +1. Optional. Enter a **Description**, for example, "This scope allows third-party AI Agent token exchange." +1. Click **Save**. + +See [Create Scopes](/docs/guides/customize-authz-server/main/#create-scopes). + +### Import your AI Agent + +The AI Agent is the machine identity that your calling application uses to sign token exchange requests. Import your third-party AI Agent following steps in [AI Agent Imports](https://help.okta.com/okta_help.htm?type=oie&id=ai-agent-imports). + +The AI Agent identity is distinct from the OIDC web app integration, which signs users in and issues the ID token. The AI Agent identity authenticates both steps of the exchange. + + + + + +Import your third-party AI agent, or, if you'd like an AI Agent identity to test the token exchange flow in this guide, you can create one manually: + +1. In the Admin Console, go to **Directory** > **AI agents**. +1. Click **Register AI agent** > **Register manually**. +1. Under **Profile**, add a name and description for your AI Agent, for example, "third-party AI Agent." +1. Click **Register**. +1. Under **Owners**, add owners to the AI Agent. Add at least two owners. Click **Save**. +1. Select your AI Agent from the list of AI Agents, and click **Credentials**. Make a note of the AI agent ID. +1. Under **Client Authentication**, generate an RSA key-pair. Click **Add public key** and +**Generate new key** or use your own public key. Click **Done**. +1. From **Actions**, select **Activate**. +1. Click **Delegations**. Under **User sign-on**, click **Add caller**. From **Application**, select your previously created OIDC app integration, for example, "AI third-party token exchange." Click **Add caller**. +1. Under **Non-human identity**, click **Configure**. From **Authorization server**, select your custom authorization server, in this example, use `default`. +1. Add a value for the **Audience/resource URL**. In this test example, use `https://example.com`. Click **Save**. +1. Click **Resource connections**, and then **Add resource connection**. Select the **Authorization server** resource type, and then from **Select Authorization server**, select your custom authorization server, in this example, use `default`. From **The following OAuth scopes**, select the custom scope you added previously, for example, `xaa:read`. Click **Add**. + +>**Note:** Make a note of the AI Agent ID. For example, `wlp9k6....GKZ5hAE0g7`. + +### Configure the access policy + +After you create the AI Agent, configure your custom authorization server's access policy to authenticate your AI Agent. + +1. In the Admin Console, go to **Security** > **API**. +1. On the **Authorization Servers** tab, select the name of an authorization server (`default` if you're using the default custom authorization server). +1. Select **Access Policies**, and then edit an existing policy. If you need to add a policy, see [Create access policies](/docs/guides/customize-authz-server/main/#create-access-policies). +1. Edit the default rule or create a rule, see [Create Rules for each Access Policy](/docs/guides/customize-authz-server/main/#create-rules-for-each-access-policy). +1. Enable grant type **JWT Bearer**. +1. Save the rule and policy. + +## Complete the token exchange flow + +Your app makes two API calls directly to Okta's token endpoints. No Okta SDK is required. The flow comprises the following two steps: + +1. Exchange the `id_token` for ID-JAG +1. Exchange the ID-JAG for an `access_token` + +To test this flow, use the following `curl` calls with your configured data. + +Use the [Create an app to test the token exchange flow](#create-an-app-to-test-the-token-exchange-flow) to demonstrate the full token exchange flow and display the ID token, ID_JAG token, and access token. + +#### Exchange the ID token for ID-JAG + +Call the org authorization server's `/token` endpoint. The `client_assertion` is signed with the agent's RSA private key. + +Ensure you update the following values in this call: `{yourOktaDomain}`, `{signed JWT}`, `{user id_token}`, and the `audience` URL. See the following parameter table. + +To generate an ID token, see [Create an app to obtain a test ID token](#create-an-app-to-obtain-a-test-id-token). + +##### Request + +```bash +curl -X POST https://{yourOktaDomain}/oauth2/v1/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ + --data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ + --data-urlencode "client_assertion={signed JWT}" \ + --data-urlencode "subject_token={user id_token}" \ + --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:id_token" \ + --data-urlencode "requested_token_type=urn:ietf:params:oauth:token-type:id-jag" \ + --data-urlencode "scope=xaa:read" \ + --data-urlencode "audience=https://example.okta.com/oauth2/default" +``` + +| Parameter | Description and value | +| --- | --- | +| grant_type | Standard OAuth 2.0 token exchange grant. The value must be `urn:ietf:params:oauth:grant-type:token-exchange`. | +| client_assertion_type | The value must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. | +| client_assertion | A signed JWT used for client authentication. Sign the JWT using the key created during the AI Agent registration. For more information on building the JWT, see [JWT with private key](https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth/#jwt-with-private-key). | +| subject_token_type | The value must be `urn:ietf:params:oauth:token-type:id_token`. | +| subject_token | A valid ID token issued to the resource app associated with the AI agent | +| requested_token_type | The value must be `urn:ietf:params:oauth:token-type:id-jag`. | +| scope | A list of scopes at the resource app being requested. This defines the permissions for the final access token. Use `xaa:read` | +| audience | The issuer URL of the resource app's authorization server. | + +##### Response + +A successful response returns an `id_jag` token. Pass this token to the next step: + +```bash +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-store +Pragma: no-cache + +{ + "issued_token_type": "urn:ietf:params:oauth:token-type:id-jag", + "access_token": "eyJhbGciOiJIUzI1NiIsI...", + "token_type": "N_A", + "expires_in": 300 +} +``` + +#### Exchange the ID-JAG for an access token + +Call the custom authorization server's token endpoint. The `client_assertion` audience is the custom authorization server token URL. + +Ensure you update the following values in this call: `{yourOktaDomain}`, `{custom-as-id}` (`default` in this example), `{signed JWT}`, and the `{ID_JAG}` token. See the following parameter table. + +##### Request + +```bash +curl -X POST https://{your-okta-domain}/oauth2/{custom-as-id}/v1/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \ + --data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ + --data-urlencode "client_assertion={signed-jwt}" \ + --data-urlencode "assertion={id_jag}" +``` + +| Parameter | Description and value | +| --- | --- | +| grant_type | The value must be `urn:ietf:params:oauth:grant-type:jwt-bearer` | +| assertion | The ID-JAG received in the exchange token ID for resource token [response](#response). | +| client_assertion_type | The value must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. | +| client_assertion | A signed JWT used for client authentication. Sign the JWT using the key created during the AI Agent registration. For more information on building the JWT, see [JWT with private key](https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth/#jwt-with-private-key). | + +##### Response + +The response contains the access token that the AI agent uses to access the resource server. + +``` http +{ + "token_type": "Bearer", + "expires_in": 3600, + "access_token": "eyJraWQiOiJoZnpMS3...tdBbjhHcIXF_OQCsUdkuPXQTaAeq8fQ", + "scope": "xaa:read" +} +``` + +## Set up a Python environment + +This project demonstrates how to set up and run standalone Python scripts using the `uv` package manager. + +### Install uv + +Install `uv` using the official installer: + +```bash +brew install uv +``` + +For more installation options (Windows, macOS without curl, and so on), see the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/). + +After installation, verify it works: + +```bash +uv --version +``` + +### Set up the project + +Go to the project directory: + +```bash +cd /yourProject +``` + +Initialize the project with `uv init`: + +```bash +uv init +``` + +After initialization, sync the environment: + +```bash +uv sync +``` + +Install the required dependencies: + +```bash +uv add python-dotenv flask requests "pyjwt[crypto]" +``` + +This adds the packages needed by the demo scripts. + +## Create an app to obtain a test ID token + +This demo script obtains an ID token for testing. See [Exchange the ID token for ID-JAG](#exchange-the-id-token-for-id-jag). + +### Create your environment file + +Create a `.env` file. The demo script references the values in this file. Include the following details from your OIDC app integration. See [Create an OIDC app integration](#create-an-oidc-web-app-integration) for these values. Add: + +```bash +OKTA_DOMAIN=https://{yourOktaDomain} +OIDC_CLIENT_ID={client_id} +OIDC_CLIENT_SECRET={client_secret} +``` + +For example: + +```bash +# OIDC config for oidc.id-token.py +# OKTA_DOMAIN must include https:// and have NO trailing slash +OKTA_DOMAIN=https://example.okta.com +OIDC_CLIENT_ID=0oazte....Vv6aZ1d7 +OIDC_CLIENT_SECRET=rPgK0mZi6aqpmRD.... +``` + +### Create the token demo file + +Create a `scripts` folder at the root level of your project, and create a file name, for example, `oidc.id-token.py`. Copy the following Python code into the file and save. + +```python +# oidc.id-token.py +import os, secrets +from flask import Flask, redirect, request, session +import requests +from dotenv import load_dotenv +load_dotenv() + +OKTA_DOMAIN = os.environ["OKTA_DOMAIN"] +OIDC_CLIENT_ID = os.environ["OIDC_CLIENT_ID"] +OIDC_CLIENT_SECRET = os.environ["OIDC_CLIENT_SECRET"] +REDIRECT_URI = "http://localhost:5000/callback" + +app = Flask(__name__) +app.secret_key = os.environ.get("FLASK_SECRET_KEY", secrets.token_hex(32)) + +@app.route("/") +def index(): + state = secrets.token_urlsafe(16) + session["oauth_state"] = state + return redirect( + f"{OKTA_DOMAIN}/oauth2/v1/authorize" + f"?response_type=code&client_id={OIDC_CLIENT_ID}" + f"&redirect_uri={REDIRECT_URI}&scope=openid+profile+email" + f"&state={state}" + ) + +@app.route("/callback") +def callback(): + # Surface any error Okta sent back instead of crashing on a missing code. + if "error" in request.args: + return ( + f"
error: {request.args.get('error')}\n"
+            f"description: {request.args.get('error_description')}
", + 400, + ) + + # Validate state to protect against CSRF. + expected_state = session.pop("oauth_state", None) + if not expected_state or request.args.get("state") != expected_state: + return "
error: state mismatch
", 400 + + code = request.args.get("code") + if not code: + return "
error: no authorization code returned
", 400 + + resp = requests.post(f"{OKTA_DOMAIN}/oauth2/v1/token", data={ + "grant_type": "authorization_code", + "code": code, + "redirect_uri": REDIRECT_URI, + "client_id": OIDC_CLIENT_ID, + "client_secret": OIDC_CLIENT_SECRET, + }) + if not resp.ok: + return f"
token endpoint error ({resp.status_code}):\n{resp.text}
", 400 + + id_token = resp.json().get("id_token", "") + return f"""\ + + + + + Okta secures AI + + + +

Okta secures AI

+ + + + + + + + + + +
TokenValue
ID token{id_token}
+ +""" + +if __name__ == "__main__": + app.run(port=5000) +``` + +### Run the demo file + +Run the demo file: + +```bash +uv run scripts/oidc-id-token.py +``` + +Then open `http://localhost:5000/` in your browser to start the sign-in flow. After you enter your Okta credentials, the ID token appears on the rendered page. You can use this ID token to test the token exchange flow API calls in [Exchange the ID token for ID-JAG](#exchange-the-id-token-for-id-jag). + +To see the full flow, create and run the following demo script. + +## Create an app to test the token exchange flow + +Use the following Python Flask app to test the token exchange flow. It obtains an ID token and then calls the two-step authentication process as documented in [Complete the token exchange flow](#complete-the-token-exchange-flow). + +### Create your environment file + +Create an `.env` file (or modify the `.env` from the previous section). The demo script will reference the values in this file. Include the following details from the remainder of your token exchange setup: + +```bash +OKTA_DOMAIN=https://{yourOktaDomain} +OIDC_CLIENT_ID={client_id} +OIDC_CLIENT_SECRET={client_secret} +CUSTOM_AS={yourCustomAS} +AGENT_CLIENT_ID={yourAgentID} +AGENT_KEY_ID={yourAgentKID} +AGENT_PRIVATE_KEY_JWK={yourAgentPrivateKey} +``` + +For example: + +```bash +# OIDC config for token-exchange-demo.py` +# OKTA_DOMAIN must include https:// and have NO trailing slash +OKTA_DOMAIN=https://example.okta.com +OIDC_CLIENT_ID=0oazte....Vv6aZ1d7 +OIDC_CLIENT_SECRET=rPgK0mZi6aqpmRD.... + +# --- Token exchange (agent on behalf of user), used by token-exchange-demo.py` --- +# Custom authorization server ID (for example, "default" or an "ausXXXX..." id) +CUSTOM_AS=default +# The agent's OAuth client id +AGENT_CLIENT_ID=wlpzx5jq6....zGJY1d7 +# The "kid" of the agent's signing key (must match a key in the client's JWKS) +AGENT_KEY_ID=98cfd0b41b99b68....fb8868188d2f5 +# The agent's PRIVATE key as a single-line JSON JWK, e.g. {"kty":"RSA","n":"...","e":"AQAB","d":"...","p":"...","q":"...","kid":"..."} +AGENT_PRIVATE_KEY_JWK={"alg":"RS256","d":"QtPaeAww4ykVlxafEqZ7A......} +``` + +### Create the demo file + +Create a `scripts` folder at the root level of your project, and create a file name, for example, `token-exchange-demo.py`. Copy the following Python code into the file and save. + +```python +# token_demo.py +import os, json, time, uuid, secrets +import jwt +import requests +from flask import Flask, redirect, request, session +from dotenv import load_dotenv + +load_dotenv() + +# --- OIDC (user login) --- +OKTA_DOMAIN = os.environ["OKTA_DOMAIN"] +OIDC_CLIENT_ID = os.environ["OIDC_CLIENT_ID"] +OIDC_CLIENT_SECRET = os.environ["OIDC_CLIENT_SECRET"] +REDIRECT_URI = "http://localhost:5000/callback" + +# --- Token exchange (agent on behalf of user) --- +CUSTOM_AS = os.environ["CUSTOM_AS"] +SCOPE = os.environ.get("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"]) +# PyJWT can't sign with a raw JWK dict; convert it to a key object once. +AGENT_SIGNING_KEY = jwt.PyJWK.from_dict(AGENT_PRIVATE_KEY_JWK).key + + +def build_client_assertion(audience: str) -> str: + now = int(time.time()) + return jwt.encode( + { + "iss": AGENT_CLIENT_ID, + "sub": AGENT_CLIENT_ID, + "aud": audience, + "iat": now, + "exp": now + 300, + "jti": str(uuid.uuid4()), + }, + AGENT_SIGNING_KEY, + algorithm="RS256", + headers={"kid": AGENT_KEY_ID}, + ) + + +def get_id_jag(id_token: str) -> str: + org_token_url = f"{OKTA_DOMAIN}/oauth2/v1/token" + custom_as_issuer = f"{OKTA_DOMAIN}/oauth2/{CUSTOM_AS}" # audience = AS issuer, not its token endpoint + resp = 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": SCOPE, + "audience": custom_as_issuer, + }) + if not resp.ok: + raise RuntimeError(f"id-jag exchange failed ({resp.status_code}) at {org_token_url}:\n{resp.text}") + return resp.json()["access_token"] + + +def get_access_token(id_jag: str) -> str: + custom_as_token_url = f"{OKTA_DOMAIN}/oauth2/{CUSTOM_AS}/v1/token" + resp = 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, + }) + if not resp.ok: + raise RuntimeError(f"access-token exchange failed ({resp.status_code}) at {custom_as_token_url}:\n{resp.text}") + return resp.json()["access_token"] + + +app = Flask(__name__) +app.secret_key = os.environ.get("FLASK_SECRET_KEY", secrets.token_hex(32)) + + +@app.route("/") +def index(): + state = secrets.token_urlsafe(16) + session["oauth_state"] = state + return redirect( + f"{OKTA_DOMAIN}/oauth2/v1/authorize" + f"?response_type=code&client_id={OIDC_CLIENT_ID}" + f"&redirect_uri={REDIRECT_URI}&scope=openid+profile+email" + f"&state={state}" + ) + + +@app.route("/callback") +def callback(): + # Surface any error Okta sent back instead of crashing on a missing code. + if "error" in request.args: + return ( + f"
error: {request.args.get('error')}\n"
+            f"description: {request.args.get('error_description')}
", + 400, + ) + + # Validate state to protect against CSRF. + expected_state = session.pop("oauth_state", None) + if not expected_state or request.args.get("state") != expected_state: + return "
error: state mismatch
", 400 + + code = request.args.get("code") + if not code: + return "
error: no authorization code returned
", 400 + + resp = requests.post(f"{OKTA_DOMAIN}/oauth2/v1/token", data={ + "grant_type": "authorization_code", + "code": code, + "redirect_uri": REDIRECT_URI, + "client_id": OIDC_CLIENT_ID, + "client_secret": OIDC_CLIENT_SECRET, + }) + resp.raise_for_status() + id_token = resp.json()["id_token"] + + id_jag = get_id_jag(id_token) + access_token = get_access_token(id_jag) + + return f"""\ + + + + + Okta secures AI + + + +

Okta secures AI

+ + + + + + + + + + + + + + + + + + +
TokenValue
ID token{id_token}
ID-JAG{id_jag}
Access token{access_token}
+ +""" + + +if __name__ == "__main__": + app.run(port=5000) + +``` + +### Run the demo + +Run the demo file: + +```bash +uv run scripts/token-exchange-demo.py +``` + +Then open `http://localhost:5000/` in your browser to start the sign-in flow. After you enter your Okta credentials, the full flow completes and the following tokens appear on the rendered page: ID token, ID-JAG token, and access token. See [Complete the token exchange flow](#complete-the-token-exchange-flow). + +## Troubleshooting + +The following errors come from the Okta token exchange module: + +| Error | Root cause | Fix | +| --- | --- | --- | +| `invalid_scope: openid not allowed` | System scopes (`openid`/`profile`/`email`) are stripped in the ID-JAG flow | Use a custom scope such as `xaa:read` on the custom AS and the managed connection | +| `invalid_client: JWKSet not configured` | The public key isn't registered on the AI Agent | Register the public JWK at **Directory** > **AI Agents** > *(agent)* > **Credentials** | +| `invalid_grant` / `invalid_token` on Step 1 | The user's `id_token` is expired or was issued by a different OIDC app than the one linked to the agent | Complete a fresh sign-in; confirm the `aud` claim equals the linked OIDC app's client ID | +| `invalid_client: kid is invalid` | The `kid` in the signing code doesn't match the registered key | Copy the `kid` from the agent's **Credentials** into `AGENT_KEY_ID` | +| `access_denied: no_matching_policy` | The custom AS access policy is missing the JWT Bearer grant | In the custom AS access policy rule, enable the JWT Bearer grant | +| `Only service apps can use client_credentials` | Wrong client type at the org AS | Only an Okta client can perform Step 1; OIDC apps can't | +| `token_exchange_invalid_audience` | Wrong flow path (for example, Web SSO instead of token exchange) | Use the AI Agent client for Step 1, not the OIDC app | + +## Next steps + + + +Authenticating third-party agents with delegated user identity is one part of the Okta for AI Agents framework. After your agent can call APIs on a user's behalf, you can: + +- Authorize access to tools and APIs: Configure brokered consent, MCP integration, and OAuth 2.0 resource server policies to define which resources and scopes agents are permitted to reach. + +- Secure agent-to-agent communication: Use A2A flows, ACT claims, and token chaining when one agent delegates work to another. + +- Deploy through an agent gateway: Centrally manage, observe, and govern agent traffic through virtual MCP servers and gateway integrations. diff --git a/packages/@okta/vuepress-site/docs/guides/index.md b/packages/@okta/vuepress-site/docs/guides/index.md index 5a9acd5ac97..d23dd38cb8e 100644 --- a/packages/@okta/vuepress-site/docs/guides/index.md +++ b/packages/@okta/vuepress-site/docs/guides/index.md @@ -4,6 +4,11 @@ guides: - add-an-external-idp - add-id-verification-idp - ai-agent-token-exchange + - ai-agent-third-party-token-exchange + - ai-agent-secure-third-party + - ai-agent-secure-azure + - ai-agent-secure-aws-bedrock + - ai-agent-secure-amazon-bedrock - app-provisioning-connection - archive-auth-js - archive-embedded-siw diff --git a/packages/@okta/vuepress-theme-prose/const/navbar.const.js b/packages/@okta/vuepress-theme-prose/const/navbar.const.js index 15a717dac0f..f88c56ba885 100644 --- a/packages/@okta/vuepress-theme-prose/const/navbar.const.js +++ b/packages/@okta/vuepress-theme-prose/const/navbar.const.js @@ -1012,6 +1012,31 @@ export const guides = [ }, ], }, + { + title: "Secure AI Agents", + subLinks: [ + { + title: "Generic - Secure Third-Party AI Agents", + guideName: "ai-agent-secure-third-party", + }, + { + title: "Set up third-party AI agent token exchange", + guideName: "ai-agent-third-party-token-exchange", + }, + { + title: "Secure Azure AI Foundry agents with Okta", + guideName: "ai-agent-secure-azure" + }, + { + title: "Secure AWS Bedrock Agents with Okta", + guideName: "ai-agent-secure-aws-bedrock" + }, + { + title: "Secure an Amazon Bedrock AgentCore agent", + guideName: "ai-agent-secure-amazon-bedrock", + }, + ], + }, { title: "Automate org management with Terraform", subLinks: [ diff --git a/packages/@okta/vuepress-theme-prose/global-components/AiAgentTokenExchangeModule.vue b/packages/@okta/vuepress-theme-prose/global-components/AiAgentTokenExchangeModule.vue new file mode 100644 index 00000000000..5fe3cf3830d --- /dev/null +++ b/packages/@okta/vuepress-theme-prose/global-components/AiAgentTokenExchangeModule.vue @@ -0,0 +1,116 @@ + + +