Available

Ask a named person to approve

Have an AI agent ask one named person to approve an action on their own device, and receive an access token that names both of them.

Sometimes an AI agent needs the approval of one particular person: the customer whose money it is moving, or the engineer whose change it is merging. The agent asks, the person answers on their own device, and the agent receives an access token that names that person.

This is OpenID Connect Client-Initiated Backchannel Authentication (CIBA), in poll mode. Backchannel means the person is asked outside the agent's own connection. Poll mode means the agent asks the token endpoint for the result until there is one.

To ask the agent's owner or approval group instead of a named person, see Ask before a sensitive action.

Before you begin

You need:

The agent authenticates here exactly as it does to sign in: with a client assertion, a short statement signed with the agent's own key. $ASSERTION in the examples is a client assertion built as in Give an AI agent an identity. An assertion is accepted once, so build a new one for every request.

Send the request to the person

POST

/apis/regional/v1/ai/auth/backchannel-authorize

The body is form-encoded.

curl https://acme.authfi.io/apis/regional/v1/ai/auth/backchannel-authorize \
  -d client_id=agt_7f3a9c21 \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion="$ASSERTION" \
  -d [email protected] \
  -d scope="transfer:accounts" \
  --data-urlencode "binding_message=Send 1,200 USD to Northwind Ltd" \
  -d requested_expiry=300
ParameterMeaning
login_hintRequired. The person to ask, by the name they sign in with.
scopeWhat the agent wants to do with this person's authority
binding_messageOne short sentence the person reads before deciding
requested_expirySeconds the person has to answer. 120 if left out, 600 at most.

client_id is the agent ID, agt_7f3a9c21 in this example. The console shows it as "Client id" on the agent's Connect tab, under Identity > AI Agents.

{ "auth_req_id": "bXl0aWNrZXQtNDgyMQ...", "expires_in": 300, "interval": 5 }

The person is notified. They see the agent's name and your binding message.

The person answers on the Approvals page of the console, under "Asking to act as you". They choose Approve to approve, or Refuse to deny. Only the named person can answer the request.

export AUTH_REQ_ID=<the auth_req_id from the response>

Poll for the answer

Ask the token endpoint with the auth_req_id, no more often than every interval seconds. Each poll carries a fresh client assertion.

POST

/apis/regional/v1/ai/auth/token
curl https://acme.authfi.io/apis/regional/v1/ai/auth/token \
  -d grant_type=urn:openid:params:grant-type:ciba \
  -d auth_req_id="$AUTH_REQ_ID" \
  -d client_id=agt_7f3a9c21 \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  -d client_assertion="$ASSERTION"

Until the person answers, the response is a 400 with an OAuth error:

{ "error": "authorization_pending" }
ResponseMeaningWhat the agent does
400 authorization_pendingThe person has not answeredWaits interval seconds, polls again
400 slow_downThe agent polls too fastWaits longer between polls
400 access_deniedThe person deniedStops
400 expired_tokenNobody answered in timeStops, or asks again
200 with an access tokenThe person approvedGoes ahead

Use the access token

The access token says who approved and who is acting.

ClaimValue
subThe person who approved
act.subThe agent, as agent:agt_…
client_idThe agent
scopeWhat the person approved
exp15 minutes after issue

Send it as a Bearer credential. A service that receives it sees both identities: this is the agent, acting for that person. To check these claims in your own service, see Accept agents at your own API.

Check that it works

Send the request with your own sign-in name as login_hint.

  1. Poll once. The response is 400 with authorization_pending.
  2. In the console, open Approvals. Under "Asking to act as you", choose Approve on the request. The console shows "Approved".
  3. Poll again. The response is 200 with an access token.
  4. Decode the access token. sub is you, and act.sub is the agent.

How the approval is protected

  • The agent cannot approve its own request. The person decides on a channel that belongs to them. The agent holds only the auth_req_id, which approves nothing.
  • The scope is the one the person saw. The access token carries what was approved, not what the agent sends when it collects the token.
  • An approval is used once. The first successful poll spends it. A second poll with the same auth_req_id is rejected.
  • No answer is a denial. An unanswered request expires and can never be approved afterwards.
  • A stolen auth_req_id is useless alone. Every poll must be signed with the agent's key.

Write a good binding message

The binding message is the only thing the person reads before deciding, usually on a phone. Say exactly what will happen.

Instead ofWrite
transfer:accountsSend 1,200 USD to Northwind Ltd
Approve agent requestMerge pull request 482 into main
Access requestedRead the Q3 payroll export

Name the amount, the destination and the thing. If the person cannot tell what will happen, they cannot approve it.

Troubleshooting

What you seeCauseFix
invalid_requestThe body could not be read, or a field is missingSend the body form-encoded, with login_hint
invalid_clientThe agent is unknown or revoked, or its assertion did not verifyCheck the key ID, the audience and the clock. Use a new jti each time.
slow_downThe agent polls more often than intervalWait longer between polls
expired_tokenThe request expired before anyone answeredSend a new request. Raise requested_expiry, up to 600.

Every error of the token endpoint is listed in the errors reference.

Next steps