DEV Community

FlareCanary
FlareCanary

Posted on

Auth0 removes enabled_clients from connection reads July 13 — Terraform/Pulumi will silently see every client as disabled

If you manage Auth0 connections through Terraform, Pulumi, or any in-house multi-tenant provisioning script, there's a quiet failure mode landing on July 13, 2026. On that date, Auth0 removes the enabled_clients field from GET /api/v2/connections and GET /api/v2/connections/{id} responses, and stops accepting enabled_clients in PATCH /api/v2/connections/{id}. The field was deprecated January 13, 2026; July 13 is end-of-life.

Nothing returns an error. Calls still get a 200. The connection object comes back, with all the same fields you've always read — minus one. The code that reads connection.enabled_clients to figure out which apps are wired to a connection gets either an empty array or undefined, depending on how Auth0 serializes the absence. Either way, your IaC plan, your drift-detection job, or your client-provisioning script reads "no clients enabled" and reacts accordingly. That reaction is usually one of two failure modes, both bad.

The Two Silent Failure Modes

Mode 1: Drift-detection wipe

Terraform's auth0_connection resource (and the Pulumi equivalent) include enabled_clients as a managed attribute. On every plan, the provider calls GET /api/v2/connections/{id}, reads the current enabled_clients, and compares it to what's in your state file. After July 13, the read returns empty, the state file still lists the configured set, and the provider sees a delta. Then apply calls PATCH to "fix" it — sending the configured enabled_clients list back. The PATCH ignores the field (it's deprecated for input too), the connection's client associations stay whatever they actually are, and the next plan shows the same drift again.

The dangerous variant: if you've ever managed Auth0 partly through Terraform and partly through the dashboard or a separate provisioning tool, your state file lists only the clients Terraform knows about. A drift-detection run that decides "the source of truth is my state file" — common in CI pipelines that auto-apply — will silently issue a corrective change. Until July 13, that worked: PATCH with the desired enabled_clients reconciled the difference. After July 13, the PATCH silently no-ops, so Terraform-managed connections look right in the state but actually still have the out-of-band associations intact. If you then also run the new endpoint-based code to "clean up disabled clients," you can wipe the out-of-band ones.

Mode 2: Multi-tenant provisioning over-restriction

A common multi-tenant pattern in SaaS apps that white-label Auth0: when a new tenant signs up, your control plane creates an Auth0 client for them and enables existing connections for that client. The reverse — enumerating which connections each client is enabled on — is usually done by walking all connections, reading enabled_clients, and filtering by the tenant's client_id.

After July 13, that walk returns empty enabled_clients for every connection. Code that interprets the empty list as "this connection has no enabled clients" and then "remediates" by re-enabling everything from a desired-state list hits either:

  • A no-op (the PATCH ignores the field, the actual associations don't change, the tenant works fine in production but every audit report says they're disabled).
  • A retry storm (if the script keeps trying to re-enable until GET confirms the change, which never happens because the field is gone).

Either way, observability dashboards keyed on enabled_clients go dark or flatline. SCIM-style sync jobs that reconcile Auth0 against an external IdP source-of-truth start over-eagerly trying to fix a discrepancy that isn't real.

What Actually Changed

The new endpoints, available now:

GET /api/v2/connections/{id}/clients — returns the enabled clients for a connection, paginated.

{
  "clients": [
    { "client_id": "abc123..." },
    { "client_id": "def456..." }
  ],
  "next": "opaque_cursor"
}
Enter fullscreen mode Exit fullscreen mode

Query params: take (1–1000, default 50), from (cursor; omit on first call). When next is absent in the response, you've walked the full set. Required scope: read:connections.

PATCH /api/v2/connections/{id}/clients — toggle enabled status for specific clients.

[
  { "client_id": "abc123...", "status": true  },
  { "client_id": "def456...", "status": false }
]
Enter fullscreen mode Exit fullscreen mode

Returns 204 No Content on success. Required scope: update:connections. The hard constraint that bites bulk operations: maximum 50 clients per request. If your provisioning code enables a connection for all 200 clients in a tenant batch via one enabled_clients PATCH today, the equivalent through the new endpoint is four sequential calls. Naive ports that don't chunk silently truncate at 50 (or, depending on the API, return 400 — the docs don't pin this down).

The new PATCH is also selective, not replacing. The old enabled_clients was a full-set replacement: PATCH with ["client_a", "client_b"] made those exactly the enabled set. The new endpoint only toggles the clients you list. Anything not in your PATCH array keeps its current status. Tools that assumed PATCH = full replacement will now leave stale enables in place.

Why It Slips Through Normal Detection

Walk the standard checks:

  • HTTP status check — passes. GET /api/v2/connections/{id} still returns 200. The connection object is still there, just without the enabled_clients key.
  • Schema validation — passes. enabled_clients was always an optional field for connections that hadn't been wired to any client yet. A response without it is structurally valid.
  • Terraform planthinks it caught it. The provider sees the configured enabled_clients is no longer present in the remote state and shows a diff. The diff says "will add three clients." apply runs, the PATCH succeeds (200, the deprecated field is silently dropped), and the next plan shows the same diff. To an operator, this looks like Terraform fighting with another process — not like a deprecated field.
  • CI / unit tests — pass if they mock Auth0. Anyone who tests against a recorded fixture from before July 13 sees the same field they always saw. Tests don't notice until they hit real Auth0.
  • Migration audits — only catch it if you specifically look for enabled_clients in your codebase. Most audits look for endpoint URL changes, not field-level removals.

The Terraform Auth0 provider will probably ship a release that switches enabled_clients to use the new endpoints under the hood — but the release calendar isn't Auth0's, it's HashiCorp's (or the community maintainers'). Pinning to an older provider version with explicit enabled_clients config delays the migration into the deprecation window without addressing it.

How To Detect It Now

1. Grep for enabled_clients across your infra repos. Every match is a migration site:

  • Terraform auth0_connection resources with enabled_clients = [...]
  • Pulumi auth0.Connection resources with enabledClients: [...]
  • Direct API calls — anything reading .enabled_clients off a connection response, or sending enabled_clients in a PATCH body
  • SDK calls: managementClient.connections.update(..., { enabled_clients: ... }) in node-auth0 / auth0-python / auth0.net

2. Verify the new endpoints work for your scopes. The new GET /api/v2/connections/{id}/clients requires read:connections, which most existing M2M tokens already have. The PATCH requires update:connections. If you've used a scoped-down token in CI that only had read:client_grants or similar, you may need to expand it.

3. Chunk PATCHes by 50. Wherever you currently send a full enabled_clients array, the replacement code has to enumerate the desired client_ids, diff against the current set (paginated via the new GET), and issue PATCH /clients calls in chunks of 50 with status: true / status: false per client. Bulk operations that don't chunk break.

4. Treat missing enabled_clients as a sentinel during migration. If your code path can run before or after July 13 (a slow CI matrix, a paused tenant, a long-lived M2M token still hitting an older runtime), add an explicit check: if enabled_clients is undefined on the connection response, fall back to the new endpoint instead of treating it as "no clients enabled." That single guard makes the cutover safe regardless of which side of July 13 the call lands on.

The Pattern

A vendor moves a piece of state from "inlined on a parent resource" to "dedicated sub-resource endpoints." The parent endpoint keeps working. The field just stops being there. Code that assumed presence of the field as "this connection has clients" and absence as "this connection has no clients" reads the new absence as the old "no clients" — and acts on it.

The same shape shows up across cloud providers and SaaS APIs every few months. The mitigation isn't to track every deprecation memo; it's to treat field absence as ambiguous (was it removed? was it always empty? was it filtered out by a scope?) and require an explicit signal before reacting. After July 13, "absent" on Auth0 connection responses means "go ask the new endpoint" — not "no clients enabled."

If you run Auth0 through IaC, the cheapest move this month is to grep for enabled_clients, write the new-endpoint reads alongside the old-field reads, and gate the cutover on a feature flag you can flip per-environment. Eight weeks of runway is enough to do it carefully; the alternative is debugging silent drift on a Monday in July.

Top comments (0)