For agent providers
Trusted agent providers — companies whose agents act on behalf of users (OpenAI, Anthropic, Cursor, and similar) — power the agent verified flow by asserting a user's identity to a downstream service. The artifact is an Identity Assertion JWT Authorization Grant (ID-JAG). Signing ID-JAGs makes your surface the identity broker for every service a user's agent touches — you keep the consent prompt, the revocation UX, and the delegation audit trail inside your product instead of leaking them to wherever the user would otherwise paste an API key.
auth.md also defines a user claimed flow, which is between an app and its user via a confirmation ceremony and does not require agent provider participation. This guide covers only the agent verified flow.
Agent verified flow sequence
The agent discovers the service's auth posture via its Protected Resource
Metadata (PRM), asks the user for consent, requests an audience-specific
ID-JAG from you, and exchanges it at the service's /agent/identity
endpoint for a service-signed identity_assertion — which it then trades
at /oauth2/token for an access_token.
sequenceDiagram
actor User
participant Agent
participant Provider as Agent Provider
participant Service
Agent->>Service: GET /api/resource
Service-->>Agent: 401 Unauthorized<br/>WWW-Authenticate: Bearer resource_metadata="..."
Agent->>Service: GET /.well-known/oauth-protected-resource
Service-->>Agent: 200 OK (PRM with authorization_servers)
Agent->>Service: GET /.well-known/oauth-authorization-server
Service-->>Agent: 200 OK (AS metadata with agent_auth block)
Agent->>User: Consent to assert identity to audience?
User-->>Agent: Consent granted
Agent->>Provider: Request audience-specific ID-JAG
Provider-->>Agent: 200 OK (ID-JAG)
Agent->>Service: POST /agent/identity<br/>{ type: identity_assertion, assertion: ID-JAG }
Service->>Provider: GET /.well-known/jwks.json
Provider-->>Service: 200 OK (JSON Web Key Set)
Service->>Service: Verify signature + claims, match user
Service-->>Agent: 200 OK (identity_assertion)
Agent->>Service: POST /oauth2/token<br/>grant_type=jwt-bearer&assertion=...
Service-->>Agent: 200 OK (access_token)Minimum implementation
- Enable agents to exchange their session for an audience-specific ID-JAG on user consent.
- Host JWKS (and optionally a CIMD) so downstream services can verify the assertions you sign.
- Direct agents to introspect the
agent_authblock of the consuming service's.well-known/oauth-authorization-server. - Accept revocation calls from the user's control plane and POST a
Security Event Token to the service's
events_endpoint.
Discovering agent auth
Agents reach the agent registration pathway through service documentation,
SDKs, self-documenting APIs, and an
RFC 9728 (OAuth 2.0 Protected Resource Metadata)
enrichment layer. Services may also publish an auth.md document
containing a breadcrumb to the protected resource document.
Discovery is two-hop:
-
Protected Resource Metadata (PRM) at
.well-known/oauth-protected-resource(per RFC 9728) — the resource server advertises its authorization servers. On any 401, the resource server includes aWWW-Authenticate: Bearer resource_metadata="..."header pointing here:json { "resource": "https://api.service.example.com/", "resource_name": "Service", "resource_logo_uri": "https://service.example.com/logo.png", "authorization_servers": ["https://auth.service.example.com/"], "scopes_supported": ["api.read", "api.write"], "bearer_methods_supported": ["header"] } -
Authorization Server metadata at
<authorization_servers[0]>/.well-known/oauth-authorization-server— this is where theagent_authblock lives. The agent readsauthorization_servers[0]from the PRM and fetches:json { "resource": "https://api.service.example.com/", "authorization_servers": ["https://auth.service.example.com/"], "scopes_supported": ["api.read", "api.write"], "bearer_methods_supported": ["header"], "issuer": "https://auth.service.example.com", "token_endpoint": "https://auth.service.example.com/oauth2/token", "revocation_endpoint": "https://auth.service.example.com/oauth2/revoke", "grant_types_supported": [ "urn:ietf:params:oauth:grant-type:jwt-bearer", "urn:workos:agent-auth:grant-type:claim" ], "agent_auth": { "skill": "https://service.example.com/auth.md", "identity_endpoint": "https://auth.service.example.com/agent/identity", "claim_endpoint": "https://auth.service.example.com/agent/identity/claim", "events_endpoint": "https://auth.service.example.com/agent/event/notify", "identity_types_supported": ["anonymous", "identity_assertion", "service_auth"], "identity_assertion": { "assertion_types_supported": [ "urn:ietf:params:oauth:token-type:id-jag" ] }, "events_supported": [ "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked" ] } }The top-level
token_endpoint,revocation_endpoint, andgrant_types_supportedare standard RFC 8414 / RFC 7009 / RFC 7523 fields. Theagent_authblock carries the profile-specific bootstrap, claim, and SET-receive endpoints.
Minting the ID-JAG
On consent, mint a short-lived JWT signed with a key published in your JWKS. The header carries the ID-JAG content type marker; the payload carries the audience-scoped claims:
{
"typ": "oauth-id-jag+jwt",
"alg": "ES256", // or RS256, etc.
"kid": "<provider key id>"
}
.
{
// required
"iss": "https://api.agent-provider.example.com",
"sub": "<opaque user identifier>",
"aud": "https://auth.service.example.com",
"client_id": "<iss or CIMD URL>",
"jti": "<unique identifier for the token to prevent replay>",
"iat": <issuance epoch seconds>,
"exp": <iat + 5m>,
"auth_time": <epoch seconds the user last authenticated at your provider>,
"email": "user@example.com",
"email_verified": true,
// optional
"amr": ["mfa"],
"name": "Jane Smith",
"phone_number": "+15553805188",
"phone_number_verified": false,
"resource": "https://api.service.example.com",
// optional agent metadata
"agent_platform": "<your-agent-surface>",
"agent_context_id": "<chat-id>"
}At minimum, the consumer needs iss, sub, aud, client_id, jti,
iat, exp, auth_time, and at least one verified contact
(email_verified or phone_number_verified) to match or provision a user.
auth_time is required, not optional. Consuming services enforce a max age
on it — typically one hour — and reject older ID-JAGs with
401 login_required, expecting the agent to refresh the user's session at
your provider (prompt=login or equivalent) before minting a new one.
Freshen auth_time whenever the user re-authenticates; don't reuse a
long-lived session timestamp.
Hosted discovery documents
Publish your
JSON Web Key Set (JWKS)
— conventionally at .well-known/jwks.json — so consuming services can
verify your ID-JAG signatures.
Optional: Client ID Metadata Document (CIMD)
Host an
OAuth Client ID Metadata Document
and use its URL as the client_id value in the ID-JAG. This decouples
your provider identity from your signing keys — you can rotate JWKS
without churning every consumer's trust list — and makes it convenient
for trusted agent registries to list providers. Adopt this if you expect
signing-key rotation or registry listing to matter; otherwise the
client_id can be your issuer URL.
{
"client_id": "https://api.agent-provider.example.com/agent-auth.json",
"client_name": "Agent Provider",
"logo_uri": "https://agent-provider.example.com/logo.png",
"client_uri": "https://agent-provider.example.com",
"tos_uri": "https://agent-provider.example.com/tos",
"policy_uri": "https://agent-provider.example.com/privacy",
"token_endpoint_auth_method": "private_key_jwt",
"jwks_uri": "https://agent-provider.example.com/.well-known/jwks.json",
"scope": "openid email profile"
}Acquiring credentials
Exchanging an ID-JAG for an access_token is a two-step dance: the service
first issues its own service-signed identity_assertion bound to the
registration, then the agent exchanges that assertion at the OAuth token
endpoint.
Step 1 — register the identity. Submit the provider ID-JAG to the
service's identity_endpoint:
POST /agent/identity HTTP/1.1
Host: auth.service.example.com
Content-Type: application/json
{
"type": "identity_assertion",
"assertion_type": "urn:ietf:params:oauth:token-type:id-jag",
"assertion": "eyJhbGc..."
}200 response — the service verified the ID-JAG, found or JIT-provisioned
the user, and minted a service-signed identity_assertion:
{
"registration_id": "reg_...",
"registration_type": "identity_assertion",
"identity_assertion": "<service-signed JWT>",
"assertion_expires": "2026-05-04T13:00:00.000Z",
"scopes": ["api.read", "api.write"]
}Step 2 — exchange the assertion at /oauth2/token. Standard
RFC 7523 JWT-bearer grant:
POST /oauth2/token HTTP/1.1
Host: auth.service.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<service-signed identity_assertion>
&resource=https://api.service.example.com/200 response is a standard OAuth token envelope:
{
"access_token": "<token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api.read api.write"
}If the access_token expires, the agent re-calls /oauth2/token with the
same assertion. If the assertion itself is expired or revoked,
/oauth2/token returns invalid_grant and the agent re-calls
/agent/identity to mint a fresh one. The flow does not issue OAuth
refresh_tokens — the two-step pattern replaces refresh.
Errors
Errors at /agent/identity describe profile-specific states; errors at
/oauth2/token follow OAuth-standard vocabulary.
{ "error": "invalid_audience", "message": "..." }| Endpoint | Error code | Meaning |
|---|---|---|
/agent/identity | invalid_issuer | Token iss isn't in the service's trusted providers list. |
/agent/identity | invalid_signature | JWKS lookup failed or the signature didn't verify against any known key. |
/agent/identity | expired | exp is in the past. |
/agent/identity | replay_detected | jti has already been seen within the replay window. |
/agent/identity | invalid_audience | aud doesn't match the service's auth server. |
/agent/identity | invalid_client_id | client_id doesn't resolve to a known provider identity. |
/agent/identity | missing_verified_email | Neither email_verified nor phone_number_verified is true. |
/agent/identity | interaction_required (401) | Step-up: the service knows the user by email/phone but needs them to confirm linking your (iss, sub). Surface the claim block to the user. |
/agent/identity | login_required (401) | auth_time missing or older than the service's max_age. Re-authenticate the user at your end and mint a fresh ID-JAG. |
/oauth2/token | invalid_grant | Assertion failed verification, expired, replayed, audience-mismatched, or revoked. |
/oauth2/token | invalid_client | client_id doesn't resolve to a known provider identity. |
/oauth2/token | unsupported_grant_type | grant_type is not urn:ietf:params:oauth:grant-type:jwt-bearer. |
Downstream verification
Services maintain a list of trusted agent providers. On first contact with
a (iss, sub) pair the service has three resolutions:
- Existing delegation —
(iss, sub)is already bound to a user. Clean match; return a service-signedidentity_assertionimmediately. - JIT-provisioned — no existing user matches the ID-JAG's verified email/phone. The service creates a new user and binds the delegation. Clean match.
- Step-up required —
(iss, sub)is new but the verified email/phone matches an existing user. The service won't silently bind; it returns401 interaction_requiredwith a claim ceremony so the user confirms the link.
Services reject ID-JAGs with neither a verified email nor a verified phone
number, and reject ID-JAGs whose auth_time is older than the service's
max_age (typically 1 hour) with 401 login_required — agents must
refresh the user's authentication at the provider before retrying.
Tracking and revocation
Track the services to which identity assertions have been delegated so the
user can revoke the delegation from a control plane in your product. The
discovery document's agent_auth.events_endpoint is where you transmit
identity-event SETs to the service — the canonical
RFC 8935 push delivery of
a Security Event Token (RFC 8417):
POST /agent/event/notify HTTP/1.1
Host: auth.service.example.com
Content-Type: application/secevent+jwt
{
"typ": "secevent+jwt",
"alg": "ES256", // or RS256, etc.
"kid": "<provider key id>"
}
.
{
"iss": "https://api.agent-provider.example.com",
"sub": "<opaque user identifier>",
"aud": "https://auth.service.example.com",
"jti": "<unique identifier to prevent replay>",
"iat": <epoch seconds>,
"events": {
"https://schemas.workos.com/events/agent/auth/identity/assertion/revoked": {}
}
}The receiving service validates the SET against your JWKS, dispatches on
the events claim, and invalidates the identity assertion (and the
credentials derived from it). A successful receive returns 202 Accepted;
failures return 400 with { "err": "<code>", "description": "..." }.
This events_endpoint is distinct from the top-level revocation_endpoint:
the latter (RFC 7009) is
for the agent or an admin to kill a single bearer credential by value; the
former is for you to notify the service of an upstream identity event — a
broader signal that invalidates the registration itself. Expect this
surface to extend with richer
SET /
CAEP / RISC events,
layered on this same push channel.