Skip to content

DocsAdd AgentID to any OIDC stack

Add AgentID to any OIDC stack

Use this page if your stack has no guide of its own: a generic OIDC connector, an OIDC library, or code you wrote. You need one discovery URL, a client ID, a few settings, and your own sign-in button.

All docs
On this page

Recommended: set it up from your project

The CLI can collect an application name and one or more callback URLs, register the relying party and store its credentials. This generic path cannot edit a connector it does not recognize, so use the fields below to finish the provider-specific wiring.

terminal
npx @agentmail/agentid-cli init

The CLI keeps credentials in your project and leaves hosted-provider sign-in disabled until you opt in. See the CLI guide for scope selection, verification and cleanup. The manual steps below remain the fallback.

Start from discovery#

If your connector has an auto-discovery or well-known field, give it this URL and it finds everything else. Prefer it to typing endpoints by hand: signing keys rotate, and discovery keeps up with them.

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

Here is the full document:

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", "actor_type", "scope",
                       "email", "email_verified", "name", "preferred_username", "owner_sub",
                       "owner_name", "owner_email", "owner_email_verified"]
}

Watch out. No discovery field? Copy the endpoints from this document, including jwks_uri. Don’t type them from memory: every path except discovery has a version in it, and a wrong key URL fails quietly.

The fields a connector asks for#

Your connector may name these fields differently. The values are the same.

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 id agentid and the display name AgentID, like every other guide. Most frameworks build the callback path from the id, so a different spelling breaks the snippets in these guides. The display name goes on your sign-in button (see the button).

Two ways to get a client ID. A URL you control works as an open client: no secret, no setup, and the scopes openid, email and profile. For a secret and the owner scopes, create an application with the CLI or in the AgentID console (choose Other / custom integration) and use the Client ID it gives you.

Watch out. Scopes are sent with each sign-in, not set when you create the application, so fill in your connector’s scope field. Add owner_profile and owner_email only if you need the owner’s name and email. They also need the App: Share Owner permission on the agent’s AgentMail API key. Without it, the agent can’t finish the sign-in alone, and the organization owner has to approve it from their AgentMail account.

Branding the button#

Hosted providers like Clerk draw the button for you. A generic connector may not, and code you wrote never does, so you may need to build it.

Label it Continue with AgentID. The name is always AgentID: one word, never shortened, never AgentMail.

To add the logo: a logo URL field takes one of the URLs below. An upload field takes the same file from the brand page. No field at all? You are drawing the button yourself, so serve the file from your own site instead of linking to ours.

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

Black goes on a light button, white on a dark one. Use the tile when you don’t know the background, for example when one field serves both light and dark themes. If you draw the button yourself and support both themes, switch between black and white instead.

Watch out. Each file also comes as a PNG, for fields that won’t take an SVG.

Show the logo next to the name, and match the shape of the buttons around it. All the rules and files are on the brand page. Send that link to whoever owns your login page.

Don’t want a visible AgentID button? See the initiate login URI.

The initiate login URI#

Don’t want a visible AgentID button? Point the initiate login URI at a URL that starts your normal AgentID sign-in: save the state and PKCE verifier the way your button does, then redirect to the authorization endpoint.

Agents connect to your app from AgentMail by opening its initiate login URI. A sign-in page with an AgentID button works as one: AgentID tells the agent to choose AgentID there. Without an initiate login URI, AgentMail opens your login URL instead, if it is public https, and asks the agent to choose AgentID on that page.

AgentID adds iss and login_hint when it opens the URI. login_hint is the connecting inbox’s address; pass it on as the authorization request’s login_hint to keep the sign-in on that inbox.

Method
Opened with a GET, like a link. A route that only accepts POST will not work.
Address
An absolute https:// URL on a public host. No fragment, and no credentials in the URL.
Parameters
Leave out iss and login_hint. AgentID adds both each time it opens the URI.

Then open your application in the AgentID console, choose Settings › Edit, and paste that URL into Initiate login URL, or your sign-in page’s if you kept the button. Click Save changes.

Defaults that will not work#

Generic connectors often get at least two of these wrong by default. Check all five.

id_token signing
ES256 only. Many libraries default to RS256 and then reject every token, usually with a confusing signature error.
Response type
code only. If your connector offers id_token or code id_token, set it back to code.
PKCE
S256 only; plain is not offered. Required for open clients. Optional for registered ones, but checked whenever you send it.
Refresh tokens
None. The only grant type is authorization_code, and there is no offline_access scope. The id_token and access_token both last ten minutes.
Client authentication
client_secret_basic or client_secret_post, or none for an open client. private_key_jwt is not offered. Each application accepts one method (client_secret_basic unless you pick the other); /token refuses the other with invalid_client. You can see and change it in the console under Settings.

There is no sign-out endpoint, so signing out only ends your own session. With no refresh tokens, renewing a session means a full new sign-in in the agent’s browser. Give your own session a lifetime that fits.

Watch out. Nothing tells you when an agent is revoked: no back-channel logout, no webhook. An id_token stays valid until exp. Within the access token’s ten minutes, /userinfo answers invalid_token once access is gone. After that, the next sign-in is the signal.

The sign-in is not instant#

There is no password form. The browser lands on an AgentID waiting page and continues once it proves it holds an AgentID session for the inbox. If it has none, the page shows the agent one command to run. The inbox owner can also sign in through AgentMail instead. More in AgentID browser enrollment.

Your callback isn’t called until the wait is over. Until then, keep the state value and the PKCE code verifier you saved before the redirect. The sign-in can take up to five minutes, and the code then lasts sixty seconds, so keep both for at least ten minutes. Some in-memory or short-lived stores drop them sooner.

You mostly wait on the first sign-in. After an inbox approves your app, the approval is remembered for 180 days from the last sign-in, and later sign-ins from that browser continue on their own. A changed redirect URI or scope list asks again.

Watch out. If state or the verifier expires during the wait, the callback fails with a mismatch that looks like an attack, not a timeout. Test this before you ship.

Mapping claims onto your user#

Store sub as the user’s key. It comes from the inbox, so the same inbox always gets the same sub, in every app. An agent with two inboxes is two users.

email always comes with email_verified: true, and owner_email with owner_email_verified: true. name comes with the profile scope, but only if the inbox has a display name, so don’t require it.

Check that aud is your client_id: the URL for an open client, or the Client ID from the console. If you wrote the client yourself, also check the iss in the callback against the issuer, next to your state check.

Watch out. Owner claims (owner_name, owner_email, owner_email_verified) are in the id_token and in /userinfo. Many libraries keep only the standard claims and drop these, so map them yourself or read /userinfo.

Applications you create (console or CLI) also get scope: what the sign-in actually granted. Read it instead of assuming you got everything you asked for. Every claim is in the claims reference.

Checking it works#

Before the first sign-in, check that discovery and the signing keys load. 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. You should get an id_token whose aud is your client_id. Sign in again from the same inbox and you should get the same sub. If you asked for owner scopes, look for owner_name or owner_email in the id_token or in /userinfo.

Watch out. Owner scopes can stall even when your setup is correct. The agent’s AgentMail API key needs the App: Share Owner permission. Without it, the agent can’t finish the sign-in alone, and the organization owner has to approve it from their AgentMail account. You never get a token with the owner claims quietly missing.

On Clerk, Supabase, Auth0 or Better Auth? Each has its own step-by-step guide: provider guides.

On Better Auth? Add AgentID to Better Auth configures this provider through the Generic OAuth plugin with an AgentMail-maintained helper.

The requests themselves — the authorization redirect and the token exchange, parameter by parameter — are under open clients, with endpoints, scopes and token claims on the reference.