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
The person signs in to your application
Your application receives an access token for the person, as usual.
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.
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.The agent calls the API
The new token has the person in
sub, the agent inactand the delegation indelegation_id, so your API sees both whom the call is for and who is making it.
Set up a delegation
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.
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:
Field Rules Scopes At least one, among the scopes the application may request. This is the ceiling: an exchange may ask for less, never more. Chaining From 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. Term Optional. In the future and at most 365 days ahead. Without one, the delegation lasts until it is withdrawn.
Written once, never edited
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.
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:
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"| Parameter | Value |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | The person’s access token, issued by the same realm and still active. |
subject_token_type | urn:ietf:params:oauth:token-type:access_token, the only type accepted. A refresh token can never be exchanged. |
requested_token_type | Optional. When sent, urn:ietf:params:oauth:token-type:access_token. |
scope | Optional. The scopes the new token should carry; without it, the subject token’s scopes are requested. |
resource | Optional. 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
{
"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
}subis still the person;azpis the acting client.actnames 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_idnames 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_accessand the other maps a person’s token has, but noclient_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 inact,delegation_idandazp. - 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: falseat once for every token that carries itsdelegation_id. - An API that only validates tokens locally keeps accepting the ones it already holds until they expire. The response’s
revocationLimitSecondssays for how long at most. - A withdrawal cannot be undone, and withdrawing twice is refused.
Errors
Errors follow RFC 6749: error and error_description, plus Authio’s own error_code.
| Error | Error code | Why |
|---|---|---|
unauthorized_client | GrantTypeDisabled | Token Exchange is not enabled on the acting client. |
invalid_request | DelegationNotPermitted | There is no live delegation for this client and this person. |
invalid_request | DelegationDepthExceeded | The subject token already has more actors than the delegation allows. |
invalid_request | DelegationSubjectTokenInactive | The subject token is expired, revoked, from an ended session or not from this realm. |
invalid_request | DelegationSubjectTokenTypeUnsupported | subject_token_type is not the access token type. |
invalid_scope | DelegationScopesExceedGrant | The requested scopes exceed the subject token or the delegation. |
invalid_target | DelegationResourceExceedsGrant | resource 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.