--- description: "Opt-in OAuth 2.1-style authorization for tdmcp Streamable HTTP, with S256 PKCE and fail-closed TouchDesigner-native consent." --- # OAuth, PKCE & TouchDesigner consent tdmcp can protect its Streamable HTTP `/mcp` endpoint with an opt-in, co-located authorization server. The Node process owns discovery, client registration, authorization codes and tokens. TouchDesigner only displays one bounded **Allow / Deny** decision in the existing non-modal Interactions inbox. Codes, PKCE verifiers, OAuth state and bearer tokens never enter the TD project. This mode targets a single-owner tdmcp deployment. Use an external identity provider/authorization server for shared, federated or multi-user deployments. ## Compatibility and modes `TDMCP_HTTP_AUTH_MODE` accepts five values: | Mode | Meaning | | --- | --- | | `auto` | Compatibility default: use `static` when `TDMCP_HTTP_AUTH_TOKEN` exists, otherwise `none`. OAuth is never enabled implicitly. | | `none` | No HTTP bearer authentication. Startup refuses an ignored static token. | | `static` | Existing pre-shared bearer token only. This is not OAuth. | | `oauth` | OAuth authorization-code flow with required S256 PKCE. Startup refuses a static token. | | `hybrid` | Explicit migration mode that accepts both OAuth access tokens and the configured legacy static bearer. | There is no silent downgrade. Invalid combinations fail startup. The TouchDesigner bridge bearer (`TDMCP_BRIDGE_TOKEN`) is a separate credential and still authenticates Node-to-TD REST calls. ## Local loopback setup Plain HTTP is allowed only as an explicit development exception on numeric loopback. `localhost`, wildcard binds and non-loopback HTTP are refused. ```bash export TDMCP_TRANSPORT=http export TDMCP_HTTP_HOST=127.0.0.1 export TDMCP_HTTP_PORT=3939 export TDMCP_HTTP_AUTH_MODE=oauth export TDMCP_PUBLIC_BASE_URL=http://127.0.0.1:3939 export TDMCP_OAUTH_ALLOW_INSECURE_LOOPBACK=1 export TDMCP_BRIDGE_TOKEN='separate-bridge-secret' tdmcp ``` Production/public use requires one canonical HTTPS origin in `TDMCP_PUBLIC_BASE_URL`, an exact external Host, an owner-private OAuth state directory and TLS termination at a trusted reverse proxy on the same host. The Node HTTP listener still binds numeric loopback; wildcard/LAN OAuth binds fail startup so cleartext cannot be exposed directly. ## Client and redirect policy Wave 11 exposes bounded Dynamic Client Registration for public clients only: - authorization-code plus rotating refresh-token grants; - `token_endpoint_auth_method: "none"`; - exact scope `tdmcp:access` and exact resource `/mcp`; - required `code_challenge_method=S256`; plain or missing PKCE is rejected; - numeric loopback HTTP callbacks with an explicit port; - non-loopback callbacks only over HTTPS and only from origins listed in `TDMCP_OAUTH_REDIRECT_ORIGINS`. Registered public clients have a **seven-day default inactivity lifetime** (bounded internally from one hour to 365 days). The inactivity clock starts at registration and advances on security-relevant token issuance/refresh, not on an unauthenticated client lookup. Once the lifetime expires, a client with no live token row is pruned. At the 128-client capacity boundary, registration evicts the oldest tokenless client before refusing the new client; any client that still owns a non-expired, non-revoked access or refresh row is retained. The lifetime is policy-construction state, not a documented environment variable. Unauthenticated DCR uses a continuously refilling bucket per opaque source (20 registrations/hour by default) under a separate global ceiling of 16 times that rate, capped at 3,600. Source state is bounded to 256 entries and expires after two hours. Direct requests use the numeric socket peer. Forwarding headers are accepted only when the immediate peer and every stripped proxy hop are explicitly pinned by `TDMCP_OAUTH_TRUSTED_PROXY_HOPS`; ambiguous, non-numeric, untrusted or canonical-host/protocol-mismatched forwarding fails closed. Source keys are process-local hashes and are neither returned nor logged. Client ID Metadata Documents are not implemented in this wave. DCR is the supported public-client registration path; clients that require CIMD need a later, separately threat-modelled interoperability wave. Discovery is path-specific at `/.well-known/oauth-protected-resource/mcp`; the legacy root protected-resource metadata URL intentionally returns 404. Authorization-server metadata is at `/.well-known/oauth-authorization-server`. ## Native consent and failure behavior Opening `/authorize` returns a small `202` waiting page immediately; it does not hold the original request open while TouchDesigner waits. The TD inbox shows the bounded self-asserted client name, redirect, resource and scope with exact choices **Allow / Deny**. Only a consumed, target-matching **Allow** can create a code. The fail-closed contract maps close, timeout, disconnect, duplicate consumption, queue saturation, Perform Mode, missing/headless UI, scheduling error or Node shutdown to **Deny**. A disposable TD 2025.32820 sandbox passed Allow, Deny, timeout, close, disconnect and Perform-mode denial through the native callback transport. Actual headless TD, a physical pointer click and production HTTPS/TLS deployment remain UNVERIFIED. Panic, blackout and emergency paths never wait for OAuth consent. `TDMCP_BRIDGE_ALLOW_EXEC=0` remains supported because consent uses structured, authenticated bridge routes, not `/api/exec`. The OAuth state directory stores public client metadata and HMAC digests of access/refresh tokens in owner-private, atomically replaced files. Raw tokens, authorization codes and pending consent are not persisted. Access tokens default to 15 minutes; refresh tokens default to 30 days and rotate on use. ## Evidence examples ```json { "status": "PASS", "evidence": "offline authorization issued a scoped token only after simulated Allow; DCR pruned expired tokenless rows, evicted the oldest tokenless row at capacity and retained a live-token owner" } ``` ```json { "status": "FAIL", "evidence": "startup refused oauth mode with a legacy static token instead of silently accepting it" } ``` ```json { "status": "UNVERIFIED", "reason": "final integrated TD-native prompt was not exercised in a fresh disposable sandbox", "checks": ["Allow and Deny in TD 2025.32820", "Perform/headless UI", "production HTTPS deployment"] } ``` Offline proof is not live TouchDesigner proof. Keep the final rows UNVERIFIED until a fresh isolated bridge runs them without a thread-conflict alert. ## Configuration reference See [Environment variables](/reference/environment) for body limits, redirect origins, state paths and bounded token/consent TTLs. There is currently no public environment variable for registered-client inactivity. `tdmcp status` reports only the resolved HTTP auth mode; it never prints tokens or the bridge bearer.