Generate an OBO token for agents to act on behalf of a user

The On-Behalf-Of (OBO) token exchange pattern produces a single token that carries both the user's identity and the agent's identity, so every resource server knows who authorised the action and which agent performed it.

When AI agents act on behalf of users — reading data, calling APIs, triggering workflows — service account tokens do not indicate which user delegated an action and cannot be revoked for individual users without affecting all callers. The OBO token exchange pattern solves this by combining the user's subject token and the agent's actor token into a single delegated token.

IBM Verify references used in this guide:

Key learnings

  1. How subject and actor identities are combined in an OBO token exchange.
  2. How to configure the OAuth clients required for the flow.
  3. How to obtain the subject token (user), actor token (agent), and delegated token.
  4. What the resulting delegated token looks like and what its identity claims mean.
  5. How to query IBM Verify events to trace all token and agent identity activity.

Audience

  • IAM administrators
  • Security architects
  • Application developers integrating agent flows with IBM Verify

Concepts used in this guide

Concept Description
Agent ID The persistent identity record for the agent in IBM Verify
Subject client (Client 1) The OIDC application used for user login and subject token issuance
Actor client (Client 2) The OAuth client used by the agent to authenticate itself
Token exchange client (Client 3) The OAuth client that performs the OBO token exchange
Subject token The user's access token — identifies who delegated the action
Actor token The agent's access token — identifies which agent is performing the action
Delegated token (OBO token) The result of the exchange — carries sub=UserID and act.sub=AgentID

Prerequisites

  • An IBM Verify tenant with the Verify Agent Identity feature enabled. Contact IBM Support to enable the feature on your tenant.
  • Create a new agent identity or onboard an existing one in the IBM Verify tenant. See Onboarding an AI agent in IBM Verify.
  • An API client such as Insomnia or Postman for running the token requests.
  • Familiarity with OAuth 2.0 Authorization Code and Client Credentials flows.

Agent identity in IBM Verify

IBM Verify manages AI agents as first-class identities — separate from human identities. Every agent onboarded in IBM Verify is assigned a unique Agent ID — a persistent, durable identifier that becomes the anchor for ownership, credentials, and audit events.

Property Detail
Decoupled from credentials The Agent ID is generated at registration, before any OAuth client is attached. Identity and credentials are separate concerns.
Linkage point All management events (onboarding, owner assignment, client attachment, policy changes) and all runtime events (token issuance, token exchange, API calls) reference this single ID.
Traceability anchor The Agent ID is the answer to "which agent did this?" — across both the management plane and the runtime plane.

When onboarding an agent, you supply metadata that reflects operational intent, including fields such as name, type, agent owner, and tags. For the full list of fields, see Onboarding an AI agent in IBM Verify. IBM Verify returns an Agent ID that is used in all subsequent configuration steps.

Configure the OAuth clients

The OBO flow requires three OAuth clients. Set them up before running the flow.

Client 1 — Subject client (user authentication)

This client authenticates the human user and produces the subject_token. It is not attached to any agent.

Field Value
Grant type authorization_code, ropc, or other user-facing flows
Attached to agent No
Token role subject_token — identifies the user on whose behalf the agent acts

Create an OIDC application by using the Authorization Code grant. For full configuration details see OpenID Connect applications in IBM Verify. Save the client_id and client_secret — these are referred to as subject_client_id and subject_client_secret.

Configure a may_act token attribute mapping rule on Client 1 so the subject token authorises a specific actor to act on behalf of the user. For the full attribute mapping reference see OpenID Connect introspect, ID token, and user info mapping.

Attribute name Value
may_act {'sub': '<agent_id>'}

After the rule is in place, every subject token issued by Client 1 contains:

{
  "may_act": {
    "sub": "<agent-id>"
  }
}
Introspect mapping configuration in IBM Verify — Verify attribute trading_may_act mapped to target attribute may_act

Client 2 — Actor client (agent authentication)

This client authenticates the agent itself and produces the actor_token. It must be linked to the Agent ID in IBM Verify.

Field Value
Grant type client_credentials or urn:ietf:params:oauth:grant-type:jwt-bearer
Attached to agent Yes — linked through entityId
Purpose tag agent_id
Token role actor_token — identifies the agent performing the action

How the Agent ID appears in the actor token depends on the grant type:

Create an OIDC application or use DCR. Link it to the Agent ID at creation time by using one of the two methods below. Save actor_client_id and actor_client_secret.

Method A — Dynamic Client Registration (DCR)

curl --request POST \
  --url https://<tenant>.verify.ibm.com/oauth2/register \
  --header 'Authorization: Bearer <registration-access-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "client_name": "My Agent - Identity Client",
  "grant_types": ["client_credentials"],
  "extension": {
    "entity_id": "<agent-id>",
    "entity_type": "agent"
  }
}'

Method B — Applications API (create)

The following snippet shows only the agent-linkage fields. For the full list of required fields see OpenID Connect applications in IBM Verify.

curl --request POST \
  --url https://<tenant>.verify.ibm.com/v1.0/applications \
  --header 'Authorization: Bearer <access-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "... other application fields ...": "...",
  "providers": {
    "oidc": {
      "properties": {
        "... other oidc properties ...": "...",
        "entityType": "agent",
        "entityId": "<agent-id>"
      }
    }
  },
  "tags": [
    "... other tags ...",
    "purpose=agent_id"
  ]
}'

Method B — Applications API (patch existing app)

curl --request PATCH \
  --url https://<tenant>.verify.ibm.com/v1.0/applications/<application-id> \
  --header 'Authorization: Bearer <access-token>' \
  --header 'Content-Type: application/json' \
  --data '[
  {
    "op": "add",
    "path": "/providers/oidc/properties",
    "value": {
      "entityType": "agent",
      "entityId": "<agent-id>"
    }
  },
  {
    "op": "add",
    "path": "tags",
    "value": "purpose=agent_id"
  }
]'

Client 3 — Token exchange client

This client performs the OBO exchange — combining the subject token and actor token into a single delegated token. It must also be linked to the same Agent ID. An agent may have more than one token exchange client — each linked to the same Agent ID but targeting a different resource server.

Field Value
Grant type urn:ietf:params:oauth:grant-type:token-exchange
Attached to agent Yes — linked through entityId
Purpose tag act_on_behalf_of_a_user
Token role Issues the delegated token with sub from the subject token and act from the actor token

Method A — Dynamic Client Registration (DCR)

curl --request POST \
  --url https://<tenant>.verify.ibm.com/oauth2/register \
  --header 'Authorization: Bearer <registration-access-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "client_name": "My Agent - OBO Client",
  "grant_types": ["urn:ietf:params:oauth:grant-type:token-exchange"],
  "extension": {
    "entity_id": "<agent-id>",
    "entity_type": "agent"
  }
}'

Method B — Applications API (create)

Same structure as Client 2, with the agent-linkage fields and purpose=act_on_behalf_of_a_user in the tags field. For the full list of required fields see OpenID Connect applications in IBM Verify.

curl --request POST \
  --url https://<tenant>.verify.ibm.com/v1.0/applications \
  --header 'Authorization: Bearer <access-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "... other application fields ...": "...",
  "providers": {
    "oidc": {
      "properties": {
        "... other oidc properties ...": "...",
        "entityType": "agent",
        "entityId": "<agent-id>"
      }
    }
  },
  "tags": [
    "... other tags ...",
    "purpose=act_on_behalf_of_a_user"
  ]
}'

Method B — Applications API (patch existing app)

curl --request PATCH \
  --url https://<tenant>.verify.ibm.com/v1.0/applications/<application-id> \
  --header 'Authorization: Bearer <access-token>' \
  --header 'Content-Type: application/json' \
  --data '[
  {
    "op": "add",
    "path": "/providers/oidc/properties",
    "value": {
      "entityType": "agent",
      "entityId": "<agent-id>"
    }
  },
  {
    "op": "add",
    "path": "tags",
    "value": "purpose=act_on_behalf_of_a_user"
  }
]'

Each client has a single, well-scoped responsibility. Each can be managed, rotated, and revoked independently.

Run the flow

OBO flow for agents

Step 1 — User obtains the subject token

The user (or the application acting on their behalf) authenticates against IBM Verify using Client 1. The application then passes this token to the agent. For the full token endpoint reference see POST /oauth2/token.

POST /oauth2/token

grant_type=authorization_code
client_id=<subject_client_id>
client_secret=<subject_client_secret>
code=<authorization-code>
redirect_uri=<redirect-uri>

The response contains an access token. Decoded, its claims include:

{
  "sub":       "<user-id>",
  "client_id": "<subject_client_id>",
  "may_act": {
    "sub": "<agent-id>"
  },
  "iss":       "https://<tenant>.verify.ibm.com/oauth2",
  "iat":       1720000000,
  "exp":       1720003600
}

The sub is the authenticated user's identifier. The may_act.sub carries the Agent ID, identifying which agent is authorised to perform the exchange on behalf of this user.

Step 2 — Agent obtains the actor token

The agent authenticates independently using Client 2. No user is involved — this token asserts the agent's identity. For the full token endpoint reference see POST /oauth2/token.

Option A — JWT Bearer (sub=AgentID natively):

POST /oauth2/token

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
client_id=<actor_client_id>
assertion=<signed-jwt-asserting-agent-id>
scope=openid

The response contains an access token. Decoded, its claims include:

{
  "sub":       "<agent-id>",
  "client_id": "<actor_client_id>",
  "iss":       "https://<tenant>.verify.ibm.com/oauth2",
  "iat":       1720000005,
  "exp":       1720003605
}

Option B — Client credentials with token attribute mapping rule (agent_id as a custom claim):

POST /oauth2/token

grant_type=client_credentials
client_id=<actor_client_id>
client_secret=<actor_client_secret>
scope=openid

The response contains an access token. Decoded, its claims include:

{
  "sub":       "<actor_client_id>",
  "agent_id":  "<agent-id>",
  "client_id": "<actor_client_id>",
  "iss":       "https://<tenant>.verify.ibm.com/oauth2",
  "iat":       1720000005,
  "exp":       1720003605
}

The agent_id custom claim carries the Agent ID forward into the act claim of the delegated token.

Step 3 — Token exchange to obtain the delegated token

The agent uses Client 3 to combine both tokens into a single delegated token. For the full token endpoint reference see POST /oauth2/token.

POST /oauth2/token

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
client_id=<token_exchange_client_id>
client_secret=<token_exchange_client_secret>
subject_token=<subject-token-from-step-1>
subject_token_type=urn:ietf:params:oauth:token-type:access_token
actor_token=<actor-token-from-step-2>
actor_token_type=urn:ietf:params:oauth:token-type:access_token
requested_token_type=urn:ietf:params:oauth:token-type:access_token
scope=<scopes for this operation>
audience=<target-resource-uri>

The response contains an access token. Decoded, its claims include:

{
  "sub":       "<user-id>",
  "client_id": "<token_exchange_client_id>",
  "act": {
    "sub": "<agent-id>"
  },
  "aud":       "<target-resource-uri>",
  "scope":     "<scopes for this operation>",
  "iss":       "https://<tenant>.verify.ibm.com/oauth2",
  "iat":       1720000010,
  "exp":       1720001810
}

The two identity claims are the core of the OBO pattern:

  • sub is the user ID — who delegated the action; the resource server applies the user's access-control policies against this value.
  • act.sub is the Agent ID — who performed the action; its value is derived from may_act.sub in the subject token. To include this claim in the delegated token, add act as a mapped attribute in the token attribute mapping rule configured on Client 3.
Tip: The requested_token_type can also be set to urn:ietf:params:oauth:token-type:txn_token to obtain a Transaction Token instead of a standard access token. Transaction Tokens are useful in agent-to-agent (A2A) call chains where each service in the chain needs to verify the original user context and the sequence of callers. For details see Transaction Tokens in IBM Verify.

Step 4 — Agent calls the resource server API

POST /v1/<resource-action>
Authorization: Bearer <runtime-token>
Host: <resource-server-host>

The resource server (or the Identity Runtime Proxy placed ahead of it) validates the token and confirms:

  • sub is a known user identity — the resource server applies the user's access-control policies.
  • act.sub is a known Agent ID in IBM Verify — confirming the action was performed by an onboarded, tracked agent.
  • The token was obtained through a proper exchange, not issued directly to an unregistered client.
  • aud matches this service's resource identifier.

Traceability — querying events by Agent ID

The Agent ID is the single correlation key across two planes of activity: the management plane (agent identity operations) and the runtime plane (token issuance).

Management-plane events

To retrieve all management-level events for a specific agent, query the Events API with:

Filter parameter Value
filter_key data.id
filter_value <agent-id>

This surfaces:

  • Agent onboarded: timestamp, submitting user, metadata snapshot
  • Owner assigned or changed
  • Client 2 and Client 3 created and linked
  • Agent suspended or activated — when an agent is suspended, its linked OAuth clients are disabled and can no longer issue tokens, immediately blocking the actor token and token exchange steps

Runtime-plane events

To retrieve all token activity for a specific agent, query the Events API with:

Filter parameter Value
filter_key data.entity_id
filter_value <agent-id>

This surfaces:

  • Actor token issued (Step 2) — agent authenticating as itself
  • Delegated token issued through exchange (Step 3) — sub=UserID, act.sub=AgentID

With the Agent ID represented as data.id in management events and data.entity_id in token events, the same identifier can be used to trace an agent's complete lifecycle, from onboarding to every autonomous action it performs. No custom correlation logic is required. Through this you can answer:

  • Who onboarded the agent and when
  • Who owns it and approved its operation
  • Every token it has ever been issued, and which user delegated each action
  • Any ownership or policy changes in the agent's history