Integrate

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.

Agent verified flow
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

  1. Enable agents to exchange their session for an audience-specific ID-JAG on user consent.
  2. Host JWKS (and optionally a CIMD) so downstream services can verify the assertions you sign.
  3. Direct agents to introspect the agent_auth block of the consuming service's .well-known/oauth-authorization-server.
  4. 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:

  1. 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 a WWW-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"]
    }
  2. Authorization Server metadata at <authorization_servers[0]>/.well-known/oauth-authorization-server — this is where the agent_auth block lives. The agent reads authorization_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, and grant_types_supported are standard RFC 8414 / RFC 7009 / RFC 7523 fields. The agent_auth block 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:

json
{
  "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.

json
{
  "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:

http
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:

json
{
  "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:

http
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:

json
{
  "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.

json
{ "error": "invalid_audience", "message": "..." }
EndpointError codeMeaning
/agent/identityinvalid_issuerToken iss isn't in the service's trusted providers list.
/agent/identityinvalid_signatureJWKS lookup failed or the signature didn't verify against any known key.
/agent/identityexpiredexp is in the past.
/agent/identityreplay_detectedjti has already been seen within the replay window.
/agent/identityinvalid_audienceaud doesn't match the service's auth server.
/agent/identityinvalid_client_idclient_id doesn't resolve to a known provider identity.
/agent/identitymissing_verified_emailNeither email_verified nor phone_number_verified is true.
/agent/identityinteraction_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/identitylogin_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/tokeninvalid_grantAssertion failed verification, expired, replayed, audience-mismatched, or revoked.
/oauth2/tokeninvalid_clientclient_id doesn't resolve to a known provider identity.
/oauth2/tokenunsupported_grant_typegrant_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:

  1. Existing delegation(iss, sub) is already bound to a user. Clean match; return a service-signed identity_assertion immediately.
  2. 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.
  3. Step-up required(iss, sub) is new but the verified email/phone matches an existing user. The service won't silently bind; it returns 401 interaction_required with 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):

http
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.