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 Registration | CIMD | |
|---|---|---|
| Onboarding | POST to /register first | use the URL as client_id directly |
| Endpoint to run | yes (/register, write path) | none |
| Per-agent DB rows | one per registration | optional (JIT cache only) |
| Abuse surface | open write endpoint | a guarded GET fetch |
| Source of truth | your snapshot | the client's live URL |
| Client secret | optional | none (public + PKCE) |
| New risk to manage | registration spam | SSRF 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.254defeats 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_idmatch. Reject the document unless itsclient_idequals 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.