Client authentication
A client proves its identity at the back-channel endpoints — /token and /par. Four methods are
supported, advertised in discovery as token_endpoint_auth_methods_supported:
| Method | Credential | For |
|---|---|---|
none | None; PKCE only | SPAs, native apps, MCP clients |
client_secret_basic | Secret in the Authorization header | Server-side apps |
client_secret_post | Secret in the form body | Server-side apps whose stack cannot set the header |
private_key_jwt | A signed JWT assertion | Agents and services that should hold no shared secret |
Assertion signing accepts ES256, EdDSA, and RS256.
Public clients
Section titled “Public clients”A public client has no secret. It presents its client_id, and PKCE is what binds the authorization
code to the client instance that started the flow:
POST /tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=…&client_id=client_7f3a…&code_verifier=…&redirect_uri=…This is the right choice for anything running on a device you do not control. A secret shipped in a browser bundle or a mobile binary is not a secret.
Client secrets
Section titled “Client secrets”Confidential clients authenticate with client_secret_basic (preferred) or client_secret_post:
POST /tokenAuthorization: Basic <base64(client_id:client_secret)>Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=tool:searchSecrets are compared against a stored hash in constant time, and are returned exactly once at
creation. Rotate an agent’s secret with
POST /v1/organizations/:orgId/agents/:clientId/rotate_secret.
A confidential client must authenticate even on the authorization-code and refresh grants. PKCE
alone is not sufficient for a client that has a registered secret — failing to present it returns
401 invalid_client.
private_key_jwt
Section titled “private_key_jwt”The strongest option, and the right default for agents and services: the client registers a JWK Set and authenticates with a JWT it signs itself. No shared secret exists to leak, and nothing long-lived crosses the wire.
Register with the JWKS in place of a secret:
POST /registerContent-Type: application/json
{ "client_name": "Indexing service", "client_type": "agent", "grant_types": ["client_credentials"], "token_endpoint_auth_method": "private_key_jwt", "jwks": { "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "…", "kid": "svc-1" } ] }}Then authenticate by signing an assertion (RFC 7523):
{ "iss": "client_7f3a…", "sub": "client_7f3a…", "aud": "https://acme.oauth.work/token", "jti": "<unique per assertion>", "exp": 1750000060}POST /tokenContent-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=<signed JWT>The assertion is audience-bound to the token endpoint and its jti is single-use, so a captured
assertion cannot be replayed. Pair it with DPoP and neither the client’s credential nor its
issued token is useful to anyone who intercepts them.
Dynamic client registration
Section titled “Dynamic client registration”RFC 7591. POST /register needs no credential, which is what lets an MCP client register itself the
first time a user connects it:
POST /registerContent-Type: application/json
{ "client_name": "Research Agent", "redirect_uris": ["https://agent.example.com/callback"], "grant_types": ["authorization_code", "refresh_token"], "token_endpoint_auth_method": "none", "scope": "openid offline_access"}Because registration is anonymous, it is deliberately constrained:
- Grants. Ordinary clients may register
authorization_codeandrefresh_tokenonly. Clients registering asclient_type: "agent"may additionally requestclient_credentials. - Redirect URIs. Required for the authorization-code grant, and must be https, http loopback, or a safe custom scheme. Anonymous registration of a non-agent client with a custom scheme is refused outright — that shape is a phishing surface.
- Scopes. First-party scopes only. A dynamically registered client cannot request tool scopes; those require registering the agent through the authenticated management API.
- No org binding. Dynamically registered clients are never bound to an organization, because an
org-bound client’s tokens carry
org_idand can mint org-signed credentials. Org binding happens only through an authenticated/v1call.
A client_secret is returned when the requested auth method implies one; like every other secret,
once.
URL client IDs
Section titled “URL client IDs”An MCP or agent client may use an https URL as its client_id. The authorization server fetches
that URL for the client’s metadata rather than requiring registration at all — domain control is the
identity. Advertised in discovery as client_id_metadata_document_supported: true.
Host a document at the URL you intend to use as your client id:
{ "client_id": "https://agent.example.com/oauth-client.json", "client_name": "Research Agent", "redirect_uris": ["https://agent.example.com/callback"], "grant_types": ["authorization_code", "refresh_token"], "token_endpoint_auth_method": "none"}Then use that URL directly:
GET /authorize?client_id=https://agent.example.com/oauth-client.json&…The rules follow from what a URL can and cannot do:
- The document’s
client_idmust equal its own URL. Otherwise one document could claim to be another client. - URL clients are always public. A URL cannot keep a secret, so
token_endpoint_auth_methodmust benoneand PKCE is enforced. authorization_codeandrefresh_tokenonly, first-party scopes only, no org binding — the same posture as anonymous registration.- The document is cached for 5 minutes, so a compromised or corrected document ages out fast.
Choosing
Section titled “Choosing”| Situation | Use |
|---|---|
| SPA or mobile app | none + PKCE |
| Server-side web app | client_secret_basic |
| Agent or service you provision | private_key_jwt, registered via /v1 agents |
| MCP client connecting to many servers | URL client id, or dynamic registration |