Skip to main content

Configure Cross App Access with Okta

Cross App Access (XAA) lets an AI agent call an application on a user's behalf without a second sign-in. In this guide, Traefik Hub API Gateway takes the agent's refresh token, exchanges it at Okta for an identity assertion (ID-JAG), redeems the assertion at the resource's authorization server, and forwards the resulting access token to your upstream service. The access token names the person in sub and the agent in act.sub, so a policy engine behind the gateway can decide on either one.

Okta plays both roles in this guide: it is the identity provider that issues the assertion and the authorization server that redeems it. For the generic chain and its sequence diagram, see Chaining for ID-JAG (Cross App Access). This page covers what to create in Okta and what to configure on the gateway. For the Okta Admin Console screens, see Okta's AI agent-to-app guide.

Prerequisites​

  • Traefik Hub API Gateway v3.21 or later, with a valid license, running on Kubernetes.
  • An Okta org with single sign-on, such as an Okta Integrator Free Plan org, and an account with the Super Admin role.
  • A user in the org who signs in to the agent. This guide calls them the signed-in user.
  • An upstream service to protect. This guide uses an upstream service that echoes request headers, so you can see what the gateway forwards.

Step 1: Create the Authorization Server for the Resource​

The authorization server issues the access token that your upstream service receives. Its issuer URL is also the address that every assertion is issued for.

  1. In the Okta Admin Console, go to Security > API and create a custom authorization server.
  2. Copy its Issuer URI. It looks like https://your-org.okta.com/oauth2/aus1a2b3c4d5e6f7g8.
  3. Add a scope named idjag.read. The assertion must carry at least one scope, so the authorization server needs one to grant.
  4. Add an access policy with a rule that allows the token exchange grant (urn:ietf:params:oauth:grant-type:token-exchange) and the JWT bearer grant (urn:ietf:params:oauth:grant-type:jwt-bearer), alongside any other grants you allow.

The authorization server now has an issuer URL, a scope, and a policy that accepts both exchanges. A rule that omits these grants makes both exchanges fail at the authorization server, and the requesting side shows no sign of the cause.

Step 2: Create the Resource App and Enable Cross App Access​

The resource app represents your upstream service in Okta. For details, see Okta's AI agent-to-app guide.

  1. Create an OIDC app integration for the resource.
  2. Open the app, go to Machine Assignments > Callers, and select Edit next to Cross App Access (XAA).
  3. Select Enable.
  4. Set Issuer URL to the Issuer URI from step 1. Compare the Issuer URL with the Issuer URI character by character. The Issuer URL becomes the aud claim of every assertion, and Okta rejects an assertion request whose audience differs from it, including by a trailing slash.
  5. Under Scopes, add idjag.read. You can leave Audience/tenant ID empty.
  6. Assign the signed-in user to the app. Okta checks whether the user can reach the resource before it issues an assertion.

Step 3: Register the Agent​

Okta generates an OIDC app for each registered AI agent. That app is the OAuth client for both exchanges, and its client identifier is the agent's own identifier. For details, see Okta's AI agent-to-app guide.

  1. Go to Directory > AI agents and select Register AI agent.
  2. Select Create a new OIDC app linked to this AI agent.
  3. Under Client registration, choose Okta-generated client ID, then Client secret. Select Generate secret, save the client ID and the secret, then select Activate > Enable.
  4. On the linked OIDC app, open the General tab, select Edit next to General Settings, and under Grant type select Refresh Token. The first exchange sends the user's refresh token.
  5. Open the menu for the agent in the agent list and select Activate. Confirm that the linked app is active.
  6. Assign the signed-in user to the agent's linked app.

Step 4: Connect the Agent to the Resource​

The connection is what makes Okta willing to issue an assertion for this agent and this resource. For details, see Okta's AI agent-to-app guide.

  1. Open the agent, go to Resource connections, and select Add resource connection.
  2. For Application instance, select the resource app from step 2.
  3. For the client ID registered in the resource app, enter the agent's own client ID. The redeeming authorization server compares this value, which Okta copies into the assertion's client_id claim, with the client that authenticates the second exchange. Okta is both ends here, so that client is the agent.
  4. Set the scope condition so the agent can request idjag.read. The examples in this guide use Allow any scope.

Step 5: Store the Agent Credentials in a Kubernetes Secret​

Reference the credentials with urn:k8s:secret:<name>:<key> instead of writing them into the middleware. Replace the placeholders with the client ID and secret from step 3:

kubectl create secret generic okta-clients --namespace apps \
--from-literal=agentID=<client-id> \
--from-literal=agentSecret=<client-secret>

Step 6: Configure the Middlewares and the Route​

oAuthTokenExchange middlewares run in order on the route. Each one authenticates as the agent.

Request the Assertion​

This middleware sends the user's refresh token to your Okta org's token endpoint and receives the assertion:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: okta-xaa-request-assertion
namespace: apps
spec:
plugin:
oAuthTokenExchange:
method: rfc8693
url: https://your-org.okta.com/oauth2/v1/token
clientID: urn:k8s:secret:okta-clients:agentID
clientSecret: urn:k8s:secret:okta-clients:agentSecret
tokenSource:
header: X-Refresh-Token
rfc8693:
subjectTokenType: urn:ietf:params:oauth:token-type:refresh_token
requestedTokenType: urn:ietf:params:oauth:token-type:id-jag
audience: https://your-org.okta.com/oauth2/aus1a2b3c4d5e6f7g8
scopes:
- idjag.read
  • Set url to your org's token endpoint, not the resource authorization server's.
  • Set audience to the Issuer URI from step 1.
  • Set rfc8693.scopes here. This is the request that issues the assertion, and the scopes are encoded into it.
  • The caller sends the refresh token in the X-Refresh-Token header, and the gateway removes the header before forwarding the request. The oidc middleware can't fill or overwrite this header, because it forwards only the ID token. On a route that a browser reaches, use the ID token chain from Chaining for ID-JAG (Cross App Access) instead, and set rfc8693.subjectTokenType to urn:ietf:params:oauth:token-type:id_token. Okta accepts an ID token as the subject token.
  • Leave store unset. An assertion can be redeemed once, so caching it breaks the second request. Every request therefore asks Okta for a new assertion. Depending on your Okta subscription, Okta limits how many assertions a user can get for a resource app each month. For the current limit, see Okta's AI agent-to-app guide.

Redeem the Assertion​

This middleware presents the assertion to the resource authorization server and receives an access token:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: okta-xaa-redeem-assertion
namespace: apps
spec:
plugin:
oAuthTokenExchange:
method: rfc7523
url: https://your-org.okta.com/oauth2/aus1a2b3c4d5e6f7g8/v1/token
clientID: urn:k8s:secret:okta-clients:agentID
clientSecret: urn:k8s:secret:okta-clients:agentSecret
usernameClaim: sub
forwardHeaders:
X-Idjag-User: sub
X-Idjag-Agent: act.sub
  • Set url to the token endpoint of the authorization server from step 1.
  • Don't add rfc7523.scopes. The assertion already carries the scopes, and Okta rejects a redemption whose scope parameter differs from the assertion's scopes.
  • Set usernameClaim to sub so the access log's ClientUsername names the person. Without it, the record identifies no principal.
  • forwardHeaders copies claims from the access token into request headers, one header per party. The upstream service receives the access token in the Authorization header as well.

Wire the Middlewares onto the Route​

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: okta-xaa
namespace: apps
spec:
entryPoints:
- websecure
routes:
- match: Host(`xaa.example.com`)
kind: Rule
services:
- name: echo
port: 80
middlewares:
- name: okta-xaa-request-assertion
- name: okta-xaa-redeem-assertion

List the request middleware first, then the redeem middleware, then any middleware that authorizes. A middleware placed before the redeem middleware sees the claims of the assertion instead of the access token's claims, which usually bypasses the check you meant to apply.

Step 7: Record the Person and the Agent in the Access Log​

This step is optional. The access log drops request headers by default. To keep the forwarded headers in the record, name them in the gateway's Helm values:

accessLog:
enabled: true
fields:
headers:
names:
X-Idjag-User: keep
X-Idjag-Agent: keep

Step 8: Verify the Flow​

Check the Assertion​

Run the first exchange by hand before you trust the rest. Set ORG_URL, ISSUER, REFRESH_TOKEN, CLIENT_ID, and CLIENT_SECRET to your values:

curl -s -X POST "$ORG_URL/oauth2/v1/token" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d requested_token_type=urn:ietf:params:oauth:token-type:id-jag \
--data-urlencode "audience=$ISSUER" \
--data-urlencode "scope=idjag.read" \
--data-urlencode "subject_token=$REFRESH_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:refresh_token \
-d "client_id=$CLIENT_ID" --data-urlencode "client_secret=$CLIENT_SECRET"

Decode the access_token value in the response with a JWT decoder. The assertion carries these claims:

ClaimContent
audThe Issuer URI from step 1.
subThe signed-in user's Okta user identifier.
act.subThe agent's identifier.
client_idThe client ID you entered in step 4.
scopeidjag.read.

The agent's app refuses the password grant, so the refresh token must come from an interactive sign-in through the agent's own app, not from a script that sends a password.

Call the Route​

Send a request with the refresh token:

curl -i -H "X-Refresh-Token: $REFRESH_TOKEN" https://xaa.example.com/

The route answers 200. A request without the header is rejected with 401. The echo service shows an Authorization: Bearer header holding the access token, X-Idjag-User holding the user's login, and X-Idjag-Agent holding the agent's identifier. The access token carries sub for the person, act.sub for the agent, and scp with idjag.read.

You now have a route where the upstream service receives an access token that names both the user and the agent.

Troubleshooting​

The route answers 404 page not found

A middleware with a configuration error disables the whole router, so the route no longer matches. Open the router's entry in the runtime API (/api/http/routers) and read its error field. For example, building token exchanger: unsupported method "bogus" names the method value that the plugin rejects.

The route answers 401 although the refresh token is valid

The gateway returns 401 when either exchange is refused and doesn't include Okta's reason in the response. Set the gateway log level to DEBUG and read the Unable to exchange token entry for the error that Okta returned. The entries below list the errors you can find there.

invalid_target: Token Exchange requests must include a valid audience of the authorization server

The audience on the request middleware doesn't match the Issuer URL you set in step 2. Copy the Issuer URI from the authorization server again and remove any trailing slash.

invalid_grant: The id-jag must include at least one scope

The assertion carries no scope, and Okta refuses it when the redeem middleware presents it. Set rfc8693.scopes on the request middleware.

invalid_request: The scope parameter must exactly match the scopes in the id-jag

The redeem middleware sends scopes that differ from the assertion's scopes. Remove rfc7523.scopes from the redeem middleware.

invalid_scope: One or more scopes are not configured for the authorization server resource

The request middleware asked for a scope that the authorization server from step 1 doesn't define. Add the scope to the authorization server, or request idjag.read.

invalid_grant: The id-jag was already used

An assertion can be redeemed once. Remove store from the request middleware, and check that no other middleware or client redeems the same assertion.

The first exchange succeeds and the second exchange fails

Check the client ID you entered when you connected the agent to the resource in step 4. The redeeming side compares the assertion's client_id claim with the client that authenticates the second exchange, and the two values are set in different places. Also check that the authorization server's rule from step 1 allows the JWT bearer grant.

Next Steps​