Skip to content

Delegation and token exchange

Let an agent or a service act on a person’s behalf with a narrower token, under a delegation you can withdraw at any time — OAuth 2.0 Token Exchange (RFC 8693).

Sometimes an application has to call an API as someone else: an AI agent working on a person’s documents, a back-end service continuing a request a front end received. Authio supports this with OAuth 2.0 Token Exchange (RFC 8693): the acting client presents the person’s access token and receives a new one, issued to itself, that keeps the person as the subject and names the client as the actor.

An exchange only succeeds when a delegation permits it: a written record that this application may act for this person, within these scopes, for this long. Administrators write delegations in the console or through the admin API, and can withdraw them at any time.

How it works

  1. The person signs in to your application

    Your application receives an access token for the person, as usual.

  2. The agent receives the person’s token

    Your application hands the access token to the agent — a client of the same realm — together with the task it was asked to do.

  3. The agent exchanges it

    It calls the token endpoint with its own credentials and the person’s token as subject_token. Authio checks that the token is still active, finds a live delegation for this client and this person that covers the requested scopes, and issues a new access token.

  4. The agent calls the API

    The new token has the person in sub, the agent in act and the delegation in delegation_id, so your API sees both whom the call is for and who is making it.

Set up a delegation

  1. Enable Token Exchange on the acting client

    Open the agent’s client in Clients and enable the Token Exchange grant on its Advanced tab. Enable on it only the scopes the agent needs: a delegation can only offer scopes the client may request.

  2. Write the delegation

    Open Delegations, choose New delegation, pick the realm, the application that will act and the person it acts for, then set the limits:

    FieldRules
    ScopesAt least one, among the scopes the application may request. This is the ceiling: an exchange may ask for less, never more.
    ChainingFrom 0 to 3: how many actors may already be in the token this application exchanges. 0, the safe choice, means it can only exchange the person’s own token.
    TermOptional. In the future and at most 365 days ahead. Without one, the delegation lasts until it is withdrawn.

Written once, never edited

To change a delegation, withdraw it and write another. Both stay on the record, so a later access review can read what was allowed and who ended it.

The admin API takes the same fields, plus two that the console form does not show: allowedResources, the resource servers the delegated tokens may be addressed to, and organizationId, which confines the delegation to an organization the person belongs to. It requires write access to the realm; see Admin API.

Create a delegationBash
curl -X POST https://authio.api.hyperio.tech/api/v1/admin/realms/acme/delegation-grants \
  -H "Authorization: Bearer $AUTHIO_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "acme-agent",
    "subjectUserId": "6f1c2e7a-4b8d-4f0e-9a51-3c2d7e8b9f10",
    "scopes": ["documents:read"],
    "maxDepth": 0,
    "allowedResources": ["https://api.acme.com"],
    "expiresAt": "2026-12-31T23:59:59Z"
  }'

Exchange a token

The acting client calls the realm’s token endpoint, authenticated with its own credentials in the method registered for it:

RequestBash
curl -X POST https://authio.api.hyperio.tech/realms/acme/protocol/openid-connect/token \
  -u "acme-agent:YOUR_CLIENT_SECRET" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "subject_token=$USER_ACCESS_TOKEN" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "scope=documents:read" \
  -d "resource=https://api.acme.com"
ParameterValue
grant_typeurn:ietf:params:oauth:grant-type:token-exchange
subject_tokenThe person’s access token, issued by the same realm and still active.
subject_token_typeurn:ietf:params:oauth:token-type:access_token, the only type accepted. A refresh token can never be exchanged.
requested_token_typeOptional. When sent, urn:ietf:params:oauth:token-type:access_token.
scopeOptional. The scopes the new token should carry; without it, the subject token’s scopes are requested.
resourceOptional. The API the new token is for. It must be among the delegation’s resources when it lists any, and a resource server the acting client may request tokens for.

Authio then checks that:

  • The subject token is still active — signature, expiry, revocation and session are checked by introspection. Every refusal about it gets the same answer, whatever the reason.
  • The requested scopes are within the subject token’s scopes and fit entirely inside one live delegation for this client and this person. Several delegations never add up, and a request for more is refused with the excess named — never quietly trimmed.
  • The number of actors already in the subject token is within the delegation’s chaining limit.
  • The person is still a member of the organization, when the delegation is confined to one.

The delegated token

Access token payloadJSON
{
  "iss": "https://authio.api.hyperio.tech/realms/acme",
  "sub": "6f1c2e7a-4b8d-4f0e-9a51-3c2d7e8b9f10",
  "aud": "https://api.acme.com",
  "azp": "acme-agent",
  "scope": "documents:read",
  "sid": "0b7e4c2a-9d1f-4a6e-8c3b-5f2d1e9a7c40",
  "act": { "client_id": "acme-agent" },
  "delegation_id": "3f1d9b6e-0c4a-4f7d-b2e8-5a9c1d7e3b40",
  "exp": 1791372600
}
  • sub is still the person; azp is the acting client.
  • act names the actor, in the shape RFC 8693 describes. When a delegated token is exchanged again, the new actor wraps the previous one: {"client_id": "acme-summarizer", "act": {"client_id": "acme-agent"}}. Each application in a chain needs a delegation of its own for the person.
  • delegation_id names the delegation this token was issued under.
  • It carries the person’s access and nothing of the acting client’s own: the roles the person holds, directly or through a group (roles exclusive to the acting client included), in realm_resource_access and the other maps a person’s token has, but no client_resource_access, the map of the roles granted to the client itself. An API reading roles from a delegated token sees what the person may do, never what the agent may do on its own; the agent appears only in act, delegation_id and azp.
  • The token keeps the person’s sid, so ending their session also ends the delegated tokens for APIs that introspect.
  • It expires no later than the delegation, and no refresh token is issued.

Withdraw a delegation

Open the delegation in Delegations, choose Withdraw and say why. The reason is required and kept with who withdrew it and when. Through the admin API, send POST /api/v1/admin/realms/acme/delegation-grants/{grantId}/withdraw with a reason in the body.

  • Nothing more is issued under it, and no token issued under it can be exchanged again.
  • Introspection answers active: false at once for every token that carries its delegation_id.
  • An API that only validates tokens locally keeps accepting the ones it already holds until they expire. The response’s revocationLimitSeconds says for how long at most.
  • A withdrawal cannot be undone, and withdrawing twice is refused.
APIs that accept delegated tokens should introspect them — see Resource servers — or keep the access token lifetime short, so a withdrawal applies right away.

Errors

Errors follow RFC 6749: error and error_description, plus Authio’s own error_code.

ErrorError codeWhy
unauthorized_clientGrantTypeDisabledToken Exchange is not enabled on the acting client.
invalid_requestDelegationNotPermittedThere is no live delegation for this client and this person.
invalid_requestDelegationDepthExceededThe subject token already has more actors than the delegation allows.
invalid_requestDelegationSubjectTokenInactiveThe subject token is expired, revoked, from an ended session or not from this realm.
invalid_requestDelegationSubjectTokenTypeUnsupportedsubject_token_type is not the access token type.
invalid_scopeDelegationScopesExceedGrantThe requested scopes exceed the subject token or the delegation.
invalid_targetDelegationResourceExceedsGrantresource is outside the resources the delegation lists.

Token exchange has a second use: a resource server can exchange the token it received for the access token of an account the person connected, such as GitHub. See Connected accounts.

Next steps