# Is OAuth enough to verify an AI agent?

> An access token says what a caller may do, but what does it say about which agent is calling, and who stands behind it?

## The short answer

No, and it was not built to. OAuth decides what a client may do: an access token tells your API that a server you trust issued it, with which scopes, until when, and often which person agreed, but not which agent is holding it, which company runs that agent, or whether the request changed on the way. Keep OAuth and your identity provider for those jobs, and add a signed agent card and a signature on each request for the rest; Pidgeon is the format and the check for that part, and never the authorisation.

## What an access token tells your API

When a request arrives with `Authorization: Bearer …`, your API learns a few things, and they are the right things for its question, which is whether this call may go ahead:

- **It was issued by an authorisation server you trust**, because the token verifies against that server's key or because introspection says it is active.
- **What it covers**: the scopes, the audience and the expiry.
- **Which client asked for it**, as the client registered itself.
- **Often, whose resources it reaches**: the `sub` of the person or account that agreed.

That is authorisation, and OAuth does it well. Nothing below argues for replacing it.

## What it does not tell your API

- **Which agent is calling.** One person's token, or one client registration, is often shared by every agent that person runs. Two agents with the same token are the same caller to your API, and revoking one revokes both.
- **Whether a person or software is behind it.** A token a person agreed to looks the same whether they use it or their agent does.
- **Who stands behind the client.** A client's name and details are what it said when it registered. With dynamic registration, any software can register under any name. Nothing in the token points at a domain an outsider can check.
- **That the request is the one the agent sent.** A bearer token works for whoever holds it. Sender-constrained tokens, such as DPoP, bind a token to a key, which stops a stolen token being replayed; they still do not say whose key it is.
- **Anything outside your own system.** A token your server issued means nothing to a vendor's API, and an email carries no token at all.

## What a card and a signature add

An agent card answers who. It is a JWS signed under a key its domain publishes, and it names the agent's address and stable `sub`, its `kind`, the `operator` domain the company proved, who it acts for in `on_behalf_of`, and a coarse standing. [What is an agent ID?](https://pidgeon.ai/blog/what-is-an-agent-id) walks through one claim by claim.

A signature on each request answers whether the request is what the agent sent. Post signs HTTP requests with HTTP Message Signatures (RFC 9421) in the profile of Cloudflare's Web Bot Auth: tag `web-bot-auth`, `keyid` the key's JWK thumbprint, valid for five minutes. It covers the method, the host, the path and query, `Signature-Agent` (`https://post.acme.com`), the agent's address as `From`, and the body's `Content-Digest`. A receiver that already checks Web Bot Auth signatures needs nothing new; Post adds the address and the card beside the signature.

Your API can hand the request it received to the verifier, with no key:

```sh
curl -X POST https://pidgeon.ai/api/v1/verify \
  -H 'content-type: application/json' \
  -d '{
    "request": {
      "method": "POST",
      "url": "https://api.you.com/v1/refunds",
      "headers": { "signature-input": "…", "signature": "…", "signature-agent": "…", "from": "support-agent@acme.com", "content-digest": "…" },
      "body": "{\"order\":\"A-1\"}"
    }
  }'
```

The answer has the `verdict` (`verified`, `caution`, `failed` or `unsigned`), the sender, the operator with whether its domain is verified, and the agent's card. A `From` counts as the sender only when it is at the domain whose key verified the signature.

## How the two fit in one request

The access token and the signature travel side by side, each answering its own question:

| Question                                  | Answered by                                    |
| ----------------------------------------- | ---------------------------------------------- |
| May this call read or change this?        | The access token, and your authorisation code  |
| Whose account or resources does it reach? | The token's `sub`, from your identity provider |
| Which agent is calling?                   | The card's `sub` and address                   |
| Which company runs it?                    | The card's `operator`, a proved domain         |
| Who does it act for?                      | The card's `on_behalf_of`                      |
| Was this request changed?                 | The request signature                          |

So a refund call can be allowed by the token's scope, and logged against `support-agent@acme.com`, operated by `acme.com`, acting for Acme Ltd. When that agent misbehaves, you can block that agent and leave the person's other agents alone, because you know it is that agent.

## The costs

- **The agent holds a key.** With `pidgeon id` the key is made on the operator's machine and Pidgeon only ever sees the public half. Losing it means proving the domain again with a new one.
- **The signature lives for five minutes.** Clocks need to be roughly right on both sides.
- **The public verifier is limited per IP**: 300 checks an hour. An API taking agent traffic at volume checks signatures itself, against the keys at `https://post.<domain>/.well-known/http-message-signatures-directory`, and fetches the card from the agent's `post.<domain>`.
- **The signature does not cover your `Authorization` header.** It binds the request to the agent, not the token to the agent. Binding the token is DPoP's job.

## One token that carries both

Sign in with Pidgeon is coming soon. It uses OAuth token exchange (RFC 8693): the agent presents its card in a signed request, and gets an ID token for your site whose `sub` is the agent, and an access token for your API that can name the agent in its `act` claim and who it acts for in its `sub`. Your identity provider and your scopes stay where they are. Until it ships, the verifier above is the way to check an agent, and it works today.

## When OAuth is enough

If your agents call only your own APIs, each with its own client registration, and nobody outside needs to know which agent did what, scopes and rate limits may be all you need. The gap opens when an agent reaches an API whose owner did not issue its credentials, or when one person's token is doing the work of several agents.

## Start

[Checking a message](https://pidgeon.ai/docs/verifying.md) has every verdict and warning. [Agents](https://pidgeon.ai/docs/agents.md) shows the card and how to issue one. [The protocol](https://pidgeon.ai/protocol) is Post, the draft both formats come from. To hear when Sign in with Pidgeon ships, join the list on its [page](https://pidgeon.ai/sign-in-with-pidgeon).

## Questions

### Does an OAuth access token identify an AI agent?

Not on its own. It says a trusted server issued it, with which scopes and often for which person, but not which agent holds it or which company runs that agent.

### Should I replace OAuth to verify agents?

No. OAuth and your identity provider keep authorisation and your people. A signed agent card and a signature on each request add who the agent is and that the request is unchanged.

### How does an agent sign an HTTP request in Post?

With HTTP Message Signatures (RFC 9421) in Web Bot Auth’s profile, covering the method, host, path, query, Signature-Agent, From and Content-Digest. A signature is good for five minutes.

### Is Sign in with Pidgeon available?

It is coming soon. Today an API can check a signed agent request with POST /v1/verify, which needs no key.
