Delegation and token exchange
How a service acts for a person or another service by exchanging a token, what the new token can and cannot do, and how the act claim records who is acting.
On this page
Sometimes a program has to act for someone. A document service calls a signing service for a person. A background job reads a person's data to prepare a report. An agent works through a task on someone's behalf. In each case the program should do exactly what the person could do, and no more, and anyone looking at the result should be able to see who was acting.
Delegation is the answer. A program presents a token that already exists and asks identity for a new, narrower one aimed at the service it needs to call. Identity's mechanism is token exchange, defined in RFC 8693. This page explains the idea. The code is in Service to service.
Why not just forward the token#
The simplest thing is for service A to pass the person's access token to service B. It is also unsafe, for three reasons.
- The token is addressed to A. A resource server must refuse a token that is not addressed to it, so B would refuse it.
- Forwarding gives B everything A was given. B can then do anything the token allows, to any service that accepts it.
- B cannot tell the person acted directly from A acting for them.
An exchange fixes all three: the new token is addressed to B, carries no more than B needs, and records who is acting.
What an exchange is#
Service A sends identity a request to the token endpoint with the grant type urn:ietf:params:oauth:grant-type:token-exchange. It names:
- the subject token, the access token to exchange, which stands for the person (or service) being acted for,
- the audience, the client id of service B,
- optionally the scopes wanted, and
- optionally an actor token, for the party that is acting.
Identity checks and then issues a new access token for service B.
What identity checks#
| Check | Why |
|---|---|
| The caller is a confidential client. | A public client holds no secret and cannot be told apart from anyone, so it is refused. |
| The subject token is genuine, unexpired and current. | It must verify, its session must not be revoked, and the authorization version it carries must still match the person's. |
| The person and the organization are still active. | A suspended person or organization cannot be acted for. |
| The caller proves it holds the key the subject token is bound to. | A stolen token is no use without its key. |
| The subject token was issued to the caller. | A service cannot exchange a token that was not addressed to it. |
| The audience is a registered client, allowed to receive exchanges, and allowed into the person's organization: it is a first-party client, belongs to that organization, or its app has an active installation there. | Identity will not mint a token for a service that has not opted in. |
| The caller is allowed to exchange on behalf of someone in that organization. | The right to act for people is itself a permission, not a default. |
Any failure is refused with a standard error: invalid_grant for a bad subject or actor token, invalid_target for a bad audience, unauthorized_client for a caller that may not exchange, invalid_scope for scopes that cannot be had.
What the new token can do#
The new token is smaller than the old one in every way.
- Narrower scopes. The scopes are limited to what the subject token holds, and to what the audience is allowed to have. Asking for a scope outside both is
invalid_scope. Without a request, the new token gets that overlap. - A different audience. It is addressed to the target service only.
- Short lived. Its lifetime is the shortest of the subject token's remaining life, 15 minutes and the audience's own access token lifetime.
- No refresh token and no ID token. An exchange returns an access token only. To act again, exchange again.
- No session of its own. It has no
sid,acr,amrorauth_time, because nobody signed in at that moment. See Assurance levels. - The person's organization. It carries the same
tid. - Bound to the caller's key. The same sender constraint as other tokens applies.
Requesting a resource on an exchange is refused. An exchange names its target with audience. And requested_token_type must be the access token type.
Who is acting: the act claim#
When an actor token is part of the exchange, the new token carries an act claim:
{ "sub": "the person being acted for", "act": { "sub": "who is acting", "client_id": "the service that asked" }}sub stays the person. The act claim says someone else is using the token on their behalf. A service reading the token can then check both: who the work is for, and who is doing it.
An act claim can nest, to record a chain: an agent acting for a person, calling a tool that acts for the agent. The chain is limited to three hops. A fourth is refused.
Agents#
Status: In development.
An agent is a non-human principal that works for a person. It has its own principal and an owner. An agent exchange is stricter than a plain one. The owner sees their agents on the Agents tab of Apps and access in their account and can revoke any of them; see Connected apps.
- The person being acted for must be the agent's owner.
- The exchange must carry
authorization_detailsof the typetdaid:agent_action, describing what the agent may do. An agent exchange without one is refused, unless the subject token already carries one, which the new token then inherits. - A hop may narrow the
tdaid:agent_actionit received, never widen it. A wider request is refused withinvalid_target. - The new token's lifetime is also capped by the agent's own maximum token lifetime.
- An agent token can be exchanged again only if the agent is marked as allowed to delegate.
The authorization service reads this chain when it is asked a question with a delegated token, but only when the pdp_subject_token flag is on. That flag defaults OFF. The caller then sends the token itself as subject_token instead of naming the person, and the service verifies it. It answers only if the token was issued to the caller, belongs to the organization asked about, and has an act chain of at most three hops. Every client in the chain must be allowed into that organization, and the action must be covered by a tdaid:agent_action entry. With the flag off, the service answers 400 to a subject_token and an agent subject is answered as indeterminate, which fails closed. See Authorization. Agents are set up by an organization's administrators, so you may not have any.
Impersonation chains are denied#
A token with an act claim and no tdaid:agent_action is an impersonation token: a person's full authority carried by someone else, which the authorization service cannot narrow. With pdp_subject_token on, the service denies a question asked with such a token and records the reason impersonation_token_refused.
With the flag off, or against an older server, the service does not see the chain at all. The SDK client then falls back to naming the person, and an impersonated person is decided with their full authority. The only guard in that case is the SDK option refuseActorTokens, which makes the verifier reject every token that carries an act claim. Set it on any resource server that must not accept delegated tokens.
Delegation only narrows#
Every hop has to stay inside the one before it.
- In an exchange, the scopes can only shrink, and the lifetime can only shrink.
- In handing out roles, nobody can give away more than they hold, and what is delegated falls away with its source. See Permissions and roles.
- In an
actchain, the depth is limited to three.
Staying safe#
- Exchanged tokens are short on purpose. If one leaks, it is narrow, brief and aimed at one service.
- Revoking the source stops new exchanges. If the person's session is revoked or the person is suspended, the next exchange from that token fails.
- Everything is recorded. Each exchange is audited with the person, the actor and the scopes issued.