57°F

Aaron Parecki

  • Articles
  • Notes
  • Photos
  • Solving the Missing Trust Anchor in Dynamic Client Registration with CIMD

    July 29, 2026

    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
      • Anyone Can Register Anything
      • The Client Lifecycle Problem
      • Client Impersonation Is Undetectable
      • Credential Sprawl for Clients
    • The Root of the Problem
    • How Client ID Metadata Document Works
    • What CIMD Makes Possible
      • Domain Ownership as a Trust Signal
      • Enterprise Pre-Registration Without Client Changes
      • Clients Control Their Own Keys
      • Mobile Apps and Attestation
    • What CIMD Does Not Solve
      • Desktop Apps
    • 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 /register to receive a client_id and client_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.

    Hounslow, England, GBR • 90°F
    Wed, Jul 29, 2026 1:15pm +01:00 #oauth #mcp #cimd #dcr #ietf
Posted in /articles using quill.p3k.io

Hi, I'm Aaron Parecki, Director of Identity Standards at Okta, and co-founder of IndieWebCamp. I maintain oauth.net, write and consult about OAuth, and participate in the OAuth Working Group at the IETF. I also help people learn about video production and livestreaming. (detailed bio)

I've been tracking my location since 2008 and I wrote 100 songs in 100 days. I've spoken at conferences around the world about owning your data, OAuth, quantified self, and explained why R is a vowel. Read more.

  • Director of Identity Standards at Okta
  • IndieWebCamp Founder
  • OAuth WG Editor
  • OpenID Board Member

  • 🎥 YouTube Tutorials and Reviews
  • 🏠 We're building a triplex!
  • ⭐️ Life Stack
  • ⚙️ Home Automation
  • All
  • Articles
  • Bookmarks
  • Notes
  • Photos
  • Replies
  • Reviews
  • Trips
  • Videos
  • Contact
© 1999-2026 by Aaron Parecki. Powered by p3k. This site supports Webmention.
Except where otherwise noted, text content on this site is licensed under a Creative Commons Attribution 3.0 License.
IndieWebCamp Microformats Webmention W3C HTML5 Creative Commons
WeChat ID
aaronpk_tv