Sign-in · Poppy · Personal Agent Protocol draft 0.1

Set up your OAuth server for Poppy

What your OAuth server needs for the Personal Agent Protocol, from poppy_domains and agent keys to the session grant, DPoP and sign-out, and what to do when your provider lacks a piece.

The key idea

Poppy reuses standard OAuth. Your server has to trust agents by their client_id URL, start signed-out sessions from an agent's signed assertion, carry a session_id through sign-in, bind every token to the agent's DPoP key, and revoke on sign-out.

Most of the Personal Agent Protocol’s sign-in work happens in your OAuth authorization server. The good news: almost all of it is standard OAuth. The work is turning on the right pieces and adding a session ID that ties them together.

The pieces at a glance

Piece Standard Needed for
Metadata with poppy_domains RFC 8414 Every company with auth
Agents identified by a metadata URL OAuth Client ID Metadata Document Every company with auth
Client authentication with private_key_jwt RFC 7523 Every token request
Signed-out sessions via the JWT bearer grant, plus session_id RFC 7523 Every company with auth
DPoP-bound tokens RFC 9449 APIs and conversations
Authorization code with PKCE (S256), iss in redirects RFC 6749, 7636, 9207 Direct sign-in
Device authorization grant RFC 8628 Device sign-in
Refresh tokens as Account Tokens RFC 6749 Signing in once, using it later
Token revocation RFC 7009 Sign-out
Protected resource metadata RFC 9728 MCP servers

Metadata and poppy_domains

Publish RFC 8414 metadata at your issuer (for issuer https://auth.example.com, that is https://auth.example.com/.well-known/oauth-authorization-server). It must contain:

  • issuer, exactly the same string as auth.issuer in your poppy.json;
  • token_endpoint and revocation_endpoint;
  • authorization_endpoint if you offer direct sign-in, and device_authorization_endpoint if you offer device sign-in;
  • poppy_domains, listing every domain whose poppy.json names this issuer.

It is worth also advertising what agents will need: private_key_jwt in token_endpoint_auth_methods_supported, the JWT bearer grant in grant_types_supported, S256 in code_challenge_methods_supported, your DPoP algorithms in dpop_signing_alg_values_supported, and authorization_response_iss_parameter_supported: true.

Trusting agents by URL

A personal agent’s client_id is an https URL that returns its metadata: name, logo, jwks_uri, redirect_uris and token_endpoint_auth_method: private_key_jwt. There is no registration step by default. Your server:

  1. fetches the client_id URL (no redirects) and checks the document’s client_id equals that URL;
  2. checks jwks_uri and every redirect URI are https on the same domain as the client_id;
  3. verifies the agent’s client assertion with a key from its jwks_uri.

Fetch these safely: short timeouts, a size limit, and no private or loopback addresses. Cache them within bounds, refetch the key set when you see an unknown kid, and keep allowlists, blocklists and rate limits by client_id. You may require registration instead; an unregistered agent then gets invalid_client.

Starting a session

A session starts signed out. The agent sends a JWT bearer grant whose assertion names itself in iss and the user’s opaque ID in sub, with your token endpoint as aud, a lifetime of about a minute and a random jti:

POST /oauth/token
Content-Type: application/x-www-form-urlencoded
DPoP: <proof>

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<agent's signed assertion>
&client_id=https://agent.example/agent.json
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<signed client assertion>

Verify both assertions, check the assertion’s iss is the authenticated client, reject any jti you have seen, then answer with a new session:

{ "access_token": "…", "token_type": "DPoP", "expires_in": 3600, "scope": "", "session_id": "ses_2Lm0", "signed_in": false }

The agent renews a signed-out token by sending the same grant with session_id. Keep a record per session: the client_id, the user ID and, once signed in, the account. A session can be signed in to one account only; a request for another gets account_mismatch.

DPoP on every request

Session Tokens are bound to a key the agent holds. Every API and conversation request carries Authorization: DPoP <token> plus a DPoP proof, a small JWT signed by that key. On each request, check:

  • the proof’s signature, with the public key in its header, and that this is the key the token is bound to;
  • htm and htu match the request (build htu from your public origin, not the Host header behind a proxy);
  • iat is within about a minute, and jti hasn’t been used in that window (one replay cache shared by every server);
  • ath is the hash of the token sent.

You may require server nonces. The one exception is MCP: MCP authorization uses Bearer tokens, so an agent requests a token without a proof, with resource set to your MCP server’s URL, and that Bearer token must work only at that MCP server. Accept Bearer session tokens nowhere else.

Sign-in

Offer at least one of these, and list it under auth in poppy.json:

  • Direct: the authorization code flow with PKCE (S256 only). Your consent page shows the agent’s name, logo and client_id domain, the account, and each scope, and lets the user approve some or all of them. Return iss in every redirect. When the agent exchanges the code it adds session_id, and you sign in that session.
  • Device: RFC 8628. Opening the link must never approve by itself; the user signs in and approves on the page.
  • Mediated: the agent posts the user’s credentials (the fields you list) to your endpoint with its session token. You answer complete, code_required (with a one-time code sent to the user), failed or expired. This puts more on you: rate-limit attempts, never return credentials, ask for a code when a sign-in looks unusual, and tell the user when an agent signs in.

All three end the same way: a refresh token (the Account Token) and a signed-in Session Token for the current session. Grant no more than was requested and allowed for that sign-in type, and return the scopes actually granted.

Account Tokens and sign-out

An Account Token lets the agent start signed-in sessions later. Accept it only at your token and revocation endpoints, only from the client_id it was issued to, authenticated by its client assertion. Agents may ask for fewer scopes than it holds, never more.

Sign-out is revocation (RFC 7009): revoke the Account Token and sign out every session that used it. Check sign-in state on each request so it takes effect at once, or keep session tokens to a few minutes. Give users an account settings page listing each connected agent, its scopes and last use, with a Disconnect button.

Scopes only matter if they are enforced everywhere: on your website, your APIs, your company agent’s tools and your support staff’s tools. poppy:write does not include poppy:read.

If your provider can’t

Many hosted OAuth providers can do PKCE, device flow, refresh tokens and revocation today, but not all of them support client ID metadata documents, the JWT bearer grant with a custom session_id, DPoP-bound tokens or extra metadata fields. Check your provider’s documentation for each row of the table above. Where one is missing:

  • Metadata fields: serve the metadata document yourself at the well-known URL, copying your provider’s endpoints and adding poppy_domains. Agents only read it.
  • Sessions and DPoP: put a small gateway in front of your token endpoint and your APIs. It verifies agent assertions and DPoP proofs, issues or wraps tokens bound to the proof’s key, tracks sessions, and checks the key on every later request. Checking a proof’s signature alone is not enough.
  • Agents by URL: resolve the client_id document in that gateway and map it to a client your provider knows, or require registration.

Then run the checker to see what agents can see, and work through the checklist for the rest.

Free Poppy checker

See what your domain needs for the Personal Agent Protocol, one step at a time.

Check your domain

Written against Personal Agent Protocol (Poppy, PAP) draft 0.1, which may change. Flow is not affiliated with the protocol's authors.