DPDP Act 2023India’s data-protection law takes effect 13 May 2027.Read the Act
NamoID
All posts
NamoID Blog

CIMD vs Dynamic Client Registration: Onboarding AI Agents to OAuth

You're running an OAuth authorization server, and an AI agent nobody pre-registered shows up wanting a token. In the "Sign in with Google" world this never happens — every client is registered by hand, once, in a developer console. But the agent ecosystem doesn't work that way: there are thousands of MCP clients, they update constantly, and no human is going to file a registration ticket for each one. So how does an agent you've never seen get a client_id?

There are two answers, and the MCP authorization spec recently switched which one it recommends. The old answer is Dynamic Client Registration (RFC 7591) — you run a /register endpoint and hand out client IDs on demand. The new answer is Client ID Metadata Documents (CIMD) — the agent's client_id is just an HTTPS URL you can dereference. The difference looks small and isn't: one makes you run a stateful registration service with an abuse problem, the other makes the client_id self-describing and lets you skip the endpoint entirely. Here's the comparison, and how to support CIMD without opening an SSRF hole.

The problem: registration doesn't scale to agents

A client_id is how an authorization server recognizes the application asking for a token. Traditionally it's minted once: a developer registers their app, gets an ID, and reuses it forever. That model assumes a small, known set of clients onboarded by humans.

Agents break both assumptions. The set is large and open-ended, and no human is in the loop. You need a way for a client to become known at first contact, automatically, without you trusting it more than a public client deserves.

Option 1 — Dynamic Client Registration (RFC 7591)

RFC 7591 defines a /register endpoint. A client POSTs its metadata — name, redirect URIs, grant types — and gets back a freshly minted client_id (and optionally a secret):

POST /register HTTP/1.1
Content-Type: application/json
 
{
  "client_name": "Example Agent",
  "redirect_uris": ["https://agent.example.com/callback"],
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "none"
}
HTTP/1.1 201 Created
 
{ "client_id": "idpc_live_9fK2…", "redirect_uris": ["https://agent.example.com/callback"] }

It works, but look at what you now own. The endpoint writes a row to your database for every caller — and the caller is unauthenticated by definition (the whole point is onboarding strangers). That's an open write endpoint, which means:

  • Abuse surface. Anyone can spray registrations; you need rate limits, quotas, and probably gating, or your client table fills with junk.
  • State you have to manage. Every agent that ever connected leaves a record. Duplicates, staleness, and "which of these 40 registrations is the real one?" become your problem.
  • No live source of truth. Once registered, the metadata is a snapshot. If the agent changes its redirect URI, it re-registers and you have two records.

DCR isn't wrong — it's just a registration service, with all the operational weight that implies.

Option 2 — Client ID Metadata Documents (CIMD)

CIMD (draft-ietf-oauth-client-id-metadata-document) makes one move: the client_id is an HTTPS URL, and that URL serves the client's metadata. There's no registration call. The agent just uses its URL as the client_id in a normal authorization request:

GET /authorize?response_type=code
  &client_id=https://agent.example.com/client.json
  &redirect_uri=https://agent.example.com/callback
  &code_challenge=…&code_challenge_method=S256

The first time your server sees that client_id, it fetches the document:

// GET https://agent.example.com/client.json
{
  "client_id": "https://agent.example.com/client.json",
  "client_name": "Example Agent",
  "redirect_uris": ["https://agent.example.com/callback"]
}

The client_id inside the document must equal the URL it was fetched from — that self-reference is the trust anchor. HTTPS proves the document came from whoever controls that origin; the matching client_id proves the document is claiming itself, not impersonating another client. No secret is issued (CIMD clients are public clients, so PKCE does the authenticating). The agent's metadata lives at its own URL, so it's always current — change the redirect URI in the document and the next fetch sees it.

Head to head

Dynamic Client RegistrationCIMD
OnboardingPOST to /register firstuse the URL as client_id directly
Endpoint to runyes (/register, write path)none
Per-agent DB rowsone per registrationoptional (JIT cache only)
Abuse surfaceopen write endpointa guarded GET fetch
Source of truthyour snapshotthe client's live URL
Client secretoptionalnone (public + PKCE)
New risk to manageregistration spamSSRF on the fetch

The trade isn't "more secure vs less" — it's what kind of work you take on. DCR gives you a stateful service to defend; CIMD gives you a single outbound fetch to defend.

Why MCP moved to CIMD

The 2025-11-25 MCP authorization revision made DCR optional and pointed implementations at CIMD instead. The reasoning maps exactly to the table: in an ecosystem of thousands of agents, making every authorization server run an abuse-prone registration endpoint was the wrong default. A dereferenceable client_id lets any agent present a verifiable identity with zero pre-provisioning, and lets the authorization server stay stateless about who it has "met" before. For agent identity — open-ended, machine-driven, constantly changing — that's the better fit.

Supporting CIMD without opening an SSRF hole

The one new risk CIMD introduces is real: your server now makes an outbound HTTP request to a URL an attacker controls. Done naively, that's a server-side request forgery primitive — point the client_id at http://169.254.169.254/… and your server fetches cloud metadata. So the fetch must be guarded. The checks that matter:

  • Scheme + host allow/deny. HTTPS only. Resolve the host and refuse private, loopback, and link-local ranges (RFC 1918, 127.0.0.0/8, 169.254.0.0/16, ::1). This is the load-bearing check.
  • No redirects. Don't follow them — a public URL that 302s to http://169.254.169.254 defeats a naive allowlist. Fetch exactly the URL or fail.
  • Size + time caps. Read at most a few KB with a short timeout; a metadata document is tiny, and an unbounded read is a DoS.
  • Exact client_id match. Reject the document unless its client_id equals the URL you fetched.
  • Register as a public client. No secret; require PKCE. Cache the result so you fetch once, not on every request.

That's how NamoID resolves CIMD client_id URLs — an SSRF-guarded, no-redirect, size-capped fetch, an exact URL match, then a just-in-time public-client registration scoped to the environment that authorized it. The agent shows up with a URL; we verify it and move on. (For the resource-server side of the same flow, see securing an MCP server.)

When DCR still makes sense

CIMD isn't a universal replacement. DCR is the right tool when a client genuinely needs a confidential registration (a real secret, server-to-server), when you want to issue and control the credential rather than dereference the client's, or when you're integrating with an existing ecosystem that already speaks RFC 7591. For the agent case — public clients, open set, no human onboarding — CIMD wins. Many authorization servers will end up supporting both.


The shift from DCR to CIMD is small in spec terms and large in operational terms: it turns "run a registration service and defend it" into "dereference a URL and verify it." If you're building an authorization server for the agent era, how MCP authorization works covers the full flow, and NamoID ships CIMD resolution plus audience-bound tokens so the agents your customers have never seen can still onboard safely.