On this page
Secure Azure AI Foundry AI agents with Okta
This guide shows you how to build a FastAPI app that serves as a secure runtime wrapper for your AI agent. The app acts as the AI agent's gateway: it authenticates users with Okta, performs Okta's two-step token exchange internally to verify who they are, and then safely forwards their prompts and verified identity to Azure OpenAI. Finally, you learn how to deploy this containerized AI agent wrapper to Azure Container Apps.
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 an imported AI agent must do to authenticate as a signed-in user with Okta.
- Deploy an Azure OpenAI resource and configure a model deployment.
- Build a FastAPI wrapper that performs the Okta token exchange and calls Azure OpenAI with the resulting identity.
- Deploy the app to Azure Container Apps.
- 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 Azure subscription with Azure OpenAI access in your region
- The AI agent registered in your org. See Configure Microsoft Office 365 for AI agent imports (opens new window).
- The Azure CLI (opens new window) (
az), installed and authenticated (az login) - Azure permissions: Resource Group Contributor access (for
az acr createandaz containerapp create) and Container Registry Repository Writer permissions - Docker or access to the Azure Container Registry for building images
- 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 AI 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 AI agent 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 AI agent. You add it once as a reusable module. See Add Okta authentication to your AI agent.
- Exchange the user's
Platform integration (Azure-specific). Your FastAPI app calls the token exchange, decodes the user's identity claims from the
id_token, and passes that identity to Azure OpenAI in the system message of a chat completion request. See Create the FastAPI entry point.
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 imported AI agent token exchange.
An app integration that signs users in and issues the
id_tokenyour AI agent 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.Your imported AI agent that uses
private_key_jwtclient authentication, with its public key (JWK) registered. Link the web app, set the custom authorization server, include your custom scope, and activate the AI agent.Note: Okta doesn't retain the AI agent's private key. Store it in a secrets manager when it's generated, like Azure Key Vault, 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 FastAPI app reads these values as environment variables. The token exchange module uses the first group. The second group is specific to Azure OpenAI.
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 AI agent, for example wlp9k6... | Directory > AI Agents > (yourAgent) > Client registration |
AGENT_KEY_ID | kid of the public JWK registered on the AI agent | Directory > AI Agents > (yourAgent) > Client registration > Public/private key |
AGENT_PRIVATE_KEY_JWK | The AI agent's private JWK (single-line JSON) | Directory > AI Agents > (yourAgent) > Client registration > Public/private key. Output of Generate new key. Store the value in a secrets manager |
Azure OpenAI values (used by the platform integration):
| Environment variable | Description | Where to find it |
|---|---|---|
AZURE_OPENAI_KEY | Azure OpenAI API key | Azure Portal > your Azure OpenAI resource > Keys and Endpoint |
AZURE_OPENAI_ENDPOINT | Azure OpenAI resource endpoint URL | Azure Portal > your Azure OpenAI resource > Keys and Endpoint |
AZURE_OPENAI_DEPLOYMENT | Model deployment name (not the model name) | Azure OpenAI Studio > Deployments |
Add Okta authentication to your AI agent
The following example token_exchange.py module has no dependency on Azure or Azure OpenAI.
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 Azure resources
Create an Azure OpenAI resource
- In the Azure Portal, search for Azure OpenAI and create a resource.
- Select a region that supports the model that you want to use. Check Azure OpenAI model availability for your region before you continue.
- After the resource deploys, note the endpoint URL and one of the API keys. You need both for
AZURE_OPENAI_ENDPOINTandAZURE_OPENAI_KEY.
Deploy a model
- In the Azure Portal, open your Azure OpenAI resource and go to Azure OpenAI Studio > Deployments.
- Click Create new deployment and select your model, for example
gpt-4o. - Set a deployment name and note it exactly. It becomes
AZURE_OPENAI_DEPLOYMENT.
Important: The deployment name that you set here, not the model name, is what you pass to the API. If you get a
DeploymentNotFounderror, verify the exact name in Azure OpenAI Studio > Deployments.
Create an Azure Container Registry (ACR)
az acr create \
--name <registry-name> \
--resource-group <resource-group> \
--sku Standard
Important: Use the standard SKU. The basic SKU can cause
416or503errors during image pushes. Useaz acr build(cloud build) rather than a localdocker push. Local pushes are more likely to fail.
Create a Container Apps environment and container app
az containerapp env create \
--name <env-name> \
--resource-group <resource-group> \
--location eastus
az containerapp create \
--name <app-name> \
--resource-group <resource-group> \
--environment <env-name> \
--ingress external \
--target-port 8000
Note: The FastAPI app runs on port 8000. Set
--target-port 8000at creation time. If the container app defaults to port 80, the app returns the default Azure page.
Configure your app
Set up the project structure, install the FastAPI and Azure OpenAI dependencies, and create a Dockerfile for the containerized app.
Project structure
okta-azure-agent/
├── main.py # FastAPI entry point: token exchange + Azure OpenAI call
├── token_exchange.py # Okta token exchange module
├── requirements.txt
├── Dockerfile
├── .env # Secrets (gitignored)
└── .env.example # Template
Add the FastAPI and Azure OpenAI dependencies
Add these to the same requirements.txt, alongside the token exchange dependencies:
fastapi>=0.110.0
uvicorn>=0.29.0
httpx>=0.27.0
cryptography>=42.0.0
python-dotenv>=1.0.0
openai>=1.30.0
Install the complete set of dependencies:
pip install -r requirements.txt
Create a Dockerfile
Create a Dockerfile at the root of your project:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "main.py"]
Note: Don't copy your
.envfile into the image. Add a.dockerignorefile that contains.env, so that secrets never end up baked into the container. Azure Container Apps sets these as real environment variables at deploy time (see Deploy with environment variables).
Create your environment file
Create a .env file with both the Okta values and the Azure OpenAI values that you collected in Collect your configuration values.
Create the FastAPI 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 call Azure OpenAI with the verified user identity in the system message of a chat completion request. The following example main.py imports the reusable token exchange module and adds the Azure OpenAI call and the FastAPI-specific wiring:
import os
import jwt
from fastapi import FastAPI
from openai import AzureOpenAI
from pydantic import BaseModel
from token_exchange import get_id_jag, get_access_token
AZURE_OPENAI_KEY = os.environ["AZURE_OPENAI_KEY"]
AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"]
AZURE_OPENAI_DEPLOYMENT = os.environ["AZURE_OPENAI_DEPLOYMENT"]
app = FastAPI()
def ask_llm(prompt: str, user_claims: dict, access_token: str) -> str:
client = AzureOpenAI(
api_key=AZURE_OPENAI_KEY,
azure_endpoint=AZURE_OPENAI_ENDPOINT,
api_version="2024-02-01",
)
response = client.chat.completions.create(
model=AZURE_OPENAI_DEPLOYMENT,
messages=[
{
"role": "system",
"content": (
f"You are a helpful assistant. "
f"The authenticated user is {user_claims.get('name')} "
f"({user_claims.get('email')}). "
f"Their Okta access token is verified."
),
},
{"role": "user", "content": prompt},
],
)
return response.choices[0].message.content
class InvokeRequest(BaseModel):
id_token: str
prompt: str
@app.post("/invoke")
def invoke(request: InvokeRequest) -> dict:
# Okta authentication
id_jag = get_id_jag(request.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 system
# message. It isn't used to make an authorization decision.
user_claims = jwt.decode(request.id_token, options={"verify_signature": False})
# Platform integration (Azure OpenAI)
answer = ask_llm(request.prompt, user_claims, access_token)
return {
"ok": True,
"answer": answer,
"user": user_claims.get("email"),
"access_token_prefix": access_token[:10],
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Note: The
access_tokenis available in your app after step 2 of the token exchange. You can pass it to downstream Okta-protected APIs. In this pattern, your app verifies it but doesn't forward it directly to Azure OpenAI.
Deploy to Azure Container Apps
Package your FastAPI app into a container image, grant Azure Container Apps access to pull it, and deploy it with the environment variables you collected in Collect your configuration values.
Build the image in ACR
cd /path/to/okta-azure-agent
az acr build --registry <registry-name> --image okta-agent:latest .
Grant the container app access to ACR
az containerapp registry set \
--name <app-name> \
--resource-group <resource-group> \
--server <registry-name>.azurecr.io \
--username <registry-name> \
--password <acr-password>
Deploy with environment variables
az containerapp update \
--name <app-name> \
--resource-group <resource-group> \
--image <registry-name>.azurecr.io/okta-agent:latest \
--set-env-vars \
OKTA_DOMAIN="example.okta.com" \
OKTA_CUSTOM_AS_ID="default" \
OKTA_SCOPE="xaa:read" \
AGENT_CLIENT_ID="<wlp...>" \
AGENT_KEY_ID="<kid>" \
AGENT_PRIVATE_KEY_JWK='<private key json>' \
AZURE_OPENAI_KEY="<key>" \
AZURE_OPENAI_ENDPOINT="https://<resource-name>.openai.azure.com" \
AZURE_OPENAI_DEPLOYMENT="<deployment-name>"
Verify the target port
If the container app returns the default Azure page, the target port defaults to 80. Update it:
az containerapp ingress update \
--name <app-name> \
--resource-group <resource-group> \
--target-port 8000
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 that's linked to the AI agent. Complete an OIDC sign-in against that app to obtain one. For an example Authorization Code sign-in helper that authenticates with private_key_jwt, see Create an app to obtain a test ID token.
Note: Add the helper's callback URL (for example,
http://localhost:5000/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
Call the deployed container app's /invoke endpoint, passing the test ID token to confirm the full id_token → ID-JAG → access_token round trip:
curl -s -X POST "https://<your-container-app-url>/invoke" \
--header "Content-Type: application/json" \
--data "{\"id_token\": \"$ID_TOKEN\", \"prompt\": \"Who am I?\"}"
A successful response appears as follows and confirms the full round trip:
{
"ok": true,
"answer": "You are Jessie Smith, and you're signed in with your Okta account (jessie.smith@example.com). How can I help you today?",
"user": "jessie.smith@example.com",
"access_token_prefix": "eyJraWQiOiI..."
}
Troubleshoot your integration
The following errors are specific to the Azure integration:
| Error | Root cause | Fix |
|---|---|---|
DeploymentNotFound | Wrong or missing Azure OpenAI deployment name | Check the exact deployment name in Azure OpenAI Studio > Deployments. Names are case-sensitive. |
ACR push 416 / 503 errors | Basic SKU ACR with stale upload sessions | Upgrade ACR to standard SKU. Use az acr build instead of a local docker push. |
| Container app not pulling image | The container app has no registry credentials configured. | Run az containerapp registry set before updating the image. |
| App returns the default Azure page | Target port defaults to 80. The FastAPI app runs on 8000. | Run az containerapp ingress update --target-port 8000. |
The following errors come from the Okta token exchange and are covered in Set up imported 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 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 and the Okta for AI agents documentation on governing access to AI agents.