diff --git a/dtwo/.claude-plugin/plugin.json b/dtwo/.claude-plugin/plugin.json index 7d32bab..ded6de7 100644 --- a/dtwo/.claude-plugin/plugin.json +++ b/dtwo/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dtwo", - "version": "1.3.4", + "version": "1.3.5", "description": "Manage Dtwo gateways, policies, and Rego with the Dtwo MCP server.", "author": { "name": "Dtwo", diff --git a/dtwo/.codex-plugin/plugin.json b/dtwo/.codex-plugin/plugin.json index 97ea777..120c9d9 100644 --- a/dtwo/.codex-plugin/plugin.json +++ b/dtwo/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dtwo", - "version": "1.3.4", + "version": "1.3.5", "description": "Manage Dtwo gateways, policies, and Rego with the Dtwo MCP server.", "author": { "name": "Dtwo", diff --git a/dtwo/.cursor-plugin/plugin.json b/dtwo/.cursor-plugin/plugin.json index 34ef67a..8f9b37d 100644 --- a/dtwo/.cursor-plugin/plugin.json +++ b/dtwo/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dtwo", - "version": "1.3.4", + "version": "1.3.5", "description": "Manage Dtwo gateways, policies, and Rego with the Dtwo MCP server.", "author": { "name": "Dtwo", diff --git a/dtwo/skills/dtwo-gateway-config/SKILL.md b/dtwo/skills/dtwo-gateway-config/SKILL.md index cbf59bb..1e80f1d 100644 --- a/dtwo/skills/dtwo-gateway-config/SKILL.md +++ b/dtwo/skills/dtwo-gateway-config/SKILL.md @@ -283,7 +283,7 @@ Human-gated session clearing (session-control) registration: the IdP app the bro | Field | Required | Type | Default | Target | Guidance (from artifact) | |---|---|---|---|---|---| -| `client_id` | no | string | `not declared` | `SESSION_CONTROL_CLIENT_ID` | Leave unset on a gateway using Dtwo authentication. Set it only when the gateway trusts your own IdP: the ceremony app must live in the IdP that issues the gateway's inbound tokens, so register a dedicated public client there (authorization-code + PKCE, no refresh grant) and paste its client id here. Do not reuse the gateway API client. | +| `client_id` | no | string | `not declared` | `SESSION_CONTROL_CLIENT_ID` | Leave unset on a gateway using Dtwo authentication. Set it only when the gateway trusts your own IdP: the ceremony app must live in the IdP that issues the gateway's inbound tokens, so register a dedicated public client there (authorization-code + PKCE, no refresh grant) and paste its client id here. Do not reuse the gateway API client. On Okta that is a Native app with an Okta-generated client ID, included in the custom authorization server's access policy. The gateway picks the identity claim it binds on from the issuer host (Entra, Okta or other); most gateways need nothing more. For an Okta org on a custom domain or a sovereign-cloud Entra issuer — and only then — add `SESSION_SUBJECT_PROVIDER=okta` (or `entra`) to `gateway.advanced`, or every clear fails `subject_mismatch`. | | `redirect_uri` | no | string | `not declared` | `SESSION_CONTROL_REDIRECT_URI` | Leave unset in almost all cases — the gateway derives `/session-control/clear/callback` and the platform registers that. Set it when a reverse proxy or split-horizon DNS puts the browser on a different origin than the audience: give the URL the browser can actually reach, at the root path (`/mcp/*` routes to the streamable-HTTP handler, which rejects browser GETs). It is required when the first `jwt_audience` is not an absolute URL — e.g. an Entra ID v2.0 gateway, whose audience is the gateway app's client ID (a GUID) — since there is no origin to derive from, and the deploy rejects an armed gateway that leaves it unset. | **Cross-field constraints** (verbatim from artifact): @@ -398,7 +398,7 @@ Every field marked `secret: true` in the artifact. Put a Vault reference there ( - **`targetKind: platform`** — value is applied as a platform-side control at the named `platform.*` path rather than written to the gateway env file. - **`targetKind: sotwPath`** — value is written into the SOTW YAML at the named dotted path (e.g. `sotw.url`, `sotw.oauth_config.client_secret`). Read the `Target` column per field; do not infer a field's target from its section. - + @@ -409,6 +409,17 @@ Controls authentication, SSRF protection, `log_level`, `advanced` flags, DCR / d - **Authentication** defaults to enabled when omitted. Every field in `gateway.authentication` — including the token-validation flags (`jwt_issuer_verification`, `jwt_audience_verification`, `require_jti`, `require_token_expiration`, `mcp_oauth_resource_metadata_enabled`) — is listed in the Schema Digest above with its target, its default, and the schema's own guidance on when to set it. Read the table there rather than guessing from this summary; most of those flags carry a gateway-applied default (their Default column reads `` `true` (gateway) ``), and where the artifact declares none (`mcp_oauth_resource_metadata_enabled` reads `not declared`) the Guidance column states the effective value. - **Gateway-side `jwks_info` is independent of any `mcp_servers[].authentication` block.** When the prompt supplies an IdP tenant and audience (e.g. Auth0), populate `gateway.authentication.jwks_info` (`jwt_algorithm`, `jwt_jwks_uri`, `jwt_issuer`, `jwt_audience`) — even when the upstream MCP server uses OAuth/DCR, and even when the prompt says the upstream server "only supports OAuth" or "does not accept bearer tokens." Those statements describe the outbound leg to the MCP server, not the inbound leg from clients to the gateway. - **SSRF** defaults to strict (block localhost, block private networks, fail-closed DNS) when omitted. Set `allow_private_networks: true` to permit access to `host.docker.internal` and other private addresses. +- **Human-gated clearing on your own IdP** (a gateway whose `jwks_info` points at Okta, Entra ID or another IdP rather than Dtwo authentication) needs a `session_control.client_id` for a dedicated public client in that same IdP. The digest's `client_id` guidance covers the general rule. Things users get wrong: + - **Okta:** create a **Native** OIDC app (authorization code with PKCE, no client secret, no refresh token) and choose **Okta-generated client ID**, not a Client ID Metadata Document. Add it, and every app the MCP clients sign in with, to the custom authorization server's **access policy**, or Okta refuses the sign-in before the gateway sees it. + - **Sign-in redirect URI:** register `/session-control/clear/callback` on the clearing app, or whatever `session_control.redirect_uri` names. It is required when the first `jwt_audience` is not a URL (Entra ID v2.0), and when the browser reaches the gateway at a different origin than its audience. + - **Intent clears need intent capture:** set `gateway.intent.enabled: true`, or `set_intent` succeeds but nothing is stored and a clear finds nothing to clear. + - **Identity claim — most gateways set nothing.** The gateway detects Entra ID (`login.microsoftonline.com`, `sts.windows.net`) and Okta (`*.okta.com`, `*.oktapreview.com`, `*.okta-emea.com`, `*.okta-gov.com`) from the `jwt_issuer` host, treats every other issuer (Auth0 included) as standard OIDC, and binds clears on the claim that identifies the same person in both tokens. Do not add `SESSION_SUBJECT_PROVIDER` unless the issuer is an **Okta org on a custom domain** or a **sovereign-cloud Entra ID** host; for those two only, add the provider to `gateway.advanced` as a list entry, or every clear fails `subject_mismatch`. The accepted values are `auto` (the default), `oidc`, `entra` and `okta`; anything else fails the deploy. + + ```yaml + gateway: + advanced: + - SESSION_SUBJECT_PROVIDER=okta + ``` ### MCP Servers Section diff --git a/dtwo/skills/dtwo-gateway-config/schema-reference.json b/dtwo/skills/dtwo-gateway-config/schema-reference.json index 1940fb8..a4e4094 100644 --- a/dtwo/skills/dtwo-gateway-config/schema-reference.json +++ b/dtwo/skills/dtwo-gateway-config/schema-reference.json @@ -717,7 +717,7 @@ "deployDefault": null, "target": "SESSION_CONTROL_CLIENT_ID", "description": "Public client id of the dedicated session-control OAuth app (native, PKCE, no client secret). Optional on a gateway authenticating against Dtwo's Auth0: the platform provisions one per organization and renders it at deploy. An explicit value always wins.", - "rationale": "Leave unset on a gateway using Dtwo authentication. Set it only when the gateway trusts your own IdP: the ceremony app must live in the IdP that issues the gateway's inbound tokens, so register a dedicated public client there (authorization-code + PKCE, no refresh grant) and paste its client id here. Do not reuse the gateway API client.", + "rationale": "Leave unset on a gateway using Dtwo authentication. Set it only when the gateway trusts your own IdP: the ceremony app must live in the IdP that issues the gateway's inbound tokens, so register a dedicated public client there (authorization-code + PKCE, no refresh grant) and paste its client id here. Do not reuse the gateway API client. On Okta that is a Native app with an Okta-generated client ID, included in the custom authorization server's access policy. The gateway picks the identity claim it binds on from the issuer host (Entra, Okta or other); most gateways need nothing more. For an Okta org on a custom domain or a sovereign-cloud Entra issuer — and only then — add `SESSION_SUBJECT_PROVIDER=okta` (or `entra`) to `gateway.advanced`, or every clear fails `subject_mismatch`.", "targetKind": "envVar" }, { diff --git a/skill-harness/src/schemaArtifact.ts b/skill-harness/src/schemaArtifact.ts index d4643ed..3217af9 100644 --- a/skill-harness/src/schemaArtifact.ts +++ b/skill-harness/src/schemaArtifact.ts @@ -45,7 +45,7 @@ export const SCHEMA_ARTIFACT_VERSION = '1.1.0'; * in reviewed source. It cannot stop a PR that bumps both the artifact and * this constant — judging that change is still review's job. */ -export const EXPECTED_SCHEMA_ARTIFACT_SHA256 = '4dc79f71108e6c02642bd5fc8463e310924d0c3c453e634cd5afc60d9b46af16'; +export const EXPECTED_SCHEMA_ARTIFACT_SHA256 = '9133ce3ac2650844dc11ad453ed36ae929f11ab7b3df4d4842c1504292075313'; /** * `platform` is emitted by the currently vendored artifact (every diff --git a/skill-harness/src/validatorBundle.ts b/skill-harness/src/validatorBundle.ts index ab137e3..6e9e0aa 100644 --- a/skill-harness/src/validatorBundle.ts +++ b/skill-harness/src/validatorBundle.ts @@ -39,7 +39,7 @@ export const EXPECTED_VALIDATOR_BUNDLE_VERSION = '4.0.0'; * `__tests__/schemaDigest.test.ts`, not at module load. Bump this together * with `EXPECTED_VALIDATOR_BUNDLE_VERSION` when the bundle is re-vendored. */ -export const EXPECTED_VALIDATOR_BUNDLE_SHA256 = '9dba8b63ab3b7ef486aa3e448048ebe8eea04fac3e26598cc75f57e112d1474f'; +export const EXPECTED_VALIDATOR_BUNDLE_SHA256 = '27c46ab1263fe0b75f71e60a8a37f4f401c5699651542565859cd0843ff9868c'; const HERE = dirname(fileURLToPath(import.meta.url)); diff --git a/skill-harness/vendor/config-validator.bundle.mjs b/skill-harness/vendor/config-validator.bundle.mjs index c8d2e57..7a8bf5b 100644 --- a/skill-harness/vendor/config-validator.bundle.mjs +++ b/skill-harness/vendor/config-validator.bundle.mjs @@ -23447,7 +23447,7 @@ var SessionControlSchema = external_exports.object({ description: "Public client id of the dedicated session-control OAuth app (native, PKCE, no client secret). Optional on a gateway authenticating against Dtwo's Auth0: the platform provisions one per organization and renders it at deploy. An explicit value always wins.", target: "SESSION_CONTROL_CLIENT_ID", audience: "user", - rationale: "Leave unset on a gateway using Dtwo authentication. Set it only when the gateway trusts your own IdP: the ceremony app must live in the IdP that issues the gateway's inbound tokens, so register a dedicated public client there (authorization-code + PKCE, no refresh grant) and paste its client id here. Do not reuse the gateway API client." + rationale: "Leave unset on a gateway using Dtwo authentication. Set it only when the gateway trusts your own IdP: the ceremony app must live in the IdP that issues the gateway's inbound tokens, so register a dedicated public client there (authorization-code + PKCE, no refresh grant) and paste its client id here. Do not reuse the gateway API client. On Okta that is a Native app with an Okta-generated client ID, included in the custom authorization server's access policy. The gateway picks the identity claim it binds on from the issuer host (Entra, Okta or other); most gateways need nothing more. For an Okta org on a custom domain or a sovereign-cloud Entra issuer \u2014 and only then \u2014 add `SESSION_SUBJECT_PROVIDER=okta` (or `entra`) to `gateway.advanced`, or every clear fails `subject_mismatch`." }) ), redirect_uri: external_exports.string().min(1).refine(isRenderableRedirectUri, { @@ -23462,8 +23462,8 @@ var SessionControlSchema = external_exports.object({ ), // There is deliberately NO `issuer` field. The ceremony's issuer is always // `gateway.authentication.jwks_info.jwt_issuer` — the callback binds the - // inbound and browser-login identities as raw `iss|` (`sub`, or `oid` - // on an Entra issuer), so any other value + // inbound and browser-login identities as raw `iss|` (`sub`, `oid` + // on an Entra issuer, `uid` against the ID token's `sub` on Okta), so any other value // makes every ceremony deterministically fail subject_mismatch. A field // whose only legal value is a copy of another field is pure misconfiguration // surface, so the deploy derives SESSION_CONTROL_ISSUER from jwt_issuer