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 a Salesforce Agentforce agent

Identity Engine

This guide shows you how to build a FastAPI wrapper that authenticates users with Okta, performs Okta's two-step token exchange internally, and then calls a Salesforce Agentforce AI agent through the Agent API. Your app owns the full flow: it verifies who the user is, exchanges their identity for a scoped access token, obtains a separate Salesforce access token for the Agentforce Agent API, and passes the user's verified identity to the AI agent as context. Finally, you deploy this containerized wrapper to Azure Container Apps.

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 implement the authentication. It then shows the Salesforce Agentforce-specific code and configuration that consumes it.

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.
  • Add a token exchange module to your AI agent.
  • Authenticate to Salesforce with the OAuth 2.0 client credentials grant and call the Agentforce Agent API.
  • Pass a signed-in user's verified Okta identity to an Agentforce AI agent session.
  • 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
  • A Salesforce org with Agentforce enabled and an active Agentforce Service Agent (ExternalCopilot type) that you can call
  • An Azure subscription with access to Azure Container Apps
  • The Azure CLI (opens new window) (az), installed and authenticated (az login)
  • Docker or access to the Azure Container Registry for building images
  • Your AI agent imported and registered in Okta
  • 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:

    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 (Salesforce Agentforce-specific). Unlike the other platforms, Agentforce doesn't accept an Okta access token directly. Your wrapper authenticates to Salesforce separately with the OAuth 2.0 client credentials grant. Then it uses the resulting Salesforce token to start an Agentforce session and pass the user's verified Okta identity (name and email) as context in the message it sends to the AI agent. See Integrate the token exchange into your Agentforce agent.

Note: The Okta access_token from steps 1 and 2 confirms the user's identity to your wrapper. It isn't forwarded to Salesforce. The Salesforce token from step 3 is a separate, service-level credential that authenticates your wrapper (not the user) to the Agentforce Agent API.

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.

  • A web app integration that signs users in and issues the id_token that your AI agent exchanges. Use the Authorization Code grant type and the openid profile email scopes. The id_token must have an aud claim that matches the app's client ID.

  • A custom authorization server. Use the built-in default server or create one.

  • A custom scope on the custom authorization server, such as xaa:read. Okta strips system scopes (openid, profile, email) during the ID-JAG exchange and can cause an invalid_scope error, so you must request a custom scope instead.

  • Your Agentforce AI agent imported and registered in Okta. This AI agent identity uses private_key_jwt client authentication, with its public key (JWK) registered. Link the app, set the custom authorization server, include your custom scope, and activate the AI agent. See Import your agent from Salesforce.

    Note: Okta doesn't retain the AI agent's private key. Generate the private key and store it in a secrets manager, 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.

Salesforce import prerequisites

  • Ensure that your Salesforce tenant is configured for AI agent imports. See Configure Salesforce.com for AI agent imports (opens new window).

  • Ensure that the Agent Platform Builder permission set is configured, and your integration user has API Enabled turned on (either in another permission set or their user profile).

  • You've integrated the Salesforce AI Agent provider app in your Okta org.

Import your AI agent from Salesforce

Unlike manually registered AI agents, Okta can discover and import AI agents directly from a connected Salesforce org.

  1. In the Admin Console, go to Directory > AI agent Providers. Select the Salesforce app integration, and then click Import.
  2. After you run the import, you click View agents. You're directed to the AI agents page where you can register the imported AI agents.
  3. Note the client ID. You need it for AGENT_CLIENT_ID.
  4. In Client Authentication, select Public Key / Private Key. Generate an RSA key pair, and register the public JWK. Note the kid. You need it for AGENT_KEY_ID.
  5. In Resource connections, link the web app you created in Before you begin and the custom authorization server with the xaa:read scope.
  6. Activate the AI agent.

Set up your Salesforce org

Configure a separate, service-level integration in Salesforce so that your wrapper can authenticate to the Agentforce Agent API. This is independent of the Okta AI agent that you imported in the previous section.

Create an external client app

  1. In Salesforce Setup, go to App Manager, and then click External Client Apps.
  2. Enable OAuth settings.
  3. Note the Consumer Key and Consumer Secret. You need them for SF_CLIENT_ID and SF_CLIENT_SECRET.

Configure OAuth settings and scopes

  1. Set the grant type to Client Credentials.

  2. Under Selected OAuth Scopes, select only the following scopes:

    • api (Manage user data through APIs)
    • chatbot_api (Access chatbot services)
    • sfap_api (Access the Salesforce API Platform)

    Important: Adding more OAuth scopes than these three, combined with issuing JWT-based access tokens, can cause an invalid_request: too many scopes requested error. See Troubleshoot your integration.

  3. Enable Issue JSON Web Token (JWT)-based access tokens for named users.

  4. Set IP Relaxation to Relax IP restrictions.

Enable the client credentials flow

  1. On the Connected App, go to Manage > Edit Policies.
  2. Enable Client Credentials Flow.
  3. Set Run As to a user with at least API access. The client credentials grant runs every Agent API call as this user, so that its permissions and login IP restrictions apply to every request. See Troubleshoot your integration.

Confirm your Agentforce AI agent

  1. In Salesforce Setup, go to Agents, and then create or select an AI agent.

  2. Confirm that the AI agent's type is ExternalCopilot (an Agentforce Service Agent).

    Important: The Agent API only works with ExternalCopilot type agents. It doesn't support InternalCopilot type AI agents (Employee Agents) or AI agents of type Agentforce (Default).

  3. Publish the AI agent so that it has an active bot version.

  4. Note the AI agent ID. It starts with 0Xx. You need it for SF_AGENT_ID.

Turn on Einstein

  1. In Salesforce Setup, go to Einstein Setup, and enable Einstein.
  2. Enable Einstein Generative AI, if it's available for your org.

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

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

Salesforce Agentforce values (used by the platform integration):

Environment variable Description Where to find it
SF_MY_DOMAIN Your Salesforce My Domain hostname, for example example.my.salesforce.com Salesforce Setup > My Domain
SF_CLIENT_ID The external client app's consumer key Salesforce Setup > App Manager > your connected app > Manage Consumer Details
SF_CLIENT_SECRET The External Client App's Consumer Secret Salesforce Setup > App Manager > your connected app > Manage Consumer Details
SF_AGENT_ID The Agentforce AI agent to invoke (starts with 0Xx) Salesforce Setup > Agents

Add Okta authentication to your AI agent

The following example token_exchange.py module that you create here has no dependency on Salesforce or Agentforce.

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.

Integrate the token exchange into your Agentforce AI agent

This section is specific to Salesforce Agentforce. Here you authenticate to Salesforce, call the Agentforce Agent API, and wire the result into your FastAPI app alongside the token exchange from the previous section.

Add the FastAPI and Salesforce 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
python-dotenv>=1.0.0

Install the complete set of dependencies:

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Project structure

okta-agentforce-agent/
├── main_agentforce.py # FastAPI entry point: token exchange + Agentforce call
├── token_exchange.py  # Okta token exchange module
├── requirements.txt
├── Dockerfile
├── .env                # Secrets (gitignored)
└── .env.example         # Template

Create your environment file

Create a .env file with both the Okta values and the Salesforce values that you collected in Collect your configuration values.

Get a Salesforce access token

Authenticate to Salesforce with the OAuth 2.0 client credentials grant. This token authenticates your wrapper to the Agentforce Agent API. It's a separate, service-level credential, not the user's Okta access token.

import os
import httpx

SF_MY_DOMAIN = os.environ["SF_MY_DOMAIN"]
SF_CLIENT_ID = os.environ["SF_CLIENT_ID"]
SF_CLIENT_SECRET = os.environ["SF_CLIENT_SECRET"]


def get_salesforce_token() -> str:
    resp = httpx.post(
        f"https://{SF_MY_DOMAIN}/services/oauth2/token",
        data={
            "grant_type": "client_credentials",
            "client_id": SF_CLIENT_ID,
            "client_secret": SF_CLIENT_SECRET,
        },
    )
    resp.raise_for_status()
    return resp.json()["access_token"]

Note: The returned token has the sfap_api, chatbot_api, and api scopes. If you enabled more OAuth scopes than these three on the connected app, this call can fail. See Troubleshoot your integration.

Call the Agentforce Agent API

The Agentforce Agent API is synchronous: start a session, send a message, read the response, and end the session.

Important: The Agent API's base URL is always https://api.salesforce.com, not your Salesforce instance URL. Your instance URL only appears in the session request's instanceConfig.endpoint field.

import uuid

SF_AGENT_ID = os.environ["SF_AGENT_ID"]


def ask_agentforce(prompt: str, user_claims: dict) -> str:
    sf_token = get_salesforce_token()
    sf_headers = {
        "Authorization": f"Bearer {sf_token}",
        "Content-Type": "application/json",
    }

    # Start session. instanceConfig.endpoint is required.
    session_resp = httpx.post(
        f"https://api.salesforce.com/einstein/ai-agent/v1/agents/{SF_AGENT_ID}/sessions",
        headers=sf_headers,
        json={
            "externalSessionKey": str(uuid.uuid4()),
            "bypassUser": True,
            "instanceConfig": {"endpoint": f"https://{SF_MY_DOMAIN}"},
        },
    )
    session_resp.raise_for_status()
    session_id = session_resp.json()["sessionId"]

    try:
        # Send the prompt with the user's verified Okta identity as context.
        message_resp = httpx.post(
            f"https://api.salesforce.com/einstein/ai-agent/v1/sessions/{session_id}/messages",
            headers=sf_headers,
            json={
                "message": {
                    "sequenceId": 1,
                    "type": "Text",
                    "text": f"The user is {user_claims.get('name')} ({user_claims.get('email')}). {prompt}",
                },
            },
        )
        message_resp.raise_for_status()

        for msg in message_resp.json().get("messages", []):
            if msg.get("type") == "Inform":
                return msg.get("message", "")
        return ""
    finally:
        # Always end the session, even if reading the response failed.
        httpx.delete(
            f"https://api.salesforce.com/einstein/ai-agent/v1/sessions/{session_id}",
            headers=sf_headers,
        )

Note: If the AI agent has nothing configured for the topic of your prompt, it responds with something like "Sorry, I can't assist with that." Configure the agent's topics in Salesforce Setup > Agents, or ask it something within its configured scope.

Wire it into 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 Agentforce. The following example main_agentforce.py imports the reusable token exchange module and adds only the Salesforce-specific wiring:

import jwt
from fastapi import FastAPI
from pydantic import BaseModel

from token_exchange import get_id_jag, get_access_token
# get_salesforce_token, ask_agentforce from the previous steps

app = FastAPI()


class InvokeRequest(BaseModel):
    id_token: str
    prompt: str


@app.get("/health")
def health() -> dict:
    return {"ok": True}


@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 to pass to Agentforce.
    # It isn't used to make an authorization decision.
    user_claims = jwt.decode(request.id_token, options={"verify_signature": False})

    # Platform integration (Salesforce Agentforce)
    answer = ask_agentforce(request.prompt, user_claims)

    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)

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-agentforce-agent
az acr build --registry <registry-name> --image okta-agent-agentforce: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-agentforce:latest \
  --set-env-vars \
    OKTA_DOMAIN="https://example.okta.com" \
    OKTA_CUSTOM="default" \
    OKTA_SCOPE="xaa:read" \
    AGENT_CLIENT_ID="<wlp...>" \
    AGENT_KEY_ID="<kid>" \
    AGENT_PRIVATE_KEY_JWK='<private key json>' \
    SF_MY_DOMAIN="example.my.salesforce.com" \
    SF_AGENT_ID="0Xx..." \
    SF_CLIENT_ID="<consumer-key>" \
    SF_CLIENT_SECRET="<consumer-secret>"

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 /invoke endpoint on your deployed container app, passing the test ID token to verify the end-to-end integration:

curl -s -X POST "https://<your-container-app-url>/invoke" \
  --header "Content-Type: application/json" \
  --data "{\"id_token\": \"$ID_TOKEN\", \"prompt\": \"Hello, what can you help me with?\"}"

A successful response appears as follows and confirms the full round trip:

{
  "ok": true,
  "answer": "Hi there! Can you let me know what you need help with? I'll do my best to assist!",
  "user": "jessie.smith@example.com",
  "access_token_prefix": "eyJraWQiOiI1ZXpPR0dSZzFf..."
}

Troubleshoot your integration

The following errors are specific to the Salesforce Agentforce integration:

Error Root cause Fix
invalid_grant: no client credentials user enabled The connected app has no Run As users configured for the client credentials flow Go to the connected app's Manage > Edit Policies > Client Credentials Flow, and assign a Run As user
invalid_grant: ip restricted The Run As user's profile has login IP ranges that block the caller's IP Add the caller's IP to the profile's login IP ranges, remove the login IP ranges, or use a Run As user on a profile without IP restrictions
invalid_request: too many scopes requested The connected app has Issue JWT-based access tokens enabled along with too many OAuth scopes Reduce the connected app's OAuth scopes to only api, chatbot_api, and sfap_api
Agent API returns URL No Longer Exists (HTML 404 page) The request used the Salesforce instance URL (*.my.salesforce.com) as the Agent API base Use https://api.salesforce.com/einstein/ai-agent/v1/... as the base URL instead
BadRequestException: Empty force-config endpoint The session request body is missing instanceConfig.endpoint Include {"instanceConfig": {"endpoint": "https://your-instance.my.salesforce.com"}} in the session POST body
Agent API returns 404 for the AI agent The AI agent is InternalCopilot type (an employee AI agent) Use an ExternalCopilot type AI agent (an Agentforce service AI gent). Employee AI agents aren't supported
AI agent responds "Sorry, I can't assist with that" The AI agent's topics don't cover the question asked Configure the AI agent's topics in Salesforce Setup > Agents, or ask it something within its configured scope

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 AI agent can now authenticate as a user and call Okta-protected resources on their behalf. To define which resources and scopes the AI 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