Skip to content
Docs

Add AgentID to any OIDC stack

AgentID is a standard OpenID Connect provider, so a generic connector, an OIDC library, or a client you wrote yourself all take it the same way: one discovery URL, a client id, a handful of settings, and a sign-in button nothing draws for you. This page is the version of the provider guides for everything without one.

On this page

Start from discovery#

Anything with an auto-discovery or well-known field takes this one URL and derives the rest — endpoints, signing keys, and the algorithms it is allowed to accept. Prefer it to typing endpoints in by hand: keys rotate, and a client that reads their location from this document rotates with them.

discovery
https://auth.agentid.com/.well-known/openid-configuration

The whole document, so you can see what your library is about to derive from it:

GET /.well-known/openid-configuration
{
  "issuer": "https://auth.agentid.com",
  "authorization_endpoint": "https://auth.agentid.com/v0/authorize",
  "token_endpoint": "https://auth.agentid.com/v0/token",
  "userinfo_endpoint": "https://auth.agentid.com/v0/userinfo",
  "jwks_uri": "https://auth.agentid.com/v0/jwks.json",
  "registration_endpoint": "https://auth.agentid.com/v0/register",
  "grant_types_supported": ["authorization_code"],
  "response_types_supported": ["code"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
  "code_challenge_methods_supported": ["S256"],
  "authorization_response_iss_parameter_supported": true,
  "scopes_supported": ["openid", "email", "profile", "owner_profile", "owner_email"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["ES256"],
  "claims_supported": ["iss", "sub", "aud", "exp", "iat", "jti", "org", "scope",
                       "email", "email_verified", "name", "owner_name", "owner_email"]
}

Watch out. If your connector has no discovery field, copy the endpoints out of that document rather than out of prose — including jwks_uri, which is the one people paste from memory. Everything except discovery itself is versioned, so a hardcoded key URL is the thing that breaks quietly later.

The fields a connector asks for#

The forms differ; the values do not. Whatever your provider calls these, they are what a connection to AgentID is made of.

connection
Connector id     agentidDisplay name     AgentIDIssuer           https://auth.agentid.comDiscovery URL    https://auth.agentid.com/.well-known/openid-configurationClient ID        https://yourapp.comClient secret    (registered clients only)Scopes           openid email profilePKCE             on, S256

Use the same two names every other guide asks for: the id agentid and the display name AgentID. The id is what your code will name the connection and, in most frameworks, what the callback path is built from, so picking your own spelling makes every snippet in these guides unpasteable. Where the display name surfaces — on the button an agent’s owner clicks — is the section below, and it is the one part of this a generic setup has to build rather than configure.

A client_id that is a URL you control needs no registration or secret and may request the three agent-identity scopes: openid, email and profile. Registering swaps in an issued client_id and secret and opens the owner scopes. Which one you want is on client tiers.

Watch out. Scopes are selected per sign-in, not during client registration, and the field is easy to leave at a connector’s placeholder. Registered clients may request any supported scope at /authorize; choose only what your app needs here. Add the owner scopes only when the app needs the human owner’s identity. The agent’s signing credential remains the final gate for that information.

Branding the button#

A dashboard provider draws this for you: it has a display name field and a logo field, and it renders the button on its own login widget. A generic connector may have one of those, or neither, and a client you wrote has neither by definition — so this is the piece of a custom setup that nothing supplies.

The label has one requirement, which is that it reads AgentID — one word, both capitals, never shortened. The verb is yours: use whatever the other buttons on that page already say, so the three of them read as one list rather than as two conventions and an advertisement.

button label
Sign in with AgentIDContinue with AgentIDLog in with AgentID

The mark reaches the button one of three ways, and which one you are in decides the rest. A logo URL field takes an absolute URL, and the files are served from this origin for exactly that purpose — paste one and you are done. An upload field takes the same file downloaded from the brand page. No field at all means you are drawing the button, in which case serve the asset from your own origin rather than hotlinking it, so your login page does not depend on ours being reachable.

mark
Light backdrop     https://auth.agentid.com/brand/icon-black.svgDark backdrop      https://auth.agentid.com/brand/icon-white.svgBackdrop unknown   https://auth.agentid.com/brand/icon-black-on-white.svg

Pick by the backdrop the mark will actually land on. The first two are transparent, so they need you to know: black on a light button, white on a dark one. The third is a square tile that carries its own background and is the answer when you cannot know — a connector rendering it on a widget you do not control, or one field serving a light and a dark theme. If you are drawing the button yourself and your app has both themes, swap the transparent pair with the theme rather than reaching for the tile.

Watch out. Name the variant every time. There is no theme-neutral file to link and that is deliberate: a URL that does not say which mark it is gets pasted into a field that cannot switch on backdrop, and the mark comes out invisible against its own button. Every variant is offered as a PNG as well, for the fields that will not take an SVG.

Show the mark with the name rather than on its own, and match the shape of the buttons around it. The full rules — clear space, the 16px floor, and what you may not do to the mark — plus every file in both formats are on the brand page, which is also the URL to forward to whoever owns your login page.

Defaults that will not work#

Five settings, and a generic connector is likely to ship at least two of them set the other way. Each is drawn from the discovery document above rather than from a preference.

id_token signing
ES256, and nothing else is advertised. A library pinned to RS256 — a common default — rejects every token, usually as a signature failure rather than as an algorithm one.
Response type
code. Implicit and hybrid are not advertised and cannot complete, so a connector offering id_token or code id_token has to be set back to code.
PKCE
PKCE is S256-only; plain is not offered. Open clients require it. A registered client may omit it only when it sends and validates an OIDC nonce; AgentID enforces the verifier whenever a connector sends a challenge.
Refresh tokens
None. One grant type is advertised, authorization_code, and there is no offline_access scope to ask for one with.
Client authentication
client_secret_basic, client_secret_post, or none for an open client. private_key_jwt is not offered on either side.

There is no end-session endpoint either, so signing out ends your own session and nothing else. Because there are no refresh tokens, a session that needs renewing runs the whole flow again — and that is a real cost here rather than a redirect nobody notices, since the agent has to approve again. Give the session you mint a lifetime that reflects it.

The sign-in is not instant#

This is the one place AgentID does not behave like the provider next to it in your dashboard. There is no password form: the browser lands on a waiting page at the issuer, and the sign-in completes only once the agent that owns the inbox approves it out of band by signing the request id. The page shows the deadline it was minted with, and what the agent has to do is on approving a sign-in.

Nothing on your side is holding a connection while that happens — the browser is parked on our origin, and your callback is not called until it is over. What has to survive is the state you stashed before the redirect: the state value, and the PKCE code verifier when you use PKCE, both have to still be there minutes later, which is longer than some in-memory or short-TTL stores keep them.

Watch out. A verifier or state entry that expires mid-approval fails at the callback as a mismatch, which reads like an attack rather than a timeout and is why this one is worth checking before you ship rather than after. Anything you put a deadline on should outlast the one the waiting page is counting down.

Mapping claims onto your user#

sub is the key to store: it is derived from the inbox, and the same agent returns the same one on every sign-in. email arrives with email_verified, which is true whenever it is present. name follows the profile scope and is omitted when the inbox has no display name set, so a mapping that requires it will drop sign-ins that are perfectly valid.

Check aud against whichever client_id you are using — the URL for an open client, the issued id for a registered one. The authorization response also carries iss, which a hand-written client should compare against the issuer alongside its state check; a connector that already knows about the parameter does this for you.

Watch out. The owner claims are not in the id_token and no id_token can carry them. owner_name and owner_email are served from /userinfo with the access token from the same exchange. This is the one that catches generic connectors: most map their profile fields out of the id_token alone, so a correctly configured client asking for owner information still maps nothing until you point it at /userinfo.

A registered client also gets scope, which names what the sign-in actually granted — read it rather than assuming you received what you asked for — and org, which is what you allowlist on to accept agents from only certain organizations. Every claim, and what each scope grants, is defined on the integration reference.

Checking it works#

Before the first sign-in, confirm your client is reading the right issuer and can see the keys it will have to verify against. Both are public and neither needs a credential.

shell
curl -s https://auth.agentid.com/.well-known/openid-configuration | jq .

# The signing keys, with the ids a token's kid will name:
curl -s https://auth.agentid.com/v0/jwks.json | jq '.keys[] | {kid, alg, crv}'

Then run one real sign-in end to end, which is the only test that exercises the approval. A correct one leaves you with an id_token whose aud is your own client_id and whose sub you can store; signing in again from the same inbox returns that same sub. If you asked for owner information, the proof is a /userinfo response carrying it — not a claim in the token.

Watch out. Owner information can be refused after you have configured everything correctly: the agent’s signing credential has to permit it, and a sign-in that asks without that permission fails for the agent with a 403 rather than handing you a token quietly missing the claim. An integration that looks broken here is usually waiting on a permission on the agent side.

On Clerk, Supabase, Auth0 or Better Auth? Each of those is walked end to end, with the fields named the way that product names them: provider guides.

The requests themselves — the authorization redirect and the token exchange, parameter by parameter — are under manual setup, with endpoints, scopes and token claims on the same page.