Beginning in October, Okta release version notations are changing from year.month to year.week. For example, 2026.10.0 will become 2026.41.0, where 41 indicates that week of the year that the deployment occurs. More information is available in this knowledge base article.

On this page

Secure Azure AI Foundry AI agents with Okta

Identity Engine

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


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:

    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 AI agent. You add it once as a reusable module. See Add Okta authentication to your AI agent.

  • 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_token your AI 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.

  • Your imported AI agent that uses private_key_jwt client 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_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 Azure resources

Create an Azure OpenAI resource

  1. In the Azure Portal, search for Azure OpenAI and create a resource.
  2. Select a region that supports the model that you want to use. Check Azure OpenAI model availability for your region before you continue.
  3. After the resource deploys, note the endpoint URL and one of the API keys. You need both for AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_KEY.

Deploy a model

  1. In the Azure Portal, open your Azure OpenAI resource and go to Azure OpenAI Studio > Deployments.
  2. Click Create new deployment and select your model, for example gpt-4o.
  3. 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 DeploymentNotFound error, 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 416 or 503 errors during image pushes. Use az acr build (cloud build) rather than a local docker 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 8000 at 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 .env file into the image. Add a .dockerignore file 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_token is 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:

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

See also