05

Register Clients Without Silent Security Downgrade

Recover from provider-specific optional fields only when the rejection is explicit and bounded. • Lab status: conceptual reconstruction. Receipts are expected simulated outputs, not deployed-system measurements.

System map · Day 05

Whole-system design

Five stable layers. Today's work is expanded and linked; the rest stays in context.

Entry and policy

Covered — Client entry · Browser and callback

Onboarding and control

Connector control plane

Design target · not proved

Builds a registration request that preserves the selected public or confidential posture.

Authorization services

Authorization server

Design target · not proved

Registers the client or returns an explicit field rejection at the sanitized endpoint.

Compute and execution

Covered — Connector runtime compute · External resource serverAhead — Refresh coordinator compute

Storage and evidence

Covered — Specification draft store · Secret and token vault · Sanitized discovery cacheAhead — Ordered migration log

Connector configuration store

Source-backed today

Stores returned public client metadata while keeping confidential credentials in the vault boundary.

Evidence plane

Source-backed today

Records each targeted retry and stops instead of silently weakening client authentication.

Traversed today

discover · External resource serverAuthorization servercommit · Connector control planeConnector configuration storeobserve · Connector runtime computeEvidence plane

Overview

Why dynamic registration needs a stop rule

Day 04 fixed the client posture and authorization transaction; Day 03 supplied a sanitized registration endpoint. Dynamic Client Registration (DCR) lets software submit client metadata, but providers vary in which optional fields they accept. Blind retries can silently remove a security property or turn a confidential client into a public one.

Today you will register one public or confidential lab client under RFC 7591, using the optional DCR path described by the MCP authorization profile. DCR is provider-variable: the lesson proves bounded behavior against fixtures, not universal registration support.

Preserve the selected client type

A public client cannot keep a secret and sends token_endpoint_auth_method: none exactly when the provider supports it. A confidential client can protect its authentication material. That classification is an input to registration, never a retry variable.

{
  "client_name": "Atlas Calendar CLI",
  "redirect_uris": ["http://127.0.0.1:43121/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

rejected_fields is a lab-provider extension used to make one retry decision deterministic; RFC 7591 does not define that member. Without an equally explicit, trusted field classification, the gateway stops.

{
  "error": "invalid_client_metadata",
  "rejected_fields": ["logo_uri"],
  "error_description": "optional field is not supported"
}

Only an explicitly named, optional, non-security field may be stripped. Redirect URIs, grant type, response type, and token endpoint authentication method are never recovery candidates.

Bound compatibility recovery to three attempts

The retry loop removes at most one explicitly rejected optional field per attempt, records the diff, and stops after three total submissions. Ambiguous errors, required fields, security-bearing fields, or a missing registration endpoint fail closed and direct the operator to static registration.

for (let attempt = 1; attempt <= 3; attempt += 1) {
  const result = await register(candidate);
  if (result.ok) {
    const { publicMetadata, secretMaterial } = splitRegistrationResponse(result.metadata);
    const secretRef = secretMaterial ? await vault.store(secretMaterial) : undefined;
    return persistPublicClient({ ...publicMetadata, secretRef });
  }
  candidate = stripOneExplicitOptionalField(candidate, result.error);
}
throw new Error("registration_dependency_failed");

{
  "clientId": "calendar-cli-generated",
  "clientType": "public",
  "registrationAttempts": 2,
  "removedOptionalFields": ["logo_uri"],
  "clientSecretStored": false
}

{
  "receipt": "SCG-R05",
  "normal": "registered_on_attempt_2",
  "denied": "confidential_to_public_downgrade",
  "maxAttemptsObserved": 2,
  "fallback": "operator_managed_static_registration"
}

The denial fixture rejects token_endpoint_auth_method; the gateway must stop rather than switch to none. Recovery uses an operator-supplied static client record with the original confidential posture. A separately registered public fixture remains the unaffected control. Cleanup invokes the provider's client-deletion path when available and deletes local references without logging any returned secret.

Score one retry decision

Given an error with one explicit rejected field and a supplied field classification, choose only strip that field or stop. logo_uri may be stripped when declared optional; redirect_uris or token_endpoint_auth_method requires stop. The evidence is the exact field name or the word stop plus one rule.

The misconception is “compatibility means keep removing fields until success.” Contrast the two-attempt optional-field success with the confidential-client denial. Decline guesses based on provider prose, more than three attempts, or any confidential-to-public downgrade.

Carry registration evidence forward

SCG-R05 contains client type, sanitized registration endpoint, attempt log, exact field diffs, stored client reference, denial, fallback, and cleanup. Day 06 consumes the resulting OAuth client while separating API authorization from OpenID Connect identity evidence.