For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Okta
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-clientis 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 usecurlto get the token.agentgatewayis the gateway, an API Services app. It sends the incoming token to Okta and gets the exchanged token back.agentgateway-backendis 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 theaudclaim.
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.
| Field | Value for Okta |
|---|---|
host, path | OKTA_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. |
grantType | tokenExchange, the default. |
subjectToken.tokenType | urn: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. |
requestedTokenType | Omit. Okta returns an access token. |
audiences | Required by Okta. The audience of the custom authorization server, such as api://agentgateway-backend. |
scopes | Scopes 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. |
clientAuth | Required. 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
- Install the agentgateway binary.
- Install Docker to run a sample backend.
- 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.
- 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.
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.Save your Okta domain in an environment variable, without the
https://scheme or the-adminsuffix of the Admin Console URL.export OKTA_DOMAIN=<your-org>.okta.comCreate 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 theaudclaim 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>- Name:
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 thescpclaim of the token to enforce it. The exchanged token can carry only scopes that exist on the authorization server.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.- 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.
- 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>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.- 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. - In the Assignments section, select Skip group assignment for now, and save.
- 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>- 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
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.- Name the policy
agentgateway. For Assign to, select The following clients, addagentgatewayandagentgateway-test-client, and create the policy. - 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.
- Name the policy
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 theagentgateway-test-clientapp: 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>'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, addagentgateway-test-client. Use this policy for testing only.
Step 2: Get a user token
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
openidscope, 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)Decode the token’s payload. The
cidclaim is the client ID of the test app, and thescpclaim has onlyopenid.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
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-httpbinCreate a configuration file. The route forwards requests to httpbin, and the
oauthTokenExchangepolicy 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 EOFFor 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
clientSecretto{file: <path>}instead.In a second terminal, change to the directory that has the
config.yamlfile, and run agentgateway with the configuration. Keep the first terminal for thecurlcommands, so that theINBOUND_TOKENvariable stays available there.agentgateway -f config.yaml
Step 4: Verify the exchange
In the first terminal, send a request to the httpbin
/headersendpoint 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 requestinstead 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.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
subclaim still identifies the test user, so the backend sees the same end user. Thecidclaim is now the client ID of theagentgatewayapp, thescpclaim has thebackend.readscope that the gateway requested, and theactclaim names theagentgatewayapp 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" } }Send a request without a token. The gateway has no subject token to exchange, so it rejects the request with a
400response and does not call Okta.curl -s -w " %{http_code}\n" http://localhost:3000/headersExample 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.
| Error | Cause |
|---|---|
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
Stop agentgateway with
Ctrl+Cin its terminal, and remove the httpbin container.docker rm -f httpbinDelete the Okta resources that you created.
In the Okta Admin Console, delete the resources that you created, including theagentgateway-test-password-onlyauthentication policy, theagentgateway-backendauthorization server, theagentgatewayandagentgateway-test-clientapps, and the test user. Deactivate each app and the user before you delete them.