Concepts
Client ID Metadata Document (CIMD)
The MCP-spec 2025-11-25 preferred client-identity mechanism: clients publish their metadata at a stable URL and present that URL as their client_id.
Client ID Metadata Document (CIMD) is the OAuth WG draft the Model Context Protocol adopted as its preferred client-identity mechanism in the 2025-11-25 spec update. Instead of registering with the authorization server (DCR), the client publishes its metadata at a stable HTTPS URL and presents that URL as its client_id at the authorize endpoint.
Why CIMD beats DCR for MCP
- No round-trip. The client doesn’t need to coordinate with every AS in the world; it just publishes its document once.
- No shared secret. CIMD clients are PUBLIC by definition (per the spec); they never receive a
client_secret. Authio enforces this at the database layer via a CHECK constraint ondcr_clients.client_secret_hash IS NULLwhenregistration_kind = ‘cimd’. - Lifecycle in the client’s control. To rotate the metadata, the client just updates the document and bumps the ETag; on next sign-in Authio re-fetches and re-validates.
Example metadata document
The client serves a document like this at, e.g., https://my-mcp-server.example.com/.well-known/oauth-client-id:
{
"redirect_uris": [
"https://my-mcp-server.example.com/oauth/callback"
],
"client_name": "My MCP Server",
"client_uri": "https://my-mcp-server.example.com",
"logo_uri": "https://my-mcp-server.example.com/logo.png",
"tos_uri": "https://my-mcp-server.example.com/tos",
"policy_uri": "https://my-mcp-server.example.com/privacy",
"scope": "openid email mcp:read mcp:write",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"application_type": "web",
"software_id": "my-mcp-server",
"software_version": "1.4.0"
}
How Authio resolves a CIMD client_id
POST /oauth2/cimd/resolve(or the same logic embedded in/oauth2/authorize) sees a URL-shapedclient_id.- Auth-core looks up
dcr_clientsby(project_id, cimd_url). If the row exists andcimd_last_validated_atis within the re-validation window (1 hour), we serve the cached metadata. - Otherwise, we fetch the URL with strict guards (see below). On ETag match we just bump the validation timestamp; on a changed ETag we re-cache the metadata and emit an
oauth_client.cimd_metadata_changedaudit event so an operator can spot a sudden change in a client’s requested scopes. - On first sight we create a fresh
dcr_clientsrow withregistration_kind = ‘cimd’.
Security considerations
client_id URL cannot reach Authio’s internal infrastructure.- HTTPS only.
http://URLs are rejected before any DNS lookup. - 64 KiB body cap. We read at most 64 KiB + 1 byte; anything over is rejected with
invalid_client_id_url. - 5 s request timeout, 3 s connect timeout.
- At most 1 redirect. The redirect target is re-checked against the SSRF deny-list before following.
- Resolved-IP deny-list. Every IP the hostname resolves to is checked against the union of: RFC 1918 (10/8, 172.16/12, 192.168/16), CGNAT (100.64/10), loopback (127/8, ::1), link-local (169.254/16, including AWS metadata 169.254.169.254, fe80::/10), multicast, ULA (fc00::/7), unspecified, IETF protocol assignments, documentation ranges (192.0.2/24, 198.51.100/24, 203.0.113/24), benchmark (198.18/15), reserved (240/4), and the NAT64 prefix (64:ff9b::/96). Any match aborts the dial — defends against DNS rebinding too.
- Authio-domain suffix deny-list. Hostnames ending in
authio.com,authio.local,railway.internal, orrlwy.netare rejected before DNS, so a CIMD URL cannot point at our own public infrastructure. - SHA-256 only for hashes. CIMD validation never argon2-hashes anything — there is no secret to hash.
Caching and re-validation
We re-fetch at most once per hour per (project, URL).If-None-Match is sent when we have a cached ETag; a 304 Not Modified just bumps cimd_last_validated_at. Operators can force a fresh fetch via the dashboard’s “Preview” button or by calling POST /oauth2/cimd/resolve with {"force": true}.
Enabling CIMD
CIMD is off by default. Flip projects.cimd_enabled = truefrom the dashboard (Settings → Connect / MCP) or by checking the “MCP enabled” box (which auto-flips CIMD and bumps dcr_mode to initial_access_token for backwards compatibility).
