name: embedding-connected-apps-jwt description: A step-by-step setup for secure Tableau embedding with a Connected App (Direct Trust) + a server-minted JWT and the Embedding API v3 — minting the right claims, scoping the token, and binding the JWT identity to the RLS entitlement key. Use when embedding a viz in an app with per-user or per-tenant data isolation. The auth verdict escalates to ravenclaude-core/security-reviewer.
Embedding with Connected Apps + JWT (Direct Trust)
Owner:
tableau-admin(primary). Security verdict: escalates toravenclaude-core/security-reviewer— this is an access control, not a convenience feature. Grounded in:../../knowledge/governance-embedding-decision-trees.mdEmbedding auth tree +../../best-practices/embed-connected-apps-jwt-not-trusted-tickets.md
A repeatable procedure for the supported, modern embedding-auth path: a Connected App (Direct Trust) on the Tableau site, a JWT minted server-side by your application, and the Embedding API v3 web component rendering the viz. It replaces deprecated/insecure trusted tickets and embedded service-account credentials.
When to use
Embedding a Tableau viz in a customer-facing or internal app where users authenticate through your app and must see only their data (per-user or per-tenant isolation). Not for a public, unauthenticated, all-data viz (that needs no JWT).
The hard rule before you start
The JWT identity and the RLS entitlement key are ONE design, built together. A token that
authenticates the right user against the wrong entitlement key still leaks (see the scenario
2026-06-05-embedding-jwt-scope-and-rls-mismatch.md). Decide the entitlement key (tenant_id,
region, customer_id) and how the JWT sub maps to it before writing any code.
Steps
- Create the Connected App (Direct Trust) on the Tableau site. A site/server admin
creates it (Settings → Connected Apps → New → Direct Trust) and enables it. Capture the
client ID (
iss), generate a secret, and capture the secret ID (kid). The secret is shared between Tableau and your app — store it in a secret manager, never in committed config or client-side code.[verify-at-build]the exact UI path against current Tableau Help. - Decide the RLS design in parallel. The published data source enforces row-level security
via an entitlements table + a row-level data policy keyed on the entitlement key, and the
JWT
submust resolve to that key. (RLS mechanism choice → the RLS mechanism tree; the data-policy approach is a Data Management add-on[verify-at-build].) - Mint the JWT server-side with the required claims:
iss= Connected App client IDkid= secret ID (in the JWT header), signed (HS256) with the secret valuesub= the actual per-user / per-tenant identity (so the data policy resolves the right rows — not a shared service account)aud="tableau"[verify-at-build]exp= short-lived (minutes); alsojti/iatas requiredscp= an ARRAY of scopes — for embedding,["tableau:views:embed"](addtableau:views:embed_authoringetc. only if needed). The scope claim must be an array even for a single entry.[verify-at-build]the exact required/allowed scopes against the Access Scopes for Connected Apps doc each engagement.
- Render with the Embedding API v3 web component. Load the v3 library and use the
<tableau-viz>component, passing the minted JWT as the token. The token authenticates the embedded session; the viz inherits the published data source's RLS.[verify-at-build]the current component attribute names. - Test isolation with TWO real identities, not one. A single-tenant test passes while a
cross-tenant leak hides. Confirm tenant A cannot see tenant B's rows, and that an absent /
wrong
subfails closed. - Escalate the verdict to
ravenclaude-core/security-reviewerwith the threat model: who the populations are, the entitlement key, where the secret is stored and how it's rotated, token lifetime, and what a single-row cross-tenant leak would cost.
Troubleshooting (field-tested)
- 401 at viz load → almost always a missing/malformed claim, not the secret. Check
iss/kid/sub/aud/expand thatscpis an array, before regenerating anything. - Auth works but data leaks across tenants → the
subdoesn't map to the entitlement key (often a shared service-accountsub). Mint per-tenant; re-test with two tenants. - Tempted to scope with a URL/dashboard filter → that's a convenience filter, not a control; it's removable. Use the RLS data policy.
- "Connected App secret invalid" → confirm the Connected App is enabled at the site level
and the
kidmatches the secret you signed with.
Output
Embedding design: <where the viz is embedded; per-user or per-tenant isolation>
Connected App: <client ID (iss), secret ID (kid) — secret in: <secret manager ref>>
JWT claims: <sub mapping to entitlement key; scp array; exp lifetime>
RLS binding: <entitlements table + data-policy key; how sub → key resolves>
Isolation test: <two-identity result — A cannot see B; wrong sub fails closed>
Security escalation: <threat model handed to ravenclaude-core/security-reviewer>
Follow the team Output Contract + the cross-plugin Structured Output Protocol. Never ship the secret in any artifact; bundle a reference, not a literal.
See also
../../knowledge/governance-embedding-decision-trees.md— the Embedding auth + RLS mechanism trees../../scenarios/2026-06-05-embedding-jwt-scope-and-rls-mismatch.md— the 401 + leak field note../../templates/embedding-design-spec.md— the design template../../agents/tableau-admin.md— the owning agent