OAuth originally assumed clients would be pre-registered at an authorization server.
Before an app can talk to an OAuth server, a developer signs up for an account, registers the client by providing the name and logo and other client information, configures redirect URIs, and gets a client_id. The server has some record of who this client is and who is responsible for it.
That works fine when the ecosystem is closed. Google can require developers to register before accessing their API. Salesforce can do the same. But what about ecosystems where any client should be able to talk to any server, where it's not possible for the client developer to be aware of every server ahead of time?
This is the "open web" problem. Mastodon users expect any Mastodon client to work with any Mastodon server. BlueSky works the same way. The MCP ecosystem is heading in the same direction, users expect to be able to connect their own MCP client to any MCP server. When you have potentially thousands of clients and thousands of servers, you can't require every client developer to register with every server operator in advance.
Dynamic Client Registration (DCR) was designed to solve this. A client shows up at a server, registers itself on the spot, and gets credentials. No prior relationship required.
The problem is DCR pushes all the trust decisions onto the authorization server, with nothing to actually base those decisions on, and no real link to the client developer.
- How Dynamic Client Registration Works
- The Problems with DCR
- The Root of the Problem
- How Client ID Metadata Document Works
- What CIMD Makes Possible
- What CIMD Does Not Solve
- Where This Leaves Us
How Dynamic Client Registration Works
DCR is defined in RFC 7591. The client sends a POST request to the server's registration endpoint with its metadata: a display name, logo URL, redirect URIs, contact information. The server responds with a client_id and optionally a client_secret. From that point on, the client uses those credentials in OAuth flows with that server.
sequenceDiagram
participant C as Client
participant AS as Authorization Server
C->>AS: POST /register<br/>(name, logo, redirect_uris, ...)
Note over C,AS: Unauthenticated — no credentials required
AS->>C: 201 Created<br/>(client_id, client_secret)
This works, at least in the sense that it solves the bootstrapping problem. The client can show up without any prior arrangement and get credentials.
But there is a deeper problem that this flow makes hard to see.
The Problems with DCR
Anyone Can Register Anything
The registration endpoint must be open to the world by design. That is the whole point of dynamic registration in an open ecosystem. Any actor, whether that's a legitimate app, a bot, or an attacker, can call it and create a client registration.
graph LR
A[Web App\nname: Acme\nlogo: acme.com/logo.png]
B[Desktop App\nname: Acme\nlogo: acme.com/logo.png]
C[Attacker\nname: Acme\nlogo: acme.com/logo.png]
A -->|POST /register| R[/Register Endpoint/]
B -->|POST /register| R
C -->|POST /register| R
R --> D[client_id: aaa]
R --> E[client_id: bbb]
R --> F[client_id: ccc]
style C fill:#ffdddd
style F fill:#ffdddd
The metadata in the request is entirely self-asserted. The server has no way to verify that the entity calling /register controls the logo URL it submitted, runs the website it claims to represent, or is in any way connected to the app name it provided. The authorization server is simply asked to accept claims it cannot check. The only clue as to the real identity of this client is the redirect_uri, which is only a partial solution as we'll discuss shortly.
The Client Lifecycle Problem
Once a client registers, the authorization server is responsible for managing that registration indefinitely. This creates an operational problem that has no clean solution.
There are a few approaches servers take to clean up stale registrations:
Delete if unused within N hours. This seems reasonable until you realize it breaks clients that register in advance of a user session, or clients used infrequently. It also does nothing for malicious registrations that were used once.
Delete when the last refresh token expires. This is a cleaner signal, but it still requires keeping a record of every client until its tokens expire. For servers with high legitimate usage, this table grows continuously.
Leave it to the client to re-register. The problem here is clients have no reliable way to know whether their registration is still valid before sending a user through an OAuth flow. The client doesn't even discover the registration is gone when the flow fails, because this failure mode ends with the user on the authorization server screen, never being sent back to the client. To avoid dead ends, clients tend to re-register on every login. This compounds the very bloat you were trying to avoid.
| client_id | created | last used |
|---|---|---|
| aaa1 | 6 months ago | unknown |
| bbb2 | 3 months ago | unknown |
| ccc3 | 3 months ago | today |
| ddd4 | 1 month ago | unknown |
| eee5 | today | today |
| ... 10,000 more |
The authorization server is stuck guessing which records are safe to delete, while new ones keep arriving.
Client Impersonation Is Undetectable
The most serious problem with DCR is not the operational overhead. It is that impersonation is structurally impossible to detect.
Nothing in DCR prevents an attacker from registering a client with the same name, logo, and description as a legitimate app. Both will have a client_id. Both will show users the same consent screen. The authorization server has no mechanism to distinguish them.
graph LR
subgraph Legitimate App
L[client_id: abc123\nname: Acme Wallet\nlogo: acme.com/logo.png]
end
subgraph Malicious App
M[client_id: xyz789\nname: Acme Wallet\nlogo: acme.com/logo.png]
end
L --> U1[User sees:\n'Acme Wallet wants access']
M --> U2[User sees:\n'Acme Wallet wants access']
style M fill:#ffdddd
style U2 fill:#ffdddd
This presents a real OAuth phishing risk. A fake app can present a consent screen that looks identical to a legitimate service. If the user authorizes it, from the server's side, nothing looks wrong.
There are defenses against this, but they all require clients to opt into something extra: signed software statements, app attestation, platform-issued certificates. That pushes a significant burden onto every legitimate developer, and leaves the protection entirely voluntary.
Credential Sprawl for Clients
From the client developer's perspective, DCR introduces a class of credential that OAuth was supposed to eliminate: per-server identities that have to be managed, stored, and refreshed.
For each authorization server the client works with, it now needs to:
- Call
/registerto receive aclient_idandclient_secret - Store those credentials securely, separately from any tokens
- Handle rotation and expiration of those credentials
- Decide whether to re-register when something changes
There is also no standard mechanism for a client to verify its client_id is still valid before starting a flow. When the authorization server is about to present an OAuth consent screen, it realizes the client_id doesn't exist and ends the flow there, not sending the user back to the invalid client. The user sees a generic error screen, and the client doesn't even know this happened.
The Root of the Problem
All four of these issues trace back to the same structural flaw: DCR separates the assertion of identity from any authority over that identity.
The authorization server accepts claims about who the client is, but has no external signal to verify those claims against. The client says "I am Acme App" and the server has nothing to cross-reference that against.
Compare this to what we do for humans. When a user logs in, the server asks them to prove something: a password, a passkey, an OTP. The claim "I am Alice" is backed by something. DCR never asks the client for anything comparable.
The question is: what does a client actually control in the real world? A web app controls its domain. A mobile app has a backend, or an app store identity. These are real anchors. The question is whether the protocol uses them.
Client ID Metadata Document (CIMD) is built around that insight. The client_id is a URL. The authority comes from who controls that URL.
How Client ID Metadata Document Works
With CIMD, there is no registration step. The client's identifier is a URL on a domain the client controls. When an authorization server encounters a client_id it has not seen before, it fetches that URL to discover the client's metadata.
sequenceDiagram
participant App as Client App
participant Browser as Browser
participant AS as Authorization Server
participant Meta as app.example.com
App->>Browser: Redirect to AS<br/>client_id=https://app.example.com/client
Browser->>AS: GET /authorize?client_id=https://app.example.com/client&...
AS->>Meta: GET https://app.example.com/client
Note over AS,Meta: Back-channel fetch from client-controlled domain
Meta->>AS: Returns JSON metadata document
AS->>Browser: Show consent screen using fetched metadata
Browser->>AS: User approves
AS->>Browser: Redirect to redirect_uri with auth code
Browser->>App: Delivers auth code
App->>AS: POST /token
AS->>App: Access token
The client publishes its own display name, logo, redirect URIs, supported authentication methods, and JWKS. The AS discovers this at runtime. Nothing needs to happen in advance.
This one change, making the client_id a URL on a client-controlled domain, solves most of the problems described above.
What CIMD Makes Possible
Domain Ownership as a Trust Signal
When the AS fetches the client metadata, it knows what domain it fetched it from. That domain is something it can actually start making decisions about. This opens up trust tiers that were structurally impossible with DCR:
graph TD
subgraph Authorization Server Policy
A{Client domain\nseen before?}
A -->|No, first time| B[Show extra confirmation\nto user]
A -->|Yes, pre-approved| C[Proceed normally]
A -->|Flagged/blocked| D[Deny request]
end
B --> E[User approves]
E --> C
An AS can prompt users for extra confirmation when a client from a newly seen domain requests access — similar to how browsers warn about unfamiliar download sources. Once enough users have authorized clients at a domain and nothing suspicious has come up, the AS can gradually reduce the friction for that domain. It can integrate domain reputation services. It can maintain an explicit allowlist of verified domains for frictionless access, and block suspicious ones.
None of this requires the client to behave differently based on which AS it is talking to. A client that has been pre-enrolled by an enterprise admin and a client talking to the same AS for the first time present their client_id URL identically. The domain is implicit in the URL, and the AS decides what to do with it.
Enterprise Pre-Registration Without Client Changes
Enterprise admins need control over which apps can access company resources. With DCR, this is nearly impossible: the client_id is generated dynamically at registration time, so the admin has no way to reference it before the employee runs the app.
With CIMD, the admin pre-registers the client_id URL (for example, https://myapp.example.com/oauth/client) in the AS. When an employee runs the app, the app presents its client_id URL as it always does. The AS fetches the metadata, finds the URL is already registered by the admin, and proceeds with the enterprise-approved experience.
sequenceDiagram
participant User
participant App as Client App
participant AS as Authorization Server
participant Admin
Admin->>AS: Pre-register client URL<br>https://myapp.example.com/oauth/client
Note over Admin,AS: Setup happens once, in advance
User->>App: Starts OAuth flow
App->>AS: client_id=https://myapp.example.com/oauth/client
AS->>App: Fetch metadata from URL
App->>AS: Returns metadata
Note over AS: URL matches pre-registered entry
AS->>User: Proceeds with approved experience
The client doesn't know or care whether it is being used in an enterprise context. Its client_id URL is the same everywhere. The enterprise filtering happens entirely at the AS.
Clients Control Their Own Keys
Because the client publishes a JWKS (or a JWKS URI) in its metadata document, it can rotate keys without coordinating with the authorization server. The AS fetches fresh metadata when the cache expires and picks up the new keys automatically. This makes private_key_jwt client authentication practical for any client with a web presence.
Authorization servers that want to enforce strong client authentication can validate the signatures. Servers that do not have that requirement can ignore the signature and proceed with whatever they accept. The client publishes good metadata and lets each AS enforce what it needs.
Mobile Apps and Attestation
Mobile apps have always been a challenging case for client identity. The app binary has no inherent web identity, and DCR gives it none.
With CIMD, a mobile app can follow the pattern in OAuth Attestation-Based Client Authentication: the app's backend (an "attester backend") hosts the CIMD document and manages the client's keys. The AS fetches the CIMD URL, which points to the attester backend, and can perform attestation checks against the key material there.
sequenceDiagram
participant App as Mobile App
participant AB as Attester Backend
participant AS as Authorization Server
Note over AB: Publishes CIMD at<br>https://attester.example.com/client
Note over AB: Manages JWKS and<br>attestation material
App->>AS: client_id=https://attester.example.com/client
AS->>AB: Fetch CIMD document
AB->>AS: Returns metadata + JWKS URI
AS->>AB: Fetch JWKS
AB->>AS: Returns public keys
Note over AS: Can now verify app-signed assertions<br>using attester-managed keys
Mobile platforms also give apps a way to "claim" an https redirect URL, linking the app binary to a domain the developer controls. This connects the redirect URL and the CIMD URL to the same domain end to end, giving the AS another corroborating signal.
One thing worth noting for readers familiar with DCR: the spec does define a software_statement property that was intended to solve a similar problem. In practice it was left underspecified — the DCR spec itself says nothing about how to create one, what it should contain, or how keys should be managed. Any ecosystem trying to use it would need to define all of that separately, and then convince every mobile app developer and every AS to adopt the new behavior. CIMD combined with Attestation-Based Client Authentication layers on top of the existing jwks_uri mechanism, which means it composes with what implementations already support rather than requiring a new convention from scratch.
This gives the AS a meaningful level of confidence in the mobile app's identity.
Comparison
| Dynamic Client Registration | Client ID Metadata Document | |
|---|---|---|
| Registration step | Required (unauthenticated POST) | None |
| Authority anchor | None (self-asserted) | Domain ownership |
| Client impersonation | Undetectable | Domain-keyed, harder to fake |
| Client lifecycle management | AS must manage cleanup | Client controls its own document |
| Key rotation | Requires AS coordination | Client-controlled, AS fetches on use |
| Enterprise pre-approval | Out-of-band coordination required | Admin registers URL; client behavior unchanged |
| Mobile attestation | Requires special-casing | Natural fit via attester backend |
| Per-AS credential to store | client_id + secret | None |
What CIMD Does Not Solve
CIMD is not a complete solution to the open ecosystem trust problem. A few things are worth calling out, either as known limitations or as possible future work.
Domain spoofing at the visual layer is still possible. An attacker pretending to be acme.com can register acme-login.com and host a convincing CIMD document there. Domain reputation services help, but do not eliminate this. The improvement over DCR is that there is now a domain to leverage in any decisions, and domain-based signals are much richer than nothing.
CIMD only helps as much as the AS acts on it. A server that accepts any CIMD URL without applying any domain-based policy gets roughly the same trust posture as DCR on the impersonation dimension, though the client lifecycle and credential sprawl problems are still improved.
For machine-to-machine clients without an attester backend, CIMD without private_key_jwt or mTLS is still self-asserted metadata, just fetched from a URL rather than submitted via POST. Strong client authentication still requires key material.
Desktop Apps
Desktop apps are the hardest case. Mobile platforms provide attestation APIs and let apps claim https redirect URLs. Desktop platforms currently do not. A desktop app cannot cleanly connect its running instance to a domain the developer controls, and localhost redirect URLs (which desktop apps are forced to use) can be intercepted by any app on the same machine and provide no protection against app impersonation.
This means client impersonation for desktop apps remains possible even with CIMD. That said, it is no worse than DCR, which also provides no solution here. And adopting CIMD for desktop apps still removes the credential sprawl problem and makes the AS implementation uniform across all client types, rather than requiring special handling for desktop.
Token binding is still available to desktop apps. Specs like DPoP bind access tokens and refresh tokens to client-asserted keys without trying to solve client authentication. A desktop app can leverage DPoP to limit token reuse even when client identity itself cannot be strongly verified.
Where This Leaves Us
DCR solved the bootstrapping problem but could not solve the trust problem. It gave authorization servers a way to accept unknown clients, without giving them any tools to reason about which unknown clients to trust.
CIMD is not a drop-in replacement for DCR in every deployment. But for open ecosystems like MCP, decentralized social, and federated enterprise, it provides the trust hooks that DCR structurally cannot. The domain is a useful anchor. Enterprise pre-enrollment of clients requires no client changes. Key management stays with the client. Mobile app attestation fits naturally.
For AS operators, the path forward is to accept client_id values that are HTTPS URLs, fetch the metadata document on first encounter, and build domain-based trust policies from there. For client developers, the change is even simpler: publish a metadata document at a stable URL on your domain and use that URL as your client_id.
The specs are live and moving through the IETF process. Client ID Metadata Document covers the metadata document format and discovery. Attestation-Based Client Authentication describes the architecture of using an attester backend with mobile apps.
If you are building in this space, both documents are worth reading, and the OAuth working group is actively discussing both. Feel free to chime in on the OAuth mailing list or on the individual GitHub repos for the specs.


and Canada
: Farrar, Straus and Giroux
and Commonwealth excluding Canada (Australia
, New Zealand
, India
, South Africa
, and beyond): Verso Books
: Grupo Editorial Record
: Éditions Eyrolles
: Aufbau
: Agave Konyvek
: Iperborea
: Impress Corporation
: Wydawnictwo Otwarte
: PRH Portugal
: Éditions Québec Amérique
: Mladinska Knjiga
: Next Wave Media
: Capitán Swing
: Acropolis
: Salt Publishing
: Okuyanus
: Athena Publishing































































































