Skip to content
Help guide the future of agentgateway — Take the survey

For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.

Okta

Page as Markdown

Keep user context in requests with Okta and on-behalf-of token exchange.

Attaches to:

Exchange the user’s Okta access token that a client sends to the gateway for a new access token at an Okta custom authorization server, so that the backend receives a token that is issued for its own audience and scopes.

About

Okta implements the RFC 8693 token exchange grant as on-behalf-of token exchange. A service app sends a user’s access token from a custom authorization server as the subject_token, and Okta returns a new access token for the same user, issued for the audience and scopes that the service app requests. The new token names the user in its sub claim and the service app in its act claim, so the backend knows both who the request is for and who is acting for them.

In this guide, agentgateway is the service app. The client sends the user’s Okta access token to the gateway, and the gateway exchanges it for each request to the backend. The backend receives a token that was issued for its own audience and scopes, not the token that the client sent.

    sequenceDiagram
    participant Client as Client (agentgateway-test-client)
    participant AGW as agentgateway
    participant Okta as Okta custom authorization server
    participant API as Backend

    Client->>AGW: Request + user's Okta access token
    AGW->>Okta: POST /oauth2/AUTH_SERVER_ID/v1/token<br/>subject_token=user's token<br/>audience, scope<br/>client ID and secret of the service app
    Okta-->>AGW: Access token for the backend<br/>sub=user, act.sub=service app
    AGW->>API: Request + Authorization: Bearer exchanged token
    API-->>AGW: Response
    AGW-->>Client: Response
  

The guide uses three Okta resources.

  • agentgateway-test-client is the caller. It signs in the user and gets the user’s token, called the incoming token. In this guide, you play this app yourself and use curl to get the token.
  • agentgateway is the gateway, an API Services app. It sends the incoming token to Okta and gets the exchanged token back.
  • agentgateway-backend is the custom authorization server for the backend API. It issues both the incoming token and the exchanged token, with its audience, api://agentgateway-backend, in the aud claim.

Unlike the Google Security Token Service, Okta authenticates the caller of the exchange. The gateway needs the client ID and client secret of an Okta API Services app that has the Token Exchange grant.

Okta settings

Okta uses the standard RFC 8693 grant, so you configure it with the generic oauthTokenExchange method. The following settings are specific to Okta.

FieldValue for Okta
host, pathOKTA_DOMAIN:443 and /oauth2/AUTH_SERVER_ID/v1/token, the token endpoint of a custom authorization server. Okta supports on-behalf-of token exchange on custom authorization servers only. The org authorization server, at /oauth2/v1/token, does not accept this exchange.
policies.backendTLS{}. A host port of 443 does not enable TLS by itself, so set backendTLS to originate TLS to Okta.
grantTypetokenExchange, the default.
subjectToken.tokenTypeurn:ietf:params:oauth:token-type:access_token, the default. The subject token is a user’s access token from the same custom authorization server, or from another custom authorization server in the same org that this server lists as a trusted server.
requestedTokenTypeOmit. Okta returns an access token.
audiencesRequired by Okta. The audience of the custom authorization server, such as api://agentgateway-backend.
scopesScopes that exist on the custom authorization server, and that its access policy allows for the gateway’s app. OIDC scopes such as openid and offline_access are not allowed.
clientAuthRequired. The client ID and client secret of an API Services app with the Token Exchange grant. Set method to clientSecretBasic, the default, or clientSecretPost.

Before you begin

  1. Install the agentgateway binary.
  2. Install Docker to run a sample backend.
  3. Get administrator access to an Okta org that has the NHI Authentication Tokens SKU, which is required for on-behalf-of token exchange. Note that the free Okta Integrator plan does not include access to this feature.
  4. Install jq.

Step 1: Set up Okta

Create a custom authorization server for the backend, an API Services app for the gateway, and a test app and user that get the incoming token. You can create these resources in the Okta Admin Console, or script them with the Okta management API.

  1. Open the Okta Admin Console. Sign in to your Okta org, such as https://<your-org>.okta.com, and click Admin in the upper-right corner. You can also go to the Admin Console URL directly, https://<your-org>-admin.okta.com.

  2. Save your Okta domain in an environment variable, without the https:// scheme or the -admin suffix of the Admin Console URL.

    export OKTA_DOMAIN=<your-org>.okta.com
  3. Create a custom authorization server for the backend. Go to Security > API > Authorization Servers, and click Add Authorization Server.

    • Name: agentgateway-backend
    • Audience: api://agentgateway-backend. Okta sets this value as the aud claim of the tokens that the server issues.

    Okta shows the issuer URL of the new server, in the form https://<your-org>.okta.com/oauth2/<id>. Save the ID at the end of the URL.

    export AUTH_SERVER_ID=<authorization-server-id>
  4. On the Scopes tab of the authorization server, click Add Scope, and create a scope named backend.read. The gateway requests this scope in the exchange, so that the exchanged token grants only read access to the backend. The backend can check the scp claim of the token to enforce it. The exchanged token can carry only scopes that exist on the authorization server.

  5. Create the API Services app that the gateway authenticates as. Go to Applications > Applications, click Create App Integration, select API Services, and name the app agentgateway.

    1. In the General Settings section, click Edit. In the Grant type section, expand Advanced, select Token Exchange, and save. Keep Client Credentials selected. Okta requires it for an API Services app.
    2. In the Client Credentials section, keep Client secret as the client authentication method. Copy the client ID and the client secret, and save them in environment variables.
    export GATEWAY_CLIENT_ID=<agentgateway-client-id>
    export GATEWAY_CLIENT_SECRET=<agentgateway-client-secret>
  6. Create a test app that gets a user’s access token, as an agent or web app does in production. Click Create App Integration, select OIDC - OpenID Connect and Native Application, and name the app agentgateway-test-client.

    1. In the Grant type section, select Resource Owner Password in addition to Authorization Code. This guide uses the password grant to get a user token with curl. In production, use the authorization code flow with PKCE instead.
    2. In the Assignments section, select Skip group assignment for now, and save.
    3. Copy the client ID, and save it in an environment variable. A native app has no client secret.
    export TEST_CLIENT_ID=<agentgateway-test-client-client-id>
  7. Create an access policy on the authorization server that lets both apps get tokens. Go back to Security > API > Authorization Servers, select agentgateway-backend, and on the Access Policies tab, click Add New Access Policy.

    1. Name the policy agentgateway. For Assign to, select The following clients, add agentgateway and agentgateway-test-client, and create the policy.
    2. Click Add rule, and name the rule agentgateway. In Grant type is, select Resource Owner Password and, under Advanced, Token Exchange. Keep the default of any user and any scope, and create the rule.
  8. Create a test user. Go to Directory > People, and click Add person. Enter a name and a username, such as [email protected], select I will set password, enter a password, and clear User must change password on first login. Then assign the user to the agentgateway-test-client app: go to Applications > Applications > agentgateway-test-client > Assignments, click Assign > Assign to People, and assign the user.

    export OKTA_USERNAME=[email protected]
    export OKTA_PASSWORD='<password>'
  9. Let the test user sign in to the test app with a password only. By default, Okta requires two factors to sign in to an app, and rejects the password grant. Go to Security > Authentication Policies, click Add a policy, and name it agentgateway-test-password-only. Add a rule that applies to the test user, and set User must authenticate with to Password / IdP. Then on the Applications tab of the policy, add agentgateway-test-client. Use this policy for testing only.

Step 2: Get a user token

  1. Get an access token for the test user from the custom authorization server, and save it in an environment variable. The test app requests only the openid scope, so the token does not grant access to the backend yet. Okta access tokens expire after 1 hour by default, so get a new one if you come back to this guide later.

    export INBOUND_TOKEN=$(curl -s https://$OKTA_DOMAIN/oauth2/$AUTH_SERVER_ID/v1/token \
      -d grant_type=password -d client_id=$TEST_CLIENT_ID \
      --data-urlencode username=$OKTA_USERNAME \
      --data-urlencode password=$OKTA_PASSWORD \
      -d scope=openid | jq -r .access_token)
  2. Decode the token’s payload. The cid claim is the client ID of the test app, and the scp claim has only openid.

    echo "$INBOUND_TOKEN" | cut -d. -f2 | jq -R 'gsub("-";"+") | gsub("_";"/") | . + ("=" * ((4 - (length % 4)) % 4)) | @base64d | fromjson | {iss, aud, sub, cid, scp}'

    Example output:

    {
      "iss": "https://integrator-1234567.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9",
      "aud": "api://agentgateway-backend",
      "sub": "[email protected]",
      "cid": "0oa1test2client3id4h9",
      "scp": [
        "openid"
      ]
    }

Step 3: Configure token exchange

  1. Run the httpbin sample app on port 8000. The app returns the headers of each request, so you can see the token that the gateway forwards.

    docker run -d --name httpbin -p 8000:8080 mccutchen/go-httpbin
  2. Create a configuration file. The route forwards requests to httpbin, and the oauthTokenExchange policy on the backend exchanges the incoming token at your Okta authorization server first. The shell fills in your Okta domain, authorization server ID, client ID, and client secret.

    cat > config.yaml <<EOF
    # yaml-language-server: \$schema=https://agentgateway.dev/schema/config
    gateways:
      default:
        port: 3000
    routes:
    - name: okta-backend
      backends:
      - host: localhost:8000
        policies:
          backendAuth:
            oauthTokenExchange:
              host: $OKTA_DOMAIN:443
              path: /oauth2/$AUTH_SERVER_ID/v1/token
              policies:
                backendTLS: {}
              audiences:
              - api://agentgateway-backend
              scopes:
              - backend.read
              clientAuth:
                method: clientSecretBasic
                clientId: $GATEWAY_CLIENT_ID
                clientSecret: $GATEWAY_CLIENT_SECRET
    EOF

    For a description of each setting, see Okta settings. The file has the client secret in plain text. To keep the secret out of the file, set clientSecret to {file: <path>} instead.

  3. In a second terminal, change to the directory that has the config.yaml file, and run agentgateway with the configuration. Keep the first terminal for the curl commands, so that the INBOUND_TOKEN variable stays available there.

    agentgateway -f config.yaml

Step 4: Verify the exchange

  1. In the first terminal, send a request to the httpbin /headers endpoint through the gateway, with the user’s token. The gateway exchanges the token at Okta, and forwards the request to httpbin with the exchanged token. If more than an hour passed since Step 2, get a new user token first.

    curl -s http://localhost:3000/headers \
      -H "authorization: Bearer $INBOUND_TOKEN"

    If the response is invalid request instead of the request headers, Okta rejected the exchange. In an org without the NHI Authentication Tokens SKU, Okta always rejects it. For other causes, see Troubleshooting.

  2. Decode the payload of the exchanged token.

    curl -s http://localhost:3000/headers \
      -H "authorization: Bearer $INBOUND_TOKEN" \
      | jq -r '.headers.Authorization[0] | sub("^Bearer ";"")' \
      | cut -d. -f2 \
      | jq -R 'gsub("-";"+") | gsub("_";"/") | . + ("=" * ((4 - (length % 4)) % 4)) | @base64d | fromjson | {iss, aud, sub, cid, scp, act}'

    Compared to the token that you sent, the sub claim still identifies the test user, so the backend sees the same end user. The cid claim is now the client ID of the agentgateway app, the scp claim has the backend.read scope that the gateway requested, and the act claim names the agentgateway app as the actor.

    {
      "iss": "https://integrator-1234567.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9",
      "aud": "api://agentgateway-backend",
      "sub": "[email protected]",
      "cid": "0oa1gateway2client3h9",
      "scp": [
        "backend.read"
      ],
      "act": {
        "sub": "0oa1gateway2client3h9",
        "sub_profile": "service"
      }
    }
  3. Send a request without a token. The gateway has no subject token to exchange, so it rejects the request with a 400 response and does not call Okta.

    curl -s -w " %{http_code}\n" http://localhost:3000/headers

    Example output:

    invalid request 400

The exchange does not validate the incoming token itself; Okta does. To reject an invalid or expired token at the gateway before it calls Okta, add a JWT authentication policy to the route. Set preserveToken: true on that policy, so that the token is still in the request when the exchange runs.

Troubleshooting

When Okta rejects the exchange, the gateway returns 400 with the body invalid request. When Okta rejects the gateway’s client credentials, the gateway returns 500 with the body backend authentication failed. The response from Okta is logged at debug level. To see it, run agentgateway with the RUST_LOG=info,agentgateway::http::auth::oauth=debug environment variable, and look for oauth token exchange rejected by authorization server or oauth token exchange rejected gateway credentials in the output.

ErrorCause
invalid_grant: The NHI Authentication Tokens SKU is not enabled.Your Okta org does not have the SKU that on-behalf-of token exchange requires. The free Okta Integrator plan does not include it. Contact Okta to enable it.
invalid_request: The required parameter ‘actor_token’ is missing.The path points to the org authorization server, /oauth2/v1/token, or the configuration has no audiences. Use the token endpoint of a custom authorization server, and set audiences to the server’s audience.
invalid_scope: One or more scopes are not configured for the authorization server resource.A value in scopes does not exist on the custom authorization server.
unauthorized_client: The client is not authorized to use the provided grant type.The app that clientId names does not have the Token Exchange grant. Edit the app’s general settings, and select Token Exchange in the Advanced grant types.
invalid_client: The client secret supplied for a confidential client is invalid.The clientSecret does not match the agentgateway app. The gateway returns 500 with backend authentication failed.
invalid_client: A client_id must be provided in the request.The configuration has no clientAuth. Okta authenticates the caller of the exchange, so set clientAuth with the client ID and client secret of the agentgateway app.

Cleanup

  1. Stop agentgateway with Ctrl+C in its terminal, and remove the httpbin container.

    docker rm -f httpbin
  2. Delete the Okta resources that you created.

    In the Okta Admin Console, delete the resources that you created, including the agentgateway-test-password-only authentication policy, the agentgateway-backend authorization server, the agentgateway and agentgateway-test-client apps, and the test user. Deactivate each app and the user before you delete them.
Was this page helpful?
Agentgateway assistant

Ask me anything about agentgateway configuration, features, or usage.

Note: AI-generated content might contain errors; please verify and test all returned information.

Tip: one topic per conversation gives the best results. Use the + button in the chat header to start a new conversation.

Switching topics? Starting a new conversation improves accuracy.
↑↓ navigate ↵ select esc dismiss

What could be improved?

Your feedback helps us improve assistant answers and identify docs gaps we should fix.

Need more help? Join us on Discord: https://discord.gg/y9efgEmppm

Want to use your own agent? Add the Solo MCP server to query our docs directly. Get started here: https://search.solo.io/.