Step by step · Poppy · Personal Agent Protocol draft 0.1

Get on Poppy: the full Personal Agent Protocol guide

Every step from no poppy.json to agent-ready: discovery, sign-in with Auth0, Okta, Cognito, Firebase, Clerk, Keycloak or your own login, the Poppy gateway, scopes and each interface.

The key idea

Publish poppy.json, keep your current login, and add the one piece most companies lack: an OAuth issuer that knows Poppy's rules for agents. For most setups that is a small gateway in front of the sign-in you already have.

This guide takes a company from nothing to ready for personal agents under Personal Agent Protocol draft 0.1. Every step is useful on its own, so you can ship them one at a time. Run the checker as you go: each item it reports links back to the step that fixes it.

1. Publish poppy.json

Agents start by fetching https://yourdomain.com/.well-known/poppy.json. Serve it over https as JSON, with caching headers. The smallest useful file names your company and your website:

{
  "protocol_version": "0.1",
  "organization": { "name": "Your company", "domain": "yourdomain.com" },
  "web": {}
}

That alone makes you discoverable: agents browse your site as signed-out visitors. organization.domain must be the domain agents asked (a leading www. is ignored). Every field, with three starter files, is in How to write your poppy.json.

2. Choose how users sign in through an agent

Poppy has three sign-in types. You list the ones you offer under auth, each with the scopes it can grant:

  • Direct: the user signs in on your own page in their browser and approves access. This is ordinary OAuth (authorization code with PKCE). Start here.
  • Device: the agent shows a link and a code; the user approves on any device. Good for agents that live in a chat or a speaker.
  • Mediated: the agent sends the user's credentials to you. Offer it only if you must; you then have to rate-limit attempts, ask for one-time codes when a sign-in looks unusual, and tell the user when an agent signs in.

Whichever you pick, the user signs in with whatever your login already supports: passwords, passkeys, Google or Apple. Poppy does not change your login; it adds a consent step that says which agent is asking and for what.

3. Your sign-in setup

Poppy runs on an OAuth authorization server that knows its rules for agents. Few companies have one that does all of it today, so the question is what yours already does and what to add. Find your setup below. The checker guesses it from your site and shows the same plan.

Not sure what you use?

  1. Open your site in a private window and click Sign in.
  2. Look at the address bar on the sign-in page. auth0.com means Auth0; okta.com means Okta; amazoncognito.com means Amazon Cognito; microsoftonline.com, b2clogin.com or ciamlogin.com mean Microsoft Entra; /realms/ in the address means Keycloak.
  3. If it stays on your own domain, it is your own code or a sign-in service on a custom domain. Ask your developers.

Or send your developers this:

Quick question: what handles customer sign-in on our site? Is it our own code or a service like Auth0, Okta, Cognito, Clerk, Firebase or Supabase? I am checking what we need for the Personal Agent Protocol (flow.engineer/poppy).

Auth0

You use this if: Your sign-in page is on a *.auth0.com address, or on a custom domain you set up in Auth0 (for example login.yourdomain.com).

Our recommendation: Keep Auth0 for login and put a Poppy gateway in front of it.

Already there

  • A hosted sign-in page with passwords, passkeys and social logins
  • Authorization code with PKCE, refresh tokens and revocation
  • Device authorization flow, once you enable it for an application

What to add

  • poppy_domains: you cannot add fields to Auth0's discovery document
  • Agents identified by their client_id URL: Auth0 expects registered applications
  • A grant that starts a signed-out Poppy session and carries session_id
  • DPoP-bound tokens: check whether your tenant and plan offer them
  1. Keep Auth0 for signing users in. Nothing changes for your customers. Create one Regular Web Application in Auth0 for the gateway, with https://auth.yourdomain.com/callback as its callback URL. That is the only client Auth0 needs to know; agents never talk to Auth0 directly.
  2. Run a Poppy gateway at auth.yourdomain.com. A small service that is your Poppy issuer. It trusts agents by their client_id URL, starts signed-out sessions, binds tokens to the agent's DPoP key, shows your consent page, issues Account Tokens and revokes them. It sends users to Auth0 only to prove who they are. See "The Poppy gateway" in the full guide for every endpoint it serves.
  3. Publish the gateway's metadata with poppy_domains. Serve this at https://auth.yourdomain.com/.well-known/oauth-authorization-server. poppy_domains is what lets agents trust that this issuer speaks for your domain.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at the gateway. Save this at https://yourdomain.com/.well-known/poppy.json. Start with direct sign-in; add device sign-in if you want people to approve on their phone.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Link agent sessions to your users. When sign-in comes back from Auth0, store the account on the session: keep Auth0's user ID (the sub claim) as the account. Your APIs, website and company agent then check the gateway's token (and its DPoP proof) at the edge and read the account and scopes from it.
  6. Run the checker again. It should reach "Sessions and sign-in wired". Then test a real session, DPoP and sign-in with an agent before you announce support.

Okta

You use this if: Your sign-in page is on an *.okta.com or *.oktapreview.com address, or an Okta custom domain.

Our recommendation: Keep Okta for login and put a Poppy gateway in front of it.

Already there

  • Hosted sign-in with your existing policies and MFA
  • Authorization code with PKCE, refresh tokens, revocation and the device authorization grant
  • private_key_jwt client authentication and DPoP-bound tokens for apps that turn them on

What to add

  • poppy_domains in its metadata
  • Agents identified by their client_id URL rather than registered apps
  • A grant that starts a signed-out Poppy session and carries session_id
  1. Keep Okta for signing users in. Nothing changes for your customers. Create one OIDC web app integration in Okta for the gateway, with https://auth.yourdomain.com/callback as its sign-in redirect URI.
  2. Run a Poppy gateway at auth.yourdomain.com. A small service that is your Poppy issuer. It trusts agents by their client_id URL, starts signed-out sessions, binds tokens to the agent's DPoP key, shows your consent page, issues Account Tokens and revokes them. It sends users to Okta only to prove who they are. See "The Poppy gateway" in the full guide for every endpoint it serves.
  3. Publish the gateway's metadata with poppy_domains. Serve this at https://auth.yourdomain.com/.well-known/oauth-authorization-server. poppy_domains is what lets agents trust that this issuer speaks for your domain.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at the gateway. Save this at https://yourdomain.com/.well-known/poppy.json. Start with direct sign-in; add device sign-in if you want people to approve on their phone.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Link agent sessions to your users. When sign-in comes back from Okta, store the account on the session: keep Okta's user ID (the sub claim) as the account. Your APIs, website and company agent then check the gateway's token (and its DPoP proof) at the edge and read the account and scopes from it.
  6. Run the checker again. It should reach "Sessions and sign-in wired". Then test a real session, DPoP and sign-in with an agent before you announce support.

Amazon Cognito

You use this if: Your sign-in page is on an *.amazoncognito.com address (Cognito's hosted or managed login), or your app uses AWS Amplify to sign users in.

Our recommendation: Keep Cognito for login; a Poppy gateway does all of Poppy's OAuth.

Already there

  • A user pool with hosted sign-in, MFA and social logins
  • Authorization code with PKCE, refresh tokens and revocation

What to add

  • The device authorization grant
  • private_key_jwt: app clients use a client secret
  • DPoP-bound tokens
  • poppy_domains, agents by client_id URL, and Poppy's session grant
  1. Keep Cognito for signing users in. Nothing changes for your customers. Add one app client in your user pool for the gateway, with https://auth.yourdomain.com/callback as its callback URL. The gateway does device sign-in, DPoP and agent authentication itself, since Cognito does not.
  2. Run a Poppy gateway at auth.yourdomain.com. A small service that is your Poppy issuer. It trusts agents by their client_id URL, starts signed-out sessions, binds tokens to the agent's DPoP key, shows your consent page, issues Account Tokens and revokes them. It sends users to Cognito only to prove who they are. See "The Poppy gateway" in the full guide for every endpoint it serves.
  3. Publish the gateway's metadata with poppy_domains. Serve this at https://auth.yourdomain.com/.well-known/oauth-authorization-server. poppy_domains is what lets agents trust that this issuer speaks for your domain.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at the gateway. Save this at https://yourdomain.com/.well-known/poppy.json. Start with direct sign-in; add device sign-in if you want people to approve on their phone.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Link agent sessions to your users. When sign-in comes back from Cognito, store the account on the session: keep the Cognito user's sub as the account. Your APIs, website and company agent then check the gateway's token (and its DPoP proof) at the edge and read the account and scopes from it.
  6. Run the checker again. It should reach "Sessions and sign-in wired". Then test a real session, DPoP and sign-in with an agent before you announce support.

Microsoft Entra ID

You use this if: Your sign-in page is on login.microsoftonline.com, *.b2clogin.com or *.ciamlogin.com (Entra External ID for customers).

Our recommendation: Keep Entra for login and put a Poppy gateway in front of it.

Already there

  • Hosted sign-in for your customers or staff, with conditional access
  • Authorization code with PKCE, refresh tokens and the device code flow

What to add

  • poppy_domains, agents by client_id URL, and Poppy's session grant
  • Poppy-style DPoP binding and a standard token revocation endpoint for this use: check your tenant
  1. Keep Entra for signing users in. Nothing changes for your customers. Register one web application for the gateway, with https://auth.yourdomain.com/callback as its redirect URI.
  2. Run a Poppy gateway at auth.yourdomain.com. A small service that is your Poppy issuer. It trusts agents by their client_id URL, starts signed-out sessions, binds tokens to the agent's DPoP key, shows your consent page, issues Account Tokens and revokes them. It sends users to Entra only to prove who they are. See "The Poppy gateway" in the full guide for every endpoint it serves.
  3. Publish the gateway's metadata with poppy_domains. Serve this at https://auth.yourdomain.com/.well-known/oauth-authorization-server. poppy_domains is what lets agents trust that this issuer speaks for your domain.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at the gateway. Save this at https://yourdomain.com/.well-known/poppy.json. Start with direct sign-in; add device sign-in if you want people to approve on their phone.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Link agent sessions to your users. When sign-in comes back from Entra, store the account on the session: keep the user's object ID (oid) as the account. Your APIs, website and company agent then check the gateway's token (and its DPoP proof) at the edge and read the account and scopes from it.
  6. Run the checker again. It should reach "Sessions and sign-in wired". Then test a real session, DPoP and sign-in with an agent before you announce support.

Firebase Authentication

You use this if: Your app signs users in with the Firebase SDK (firebase/auth), often with a *.firebaseapp.com sign-in handler.

Our recommendation: Add a Poppy gateway that signs users in with Firebase on its own page.

Already there

  • User sign-in with email, phone, Google, Apple and more
  • ID tokens your server can verify with the Admin SDK

What to add

  • An OAuth authorization server: Firebase signs users in to your own app, not to outside clients such as agents
  1. Keep Firebase for signing users in. Nothing changes for your customers. The gateway's sign-in page uses the same Firebase sign-in your app uses, then sends the ID token to the gateway's server, which verifies it with the Firebase Admin SDK.
  2. Run a Poppy gateway at auth.yourdomain.com. A small service that is your Poppy issuer. It trusts agents by their client_id URL, starts signed-out sessions, binds tokens to the agent's DPoP key, shows your consent page, issues Account Tokens and revokes them. It sends users to Firebase only to prove who they are. See "The Poppy gateway" in the full guide for every endpoint it serves.
  3. Publish the gateway's metadata with poppy_domains. Serve this at https://auth.yourdomain.com/.well-known/oauth-authorization-server. poppy_domains is what lets agents trust that this issuer speaks for your domain.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at the gateway. Save this at https://yourdomain.com/.well-known/poppy.json. Start with direct sign-in; add device sign-in if you want people to approve on their phone.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Link agent sessions to your users. When sign-in comes back from Firebase, store the account on the session: keep the Firebase uid as the account. Your APIs, website and company agent then check the gateway's token (and its DPoP proof) at the edge and read the account and scopes from it.
  6. Run the checker again. It should reach "Sessions and sign-in wired". Then test a real session, DPoP and sign-in with an agent before you announce support.

Supabase Auth

You use this if: Your app signs users in with supabase-js, against a *.supabase.co project.

Our recommendation: Add a Poppy gateway that signs users in with Supabase on its own page.

Already there

  • User sign-in (passwords, magic links, social) and JWTs for your app

What to add

  • Poppy's OAuth pieces for outside agents: client_id URLs, the session grant, DPoP and poppy_domains
  1. Keep Supabase for signing users in. Nothing changes for your customers. The gateway's sign-in page uses supabase-js as your app does, then sends the session's access token to the gateway's server, which checks it with your project.
  2. Run a Poppy gateway at auth.yourdomain.com. A small service that is your Poppy issuer. It trusts agents by their client_id URL, starts signed-out sessions, binds tokens to the agent's DPoP key, shows your consent page, issues Account Tokens and revokes them. It sends users to Supabase only to prove who they are. See "The Poppy gateway" in the full guide for every endpoint it serves.
  3. Publish the gateway's metadata with poppy_domains. Serve this at https://auth.yourdomain.com/.well-known/oauth-authorization-server. poppy_domains is what lets agents trust that this issuer speaks for your domain.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at the gateway. Save this at https://yourdomain.com/.well-known/poppy.json. Start with direct sign-in; add device sign-in if you want people to approve on their phone.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Link agent sessions to your users. When sign-in comes back from Supabase, store the account on the session: keep the Supabase user ID as the account. Your APIs, website and company agent then check the gateway's token (and its DPoP proof) at the edge and read the account and scopes from it.
  6. Run the checker again. It should reach "Sessions and sign-in wired". Then test a real session, DPoP and sign-in with an agent before you announce support.

Clerk

You use this if: Your app loads Clerk (@clerk/... or clerk.browser.js), or signs in through a clerk.yourdomain.com or *.clerk.accounts.dev address.

Our recommendation: Keep Clerk for login and put a Poppy gateway in front of it.

Already there

  • Hosted sign-in components, sessions and user management
  • Clerk can act as an OAuth provider for other apps

What to add

  • Poppy's rules for agents: client_id URLs, the signed-out session grant with session_id, DPoP binding and poppy_domains
  1. Keep Clerk for signing users in. Nothing changes for your customers. The gateway's sign-in page uses Clerk's sign-in, then the gateway's server verifies the Clerk session.
  2. Run a Poppy gateway at auth.yourdomain.com. A small service that is your Poppy issuer. It trusts agents by their client_id URL, starts signed-out sessions, binds tokens to the agent's DPoP key, shows your consent page, issues Account Tokens and revokes them. It sends users to Clerk only to prove who they are. See "The Poppy gateway" in the full guide for every endpoint it serves.
  3. Publish the gateway's metadata with poppy_domains. Serve this at https://auth.yourdomain.com/.well-known/oauth-authorization-server. poppy_domains is what lets agents trust that this issuer speaks for your domain.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at the gateway. Save this at https://yourdomain.com/.well-known/poppy.json. Start with direct sign-in; add device sign-in if you want people to approve on their phone.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Link agent sessions to your users. When sign-in comes back from Clerk, store the account on the session: keep the Clerk user ID as the account. Your APIs, website and company agent then check the gateway's token (and its DPoP proof) at the edge and read the account and scopes from it.
  6. Run the checker again. It should reach "Sessions and sign-in wired". Then test a real session, DPoP and sign-in with an agent before you announce support.

Keycloak

You use this if: Your sign-in page has /realms/<name>/ in its address.

Our recommendation: Keycloak can do most of it: add the missing pieces with a thin gateway (or your own extensions).

Already there

  • Your own authorization server, which you control
  • Authorization code with PKCE, the device authorization grant, refresh tokens and revocation
  • "Signed JWT" client authentication (private_key_jwt), and DPoP in recent versions: check yours

What to add

  • poppy_domains in the realm's metadata
  • Agents identified by their client_id URL
  • Poppy's signed-out session grant and session_id
  1. Use a realm for customers. Your customers' realm is where agents' users sign in. Turn on the device authorization grant and DPoP for it if your version supports them.
  2. Put a thin gateway in front of the realm. Serve the metadata (the realm's endpoints plus poppy_domains), resolve agents' client_id URLs, and handle the jwt-bearer session grant and session_id. Everything else (sign-in, consent, refresh, revocation) can pass through to Keycloak. Teams that write Keycloak extensions can add the grant there instead.
  3. Publish the metadata with poppy_domains. At your issuer's well-known address:
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at it. Save this at https://yourdomain.com/.well-known/poppy.json.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Run the checker again. Then test a real session, DPoP and sign-in with an agent.

Your own OAuth server

You use this if: Your domain (or auth. or login. on it) already publishes an OpenID configuration, so something there is an OAuth server.

Our recommendation: Find out what the server is, add what it lacks, or put a gateway in front.

Already there

  • An authorization server whose metadata you may be able to change

What to add

  • Whatever it does not support of: poppy_domains, agents by client_id URL, the jwt-bearer session grant with session_id, DPoP, PKCE, device sign-in and revocation
  1. Find out what runs it. Ask your team which product or library serves your OpenID configuration. The checker's "Advertised OAuth capabilities" row lists what its metadata says it can do.
  2. Add what it lacks, or put a gateway in front. If you can change it, add poppy_domains to its metadata and the missing grants. If you cannot, a Poppy gateway in front of it handles agents, sessions and DPoP and uses your server only to sign users in.
  3. Publish the metadata with poppy_domains.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at it.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }

Your own sign-in code

You use this if: Users sign in on your own site, with accounts in your own database: for example Auth.js (NextAuth), Devise, Django, Laravel, Passport or code you wrote.

Our recommendation: Add an OAuth server next to your login, using a library that already speaks most of Poppy.

Already there

  • Your users, passwords and sign-in page
  • Full control: you can add an OAuth server next to your login

What to add

  • An OAuth authorization server, which Poppy is built on
  1. Pick an OAuth server library. In Node, node-oidc-provider supports PKCE, the device flow, DPoP, private_key_jwt, revocation, extra metadata fields and custom grant types, which covers almost all of Poppy. In other stacks, run Ory Hydra (it hands login and consent to your app) or your framework's OAuth server library, and check each one for DPoP and the jwt-bearer grant.
  2. Reuse your sign-in page. The library asks your app to sign the user in and show consent. Your existing login page does the first; the consent page shows the agent's name and domain, the account and the scopes.
  3. Add what Poppy adds. Resolve agents by their client_id URL, register the jwt-bearer grant that starts a signed-out session, and carry session_id through sign-in and refresh. Store sessions with the agent, the user ID and the account.
  4. Publish the metadata with poppy_domains.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  5. Point poppy.json at it. Save this at https://yourdomain.com/.well-known/poppy.json.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  6. Run the checker again. Then test a real session, DPoP and sign-in with an agent.

Another sign-in service

You use this if: Your sign-in page is on another company's domain, or a custom domain run by a sign-in service.

Our recommendation: Keep it for login and put a Poppy gateway in front of it.

Already there

  • Hosted sign-in for your users

What to add

  • Most services cannot add poppy_domains, read agents' client_id URLs or start Poppy sessions
  1. Keep your sign-in service for signing users in. Nothing changes for your customers. Register one web application with it for the gateway, with https://auth.yourdomain.com/callback as its redirect URL.
  2. Run a Poppy gateway at auth.yourdomain.com. A small service that is your Poppy issuer. It trusts agents by their client_id URL, starts signed-out sessions, binds tokens to the agent's DPoP key, shows your consent page, issues Account Tokens and revokes them. It sends users to your sign-in service only to prove who they are. See "The Poppy gateway" in the full guide for every endpoint it serves.
  3. Publish the gateway's metadata with poppy_domains. Serve this at https://auth.yourdomain.com/.well-known/oauth-authorization-server. poppy_domains is what lets agents trust that this issuer speaks for your domain.
    {
      "issuer": "https://auth.yourdomain.com",
      "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
      "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
      "token_endpoint": "https://auth.yourdomain.com/oauth/token",
      "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
      "poppy_domains": ["yourdomain.com"],
      "token_endpoint_auth_methods_supported": ["private_key_jwt"],
      "grant_types_supported": [
        "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "authorization_code",
        "urn:ietf:params:oauth:grant-type:device_code",
        "refresh_token"
      ],
      "code_challenge_methods_supported": ["S256"],
      "dpop_signing_alg_values_supported": ["ES256", "RS256"],
      "authorization_response_iss_parameter_supported": true
    }
  4. Point poppy.json at the gateway. Save this at https://yourdomain.com/.well-known/poppy.json. Start with direct sign-in; add device sign-in if you want people to approve on their phone.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "auth": {
        "issuer": "https://auth.yourdomain.com",
        "direct": { "scopes": ["poppy:read", "poppy:write"] },
        "device": { "scopes": ["poppy:read"] }
      },
      "web": { "browser_session_endpoint": "https://yourdomain.com/poppy/browser-session" }
    }
  5. Link agent sessions to your users. When sign-in comes back from your sign-in service, store the account on the session: keep the service's user ID as the account. Your APIs, website and company agent then check the gateway's token (and its DPoP proof) at the edge and read the account and scopes from it.
  6. Run the checker again. It should reach "Sessions and sign-in wired". Then test a real session, DPoP and sign-in with an agent before you announce support.

No customer accounts

You use this if: Customers never sign in to your site.

Our recommendation: Publish poppy.json for your website now; add sessions only if you list APIs or an agent.

Already there

  • Nothing to protect behind sign-in, so the first step is small

What to add

  • Sessions, if you want agents to use APIs or your company agent
  1. Publish a website-only poppy.json. Agents can then find you and browse your site. No OAuth server is needed.
    {
      "protocol_version": "0.1",
      "organization": { "name": "Your company", "domain": "yourdomain.com" },
      "web": {}
    }
  2. Later: signed-out sessions for APIs or an agent. Listing apis or agent needs auth with an issuer that starts signed-out sessions (the jwt-bearer grant). With no sign-in types listed, sessions simply stay signed out.

Sign-in providers add features often. Where a plan says to check your plan or version, confirm it in the provider's documentation before you build around it.

4. OAuth metadata and poppy_domains

Whichever issuer you end up with, it publishes RFC 8414 metadata at <issuer>/.well-known/oauth-authorization-server. Agents check two things before trusting your poppy.json: the metadata's issuer is exactly your auth.issuer, and poppy_domains lists your domain. It also lists the endpoints agents call, and should advertise what they need:

{
  "issuer": "https://auth.yourdomain.com",
  "authorization_endpoint": "https://auth.yourdomain.com/oauth/authorize",
  "device_authorization_endpoint": "https://auth.yourdomain.com/oauth/device",
  "token_endpoint": "https://auth.yourdomain.com/oauth/token",
  "revocation_endpoint": "https://auth.yourdomain.com/oauth/revoke",
  "poppy_domains": ["yourdomain.com"],
  "token_endpoint_auth_methods_supported": ["private_key_jwt"],
  "grant_types_supported": [
    "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "authorization_code",
    "urn:ietf:params:oauth:grant-type:device_code",
    "refresh_token"
  ],
  "code_challenge_methods_supported": ["S256"],
  "dpop_signing_alg_values_supported": ["ES256", "RS256"],
  "authorization_response_iss_parameter_supported": true
}

Hosted providers rarely let you add poppy_domains to their own document. That is one of the jobs of the gateway in the next step, which serves this document itself.

5. The Poppy gateway

For most companies the missing piece is a small service, which we call a Poppy gateway, at auth.yourdomain.com. It is your Poppy issuer and keeps your current login for proving who the user is. It does the parts no common sign-in product does out of the box:

  • Trusts agents by URL. An agent's client_id is an https URL serving its name, logo, keys and redirect URIs. The gateway fetches it (safely: no redirects, size and time limits, public addresses only), checks it, and verifies the agent's signed client assertions with its keys.
  • Starts sessions before sign-in. The jwt-bearer grant: the agent signs an assertion naming itself and an opaque user ID; the gateway answers with a short-lived Session Token, a session_id and signed_in: false.
  • Binds every token to the agent's key with DPoP, and checks each proof (signature, method and URL, time, one-time jti, token hash) with a replay cache shared by all servers.
  • Runs sign-in and consent. It sends the user to your login, then shows its own consent page, then returns an Account Token and signs in the session.
  • Revokes. Sign-out and Disconnect revoke the Account Token and sign out every session that used it.
EndpointWhat it does
GET /.well-known/oauth-authorization-serverYour metadata, with poppy_domains.
POST /oauth/tokenStarts signed-out sessions (jwt-bearer grant), exchanges sign-in codes and device codes, and turns Account Tokens into signed-in Session Tokens. Every call carries the agent's client assertion and a DPoP proof.
GET /oauth/authorizeDirect sign-in: sends the user to your current login, then shows your consent page.
POST /oauth/deviceDevice sign-in: a link and code the user opens on any device.
POST /oauth/revokeSign-out: revokes the Account Token and signs out its sessions.
Account settings pageLists connected agents, their scopes and last use, with Disconnect.

Your APIs, website and company agent then accept the gateway's tokens: check the token and its DPoP proof at the edge, and pass the verified agent, user, account and scopes to the services behind it. MCP servers are the one exception: they take Bearer tokens issued for that one server. More detail on each piece is in Set up your OAuth server for personal agents.

6. Enforce scopes everywhere

poppy:read lets an agent view the account; poppy:write lets it make changes. They are separate: write does not include read. You can add your own scopes (describe each in auth.custom_scopes; the poppy: prefix is reserved). Then decide which operations need which scope and check them in one shared place that your website, APIs, company agent and support staff's tools all call. Never let a model decide whether an action is allowed.

7. Interfaces

Add these one at a time. Agents prefer APIs and company agents to clicking through pages, because they are faster and change less often.

Your website

Add web.browser_session_endpoint so a signed-in agent can browse your site as that session. The endpoint takes a form POST with the agent's signed browser assertion, checks it (signature, audience, a lifetime of at most 60 seconds, unused jti, an active session, return_to on your domain), sets your own Secure, HttpOnly, SameSite=Lax cookie and redirects with 303. Anything invalid gets 400 and no cookie. The cookie follows the session: signing out or changing scopes shows on the next page.

An MCP server

The quickest API for agents. List it under apis with type mcp. It must use Streamable HTTP with MCP 2025-06-18 or later, answer 401 without a token, publish protected resource metadata (RFC 9728) naming your issuer in authorization_servers, accept only Bearer session tokens issued for its own URL, and enforce their scopes.

An OpenAPI API

List the URL of its OpenAPI 3.0 or 3.1 description. Calls carry Authorization: DPoP <token> plus a proof; answer invalid_token (401), sign_in_required or insufficient_scope (403) in WWW-Authenticate. Say in the description's securitySchemes that tokens are DPoP-bound.

Your company agent

List a conversation endpoint under agent.protocols with type poppy. It is a small JSON API: start a conversation, send messages, read events (long polling or server-sent events), ask for a person, close. Every message says whether a person or an AI wrote it. When your agent needs the account, it adds an authorization event and the personal agent signs the user in. Let it do everything customers can do by phone, so personal agents never need to call.

8. Test and announce

  1. Run the checker until it reports "Ready, from the outside".
  2. Test what it cannot see with a real agent: starting a session, DPoP checks and replays, each sign-in type, Account Tokens after a restart, sign-out, and a write refused without poppy:write.
  3. Add connected agents to your account settings and say on your site that you support the Personal Agent Protocol.

The protocol is a draft. Watch personalagentprotocol.org for changes, and see the compliance checklist for every requirement in one list.

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.