Authio docs

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 on dcr_clients.client_secret_hash IS NULL when registration_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

  1. POST /oauth2/cimd/resolve (or the same logic embedded in /oauth2/authorize) sees a URL-shaped client_id.
  2. Auth-core looks up dcr_clients by (project_id, cimd_url). If the row exists and cimd_last_validated_at is within the re-validation window (1 hour), we serve the cached metadata.
  3. 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_changed audit event so an operator can spot a sudden change in a client’s requested scopes.
  4. On first sight we create a fresh dcr_clients row with registration_kind = ‘cimd’.

Security considerations

CIMD fetch is server-side outbound HTTP against a URL the requester chose. The non-negotiable SSRF guards below mean a malicious 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, or rlwy.net are 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).