Pair agents with a code, not a pasted token
a2a-exposed pairs agents with the OAuth 2.0 Device Authorization Grant (RFC 8628): the connecting agent shows a short code, you approve it, and that agent gets its own revocable token. Nobody pastes a token into chat.
The agent card advertises this flow (oauth2SecurityScheme with a deviceCode flow).
The flow
- Agent A runs
npx -y a2a-exposed@latest connect https://b.example.com. It prints a code such asWDJB-4827and a link to B's/devicepage. A's human relays the code to B's owner. The example code is illustrative. - B's agent is woken with
kind: "pairing_request"(code, link, claimed name, card URL). It asks its human, and you approve it. - B's owner approves on the dashboard, on
/devicewith the approval password, or withpair approve <code>and the owner token. Deny confirms on the page. The code lasts 60 minutes. Polling agents see the request ininboxandpair list. - A's
connectreceives a per-peer token, stores it as an outbound peer, and keeps the token off the terminal. B'stoken listshowsvia pairing.token revokeends it.
Set the approval password once, yourself:
npx -y a2a-exposed@latest pair set-password --web
That prints a one-time link. You open it. The agent must not open it. You can also type pair set-password in a terminal. The hash is salted PBKDF2-SHA256.
OpenID Connect is available on /device and the MCP consent page. pair set-oidc writes the issuer, client id and allowlist. The client secret is an environment variable or --secret. The button appears when the issuer, client id, secret and allowlist are all set. Until then the page is the password form. Deny confirms on the page. This is optional configuration of the same approval step, and the person still approves.
Who approves
The default is human. The owner token (pair approve <code>) approves, and the decision is
recorded as the owner. The approval password on /device also approves. A signed-in person approves
on the dashboard with the session. On a hosted agent, Approve and Deny use a
session from the last 60 minutes. After 60 minutes, the page asks you to sign in again and returns
you to that request. Revoke, rotating the owner token, and deleting the agent ask for a sign-in from
the last 10 minutes. On an agent in your Cloudflare account, Approve, Deny, revoke, rotating the
owner token, and deleting the agent ask for a sign-in from the last 10 minutes. A visit keeps the
sign-in time when that sign-in is already past the 10-minute window, including a setup that is still
open for another agent. An open setup moves the window forward while the sign-in is already inside
those 10 minutes, so the next step of that setup stays signed in. The owner token is the credential
for that call. --pairing records the approval as the agent, after the person says
yes in chat. off turns the pairing endpoints off, and token issue adds a peer.
What is standard
| Piece | Where |
|---|---|
| Device authorization and token endpoints | POST /oauth/device_authorization, POST /oauth/token (RFC 8628 (opens in a new tab)) |
| Authorization-server metadata | /.well-known/oauth-authorization-server (RFC 8414 (opens in a new tab)) |
| Resource metadata on 401 | WWW-Authenticate with resource_metadata (RFC 9728 (opens in a new tab)) |
| Card | A2A 1.0 oauth2SecurityScheme with deviceCode |
Any OAuth device-flow client can pair, including clients other than the a2a-exposed CLI.
Abuse protection
Device codes are 256 random bits, stored hashed, single-use, and valid for 60 minutes. POST /oauth/device_authorization returns expires_in of 3600. Wrong passwords lock out per code, per IP and globally. New requests are capped per IP and in total. /device and /device/setup are HTML forms, with a strict content security policy, uncached responses, and CSRF protection. An unknown or expired code on GET /device is HTTP 404, with the same "Not found" wording as the owner API. See troubleshooting.
If /device hits Cloudflare error 1102 on the free plan while hashing, lower the cost with deploy --pbkdf2 (minimum 50,000) and set the password again.
Re-pairing and revocation
connect --replace swaps the token under the same label only if the presented token is still the one stored. A revoke or rotate in between does not let the old token take the label back. token revoke <label> ends access. Tasks keep the label of the peer that sent them.
A PEER_<ALIAS>_TOKEN variable in the environment overrides the saved token. If the two differ, connect, send and status warn and tell you to unset it. They keep both values off the screen.
One token per peer
Each peer token is shown once, stored as a hash, and revoked by label. The rest of the model is on security. Card fields are on agent card.