Installs into .claude/skills of the current project.
Are you the author of Jwt Embed Issuance?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mcorbett51090-jwt-embed-issuance)
---
name: jwt-embed-issuance
description: Canonical 2026 JWT-embed flow for dashboard embedding — required claims (`sub`, `tenant_id`, `iat`, `exp`, `iss`, `aud`, `nonce`), tool-specific verification (Superset guest tokens, Metabase JWT URLs, Cube Authorization Bearer, Power BI MSAL-via-AAD), 5-15 min expiration policy, cross-boundary denial test contract. Invoked by `ravenclaude-core/security-reviewer` for any embed-auth review.
invoked_by: ravenclaude-core/security-reviewer
---
# Skill: jwt-embed-issuance
> **Invoked by:** `ravenclaude-core/security-reviewer` (mandatory for any embed-auth change). Generated by `dashboard-builder` when scaffolding a new embed.
>
> **When to invoke:** any change touching JWT issuance, embed authentication, tenant-claim injection, key rotation. Mandatory pre-merge review.
>
> **Output:** verified JWT-issuance code + tenant-claim shape + expiration policy + rotation plan + cross-boundary denial test pass.
## The canonical 2026 flow
```
[End user] -- logs in --> [Host application (client's web app)]
|
| Authenticated session
v
[App's JWT issuer endpoint]
|
| Issues short-lived JWT (5-15 min)
| Claims: user_id, tenant_id, allowed_dashboards[], optionally explicit RLS rules
| Signature: HS256 (shared secret) or RS256 (asymmetric, preferred for cross-system trust)
v
[Front-end React app]
|
| Passes JWT to embed (iframe param, SDK call, Cube REST/SQL header)
v
[Embed verifies signature, extracts claims, enforces RLS at query time]
|
| Query runs scoped to tenant_id
v
[Data returned, scoped to tenant_id]
```
## Required claims
Every embed JWT must carry:
| Claim | Purpose | Format |
|---|---|---|
| `sub` | Subject (user identifier) | string (user ID from host app) |
| `tenant_id` | The tenant boundary the viewer can see | string or integer |
| `iat` | Issued-at timestamp | unix seconds |
| `exp` | Expiration timestamp | unix seconds (5-15 min from `iat`) |
| `iss` | Issuer (host app identifier) | string URL or app name |
| `aud` | Audience (the embed tool) | string ("metabase" / "superset" / "cube") |
Optional but recommended:
- `allowed_dashboards` — array of dashboard IDs the viewer can access (defense in depth)
- `roles` — for Power BI Embedded, the DAX role(s) to apply
- `nonce` — replay protection
## Tool-specific verification patterns
### Apache Superset
- Issuer side: HS256 with `GUEST_TOKEN_JWT_SECRET` matching Superset's config
- Embed side: front-end SDK calls Superset's `/api/v1/security/guest_token`; Superset validates + applies the `rls` claim filters server-side
- See [Superset embedding docs](https://superset.apache.org/user-docs/using-superset/embedding/)
### Metabase Interactive Embedding (Pro+ only)
- Issuer side: JWT-signed with `embedding-secret-key` from Metabase admin
- Embed side: Metabase SDK or iframe URL with JWT parameter
- Tenant scoping via `user.email` claim + Metabase Sandbox (Pro+ feature)
### Cube
- Issuer side: HS256 with `CUBEJS_API_SECRET`
- Embed side: every Cube query carries the JWT in `Authorization: Bearer <jwt>` header
- Tenant scoping via `securityContext` rules in the Cube schema that reference `securityContext.tenant_id`
### Power BI Embedded (App-Owns-Data)
- Not strictly JWT-flow; uses MSAL acquisition of a Power BI access token via service principal
- Tenant scoping via DAX roles (`EffectiveIdentity` parameter)
- The "JWT" here is the Azure AD token, not an app-issued one
## Expiration policy
- **Default: 5-15 minute expiration.** Stateless revocation otherwise requires a revocation list (operational overhead).
- **Refresh pattern:** front-end requests a new token from the host app's JWT-issuer endpoint when the current one is near expiry (typically at ~80% of lifetime).
- **Long-lived JWTs (>30 min)** are an anti-pattern. The hook ([`../../hooks/flag-data-platform-smells.sh`](../../hooks/flag-data-platform-smells.sh)) flags these in JWT-issuance code.
## Key rotation
- **Asymmetric (RS256 + JWKS):** rotate the signing key without coordinating with the embed-verifier; verifier fetches public keys from a JWKS endpoint
- **Symmetric (HS256 + shared secret):** rotation requires coordination — both sides must update at the same time. For SMB consulting engagements, this is usually acceptable; for productized SaaS scaling to many clients, prefer RS256.
## Cross-boundary denial test
Every JWT-issuance code path ships with a denial test that:
1. Creates two test tenants with disjoint data
2. Issues a JWT for tenant A
3. Attempts to query tenant B's data via the embed
4. **Test passes only when zero rows return**
The test ships in the engagement's CI; failure means RLS or `securityContext` is misconfigured.
## Required code-template companions
The skill ships with [`../../templates/jwt-issuer.ts`](../../templates/jwt-issuer.ts) — a Node/TS scaffold that demonstrates:
- Short-lived token issuance with `tenant_id` claim
- Signing-key management via env var (never inline)
- Rotation hook (signing key version in token header)
- Audience claim per embed tool
- Smoke test that the verification round-trip works
## Anti-patterns this skill flags
- JWT with `exp` > 30 minutes
- Signing key hard-coded in source (use env var; the hook catches this)
- Tenant ID derived from the URL or query parameter (must come from the JWT claim, signed by the host app)
- JWT issued without `iss` and `aud` claims (replay across systems possible)
- Front-end React code that *constructs* the JWT client-side (must come from server-issued endpoint)
- Long-lived "API tokens" reused across viewers (no per-viewer scope)
- HS256 + shared secret for a productized SaaS with many tenants (rotate-without-coordination is too valuable; switch to RS256 + JWKS)
- A JWT-issuer endpoint without rate-limiting on the host app (token-issuance becomes a DoS vector)
- Cross-boundary denial test not run / not present
- Power BI Embedded mistakenly trying to use app-issued JWTs (Power BI uses Azure AD tokens via MSAL)
## References
- Template: [`../../templates/jwt-issuer.ts`](../../templates/jwt-issuer.ts) (runnable; security-critical)
- Skill: [`../rls-policy-authoring/SKILL.md`](../rls-policy-authoring/SKILL.md) — the DB-side or semantic-layer-side enforcement that the JWT's `tenant_id` drives
- Skill: [`../embed-csp-and-iframe-sandboxing/SKILL.md`](../embed-csp-and-iframe-sandboxing/SKILL.md) — the iframe/CSP boundary that contains the embed
- Knowledge: [`../../knowledge/multi-tenant-rls-patterns.md`](../../knowledge/multi-tenant-rls-patterns.md) — where enforcement happens for each stack
- Upstream review rubric: [`../../../ravenclaude-core/agents/security-reviewer.md`](../../../ravenclaude-core/agents/security-reviewer.md) §4 (Sessions & tokens)