skills/metabase-embedding-sso-implementation/SKILL.md
Implements JWT SSO authentication for Metabase embedding in a project. Supports all embedding types that use SSO — Modular embedding (embed.js web components), Modular embedding SDK (@metabase/embedding-sdk-react), and Full app embedding (iframe-based). Creates the JWT signing endpoint, configures the frontend auth layer, and sets up group mappings. Use when the user wants to add SSO/JWT auth to their Metabase embedding, implement user identity for embedded analytics, set up JWT authentication for Metabase, or connect their app's authentication to Metabase embedding.
npx skillsauth add metabase/agent-skills metabase-embedding-sso-implementationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Follow the workflow steps in order — do not skip any step. Create the checklist first, then execute each step and explicitly mark it done with evidence. Each step's output feeds into the next, so skipping steps produces incorrect implementations.
If you cannot complete a step due to missing info or tool failure, you must:
Each workflow step must end with Status: ✅ complete or Status: ❌ blocked. Steps are sequential — do not start a step until the previous one is complete. Each step must include evidence (detected code patterns, file paths, diffs applied, pass/fail results).
Follow the app's existing architecture, template engine, layout/partial system, code style, and route patterns. Do not switch paradigms (e.g., templates to inline HTML or vice versa). If the app has middleware for shared template variables, prefer that over duplicating across route handlers.
The JWT SSO endpoint must integrate with the app's existing authentication system. The endpoint must only issue Metabase JWTs for users who are already authenticated in the host app. Never create an endpoint that issues tokens without verifying the user's session first.
SSO requests to the Metabase instance must be proxied through the app's backend (FE → BE → Metabase /auth/sso). This keeps the Metabase instance URL and JWT tokens off the client, avoids CORS issues, and ensures auth is always validated server-side. The JWT shared secret must never be exposed to the frontend.
This skill implements JWT SSO authentication for Metabase embedding. It supports all three embedding types that use SSO:
| Embedding type | Delivery mechanism | Frontend auth config |
|---|---|---|
| Modular embedding (embed.js) | Web components (<metabase-dashboard>, etc.) | window.metabaseConfig — see fetched docs for available auth fields |
| Modular embedding SDK (@metabase/embedding-sdk-react) | React components (<InteractiveDashboard>, etc.) | defineMetabaseAuthConfig() — see fetched docs for available auth fields |
| Full App embedding | iframe with full Metabase UI | iframe src points through SSO endpoint |
The SSO endpoint response format (JSON, redirect, proxy to Metabase /auth/sso, etc.) varies by embedding type and Metabase version — consult the fetched docs to determine the correct behavior.
The consumer's app may be written in any backend language (Node.js, Python, Ruby, PHP, Java, Go, .NET, etc.). Keep instructions language-agnostic unless a specific language is detected in Step 1.
email, first_name, last_name, groups, exp)METABASE_JWT_SHARED_SECRET environment variablemetabase-modular-embedding-version-upgrade skillMETABASE_SECRET_KEY with {resource, params} payloads, not SSO)The JWT signed with METABASE_JWT_SHARED_SECRET must contain these fields:
| Field | Type | Required | Description |
|---|---|---|---|
| email | string | Yes | User's email — Metabase uses this as the unique identifier. Auto-provisions account on first login. |
| first_name | string | Yes | User's first name — synced on every login. |
| last_name | string | Yes | User's last name — synced on every login. |
| groups | string[] | Yes | Array of group names — Metabase syncs group memberships on every login when group sync is enabled. |
| exp | number | Yes | Token expiration as Unix timestamp. Recommend 10 minutes: e.g. Math.round(Date.now() / 1000) + 600. |
Additional user attributes can be included as extra key/value pairs in the JWT — Metabase will store them as user attributes for use in sandboxing and data permissions.
Fetch the version-specific llms-embedding-full.txt using this URL:
https://www.metabase.com/docs/v0.{VERSION}/llms-embedding-full.txt
The version in the URL uses the format v0.58 (normalize: strip leading v or 0., drop patch — e.g., 0.58.1 → 58 → URL uses v0.58). This single file contains all embedding documentation for that version, optimized for LLM consumption.
Other constraints:
Use AskUserQuestion and halt until answered if:
Create a checklist to track progress. In Claude Code, use TaskCreate/TaskUpdate tools:
Grep the project for these patterns (in parallel) to determine which embedding type is in use:
Modular embedding (embed.js):
embed.js or /app/embed.js in HTML/template fileswindow.metabaseConfig or defineMetabaseConfig<metabase-dashboard, <metabase-question, <metabase-browserModular embedding SDK:
@metabase/embedding-sdk-react in package.json or import statementsMetabaseProvider or defineMetabaseAuthConfigInteractiveDashboard, InteractiveQuestion, CollectionBrowser, etc.Full App embedding:
<iframe with Metabase URLs (look for the instance URL or /dashboard/, /question/, /auth/sso)/auth/sso redirect logicreturn_to parameter constructionIf no embedding code is found → AskUserQuestion: which embedding type does the user plan to use?
If multiple types are detected, the SSO endpoint must handle all of them (see dual-mode endpoint in Step 2).
Record the detected embedding type(s) — this controls the entire implementation.
The Metabase instance version determines which auth config fields, function signatures, and SSO behavior are available. Always AskUserQuestion for the instance version — even if a version appears in Docker tags, env vars, or package.json, confirm it with the user. The instance version is the source of truth for fetching docs.
SDK/instance version mismatch check: If the project uses @metabase/embedding-sdk-react (npm package), compare the SDK package version with the instance version. The SDK package version must match the instance version — e.g., SDK v0.52.x requires instance v0.52.x. If they don't match, stop and tell the user to align the versions before proceeding with SSO. Suggest using the metabase-modular-embedding-version-upgrade skill to upgrade.
Perform the project scan and doc fetch concurrently — they are independent. Use parallel tool calls within a single message wherever there are no dependencies.
Fetch llms-embedding-full.txt for the instance version confirmed in Step 0b (see "Allowed documentation sources" for URL format). Search within it for fetchRequestToken, authProviderUri, jwtProviderUri, defineMetabaseAuthConfig, JWT, and SSO to find the relevant auth documentation.
These docs are the authoritative source for auth configuration options, function signatures, deprecated fields, and endpoint response formats for the detected version. Use them in Step 2 when designing the auth architecture.
Launch this concurrently with the project scan steps below.
package.json, requirements.txt, Gemfile, pom.xml, go.mod, composer.json, etc.).This is critical — the SSO endpoint must integrate with the app's existing auth. Search for:
express-session, cookie-session, flask-login, devise, Passport, next-auth, etc.)req.user, req.session, current_user, @login_required, [Authorize], etc)email, name, first_name, last_name, role, group, etc)/login, /auth, /signin, /api/auth, etc)For each matching file, read the entire file.
Read all files identified in Step 0a. Extract:
METABASE_, MB_)Search for the user model to understand available fields:
req.user, session store)Backend: {language}, {framework}
Auth system: {mechanism} (e.g., express-session + Passport, next-auth, Django sessions)
User model: {file}:{line} — fields: {email, first_name, last_name, role, ...}
User on request: {how to access} (e.g., req.user, request.user, session[:user])
Groups/roles: {source} — values: {list of group/role names}
Embedding type: {modular | sdk | full-app | multiple}
Metabase URL: {env var or value}
Metabase version: {version}
Existing SSO endpoint: {file}:{line} or "none"
Existing auth config: {description} or "none"
Based on the project inventory, design the SSO implementation. This step produces the design — no code changes yet.
Design the SSO endpoint route:
Route: Choose a route that fits the app's existing patterns. Common choices: /sso/metabase, /api/auth/metabase, /auth/metabase-sso. Follow the app's existing route naming conventions.
HTTP method: GET (Metabase's SDK/embed.js sends GET requests to the JWT provider URI)
Auth guard: The endpoint MUST be protected by the app's existing auth middleware. Only authenticated users should receive a Metabase JWT. Specify which middleware/decorator to use based on Step 1c findings.
User mapping: Define how to extract user fields from the request:
email ← {source} (e.g., req.user.email)
first_name ← {source} (e.g., req.user.firstName or req.user.name.split(' ')[0])
last_name ← {source} (e.g., req.user.lastName or req.user.name.split(' ')[1])
groups ← {source} (e.g., [req.user.role] or req.user.groups)
Group mapping strategy: If the app's roles/groups don't directly match desired Metabase groups, define a mapping:
App role "admin" → Metabase group "Administrators"
App role "analyst" → Metabase group "Analysts"
App role "viewer" → Metabase group "Viewers"
If the mapping is unclear → AskUserQuestion.
Token expiration: 10 minutes (e.g. Math.round(Date.now() / 1000) + 600) unless the app has a specific session timeout that should be matched.
Consult the fetched docs to determine what the SSO endpoint should return for the detected embedding type and Metabase version. The response format (JSON with a signed JWT, redirect to Metabase /auth/sso, server-side proxy, etc.) varies — do not assume a fixed pattern. The docs describe how the SDK/embed.js/iframe expects to receive the authentication token for the detected version.
If the project uses multiple embedding types, the endpoint may need to support multiple response modes (e.g., distinguishing via a query parameter). Check the docs for how each embedding type calls the JWT provider.
Decide the frontend auth approach based on embedding type. Consult the docs fetched in Step 1a to determine which auth config fields are available for the detected version — field names, signatures, and supported options vary across versions. The exact code patterns are specified in Step 3d.
If the project doesn't already have a JWT library:
| Language | Library | Install command |
|---|---|---|
| Node.js | jsonwebtoken | npm install jsonwebtoken |
| Python | PyJWT | pip install PyJWT |
| Ruby | jwt | gem install jwt |
| PHP | firebase/php-jwt | composer require firebase/php-jwt |
| Java | io.jsonwebtoken:jjwt | Add to Maven/Gradle |
| Go | github.com/golang-jwt/jwt/v5 | go get github.com/golang-jwt/jwt/v5 |
| .NET | System.IdentityModel.Tokens.Jwt | dotnet add package System.IdentityModel.Tokens.Jwt |
If a JWT library is already in the project, use it.
Create a complete file-by-file change plan. Every change should be specified with the target file, the old code (if modifying), and the new code.
METABASE_JWT_SHARED_SECRET to the project's environment configuration (.env, .env.example, docker-compose.yml, or wherever other Metabase env vars are defined)Specify the exact code for the new endpoint:
METABASE_JWT_SHARED_SECRETThe endpoint must:
METABASE_JWT_SHARED_SECRET from environmentemail, first_name, last_name, groups, expThe endpoint must NOT:
Use the docs fetched in Step 1a as the authoritative source for auth config fields, function signatures, and deprecated options for the target Metabase version. Config fields, parameter signatures, and deprecated options change across versions — do not assume any specific field exists without confirming it in the docs.
Modular embedding (embed.js):
window.metabaseConfig supports in the detected version (e.g., jwtProviderUri may or may not be available).Modular embedding SDK:
defineMetabaseAuthConfig options and the fetchRequestToken function signature — especially whether it receives parameters or not.fetchRequestToken body unless the docs say otherwise.authProviderUri was removed in later versions). The docs for the detected version are the source of truth for what fields are valid.Full App embedding:
src to route through the SSO endpoint or Metabase's /auth/sso path with a return_to parameter containing the URL-encoded destination path (e.g., %2Fdashboard%2F1).Remove any auth config fields that are not listed in the detected version's docs. Common examples:
apiKey — API keys are for local dev only, not production SSOuseExistingUserSession — uses the admin browser session, not for productionList these as part of the plan — they will be included in the final summary:
METABASE_JWT_SHARED_SECREThttp://localhost:9090/sso/metabase) — check the fetched docs to determine whether this is required or optional for the detected version (it depends on whether the frontend config supports a JWT provider field).Apply all changes from Step 3 in this order:
Constraints:
old_string / new_string for every change to existing filesPerform all of these checks. Each check should have an explicit pass/fail result.
Read the SSO endpoint file. Verify:
email, first_name, last_name, groups, expMETABASE_JWT_SHARED_SECRET from environmentPass criteria: endpoint is complete and auth-protected.
Verify the frontend auth config uses only fields listed in the detected version's docs. No deprecated or removed fields should remain.
window.metabaseConfig auth fields match what the docs supportdefineMetabaseAuthConfig options and fetchRequestToken signature match the docsPass criteria: frontend auth matches the embedding type and detected version docs.
Use Grep to search for useExistingUserSession across all project files (excluding node_modules, .git). Also search for apiKey near metabaseConfig or defineMetabaseAuthConfig — a bare apiKey grep is too broad and will match unrelated code. Also verify no deprecated config fields remain (compare against the detected version docs).
Pass criteria: no Metabase-specific development-only or deprecated auth methods remain.
Check that the JWT library is in the project's dependencies (e.g., package.json, requirements.txt).
Pass criteria: library is listed or was already present.
Verify the endpoint does NOT:
METABASE_JWT_SHARED_SECRETPass criteria: all security checks pass.
If any check fails:
Organize the final output into these sections:
| Metabase field | Source | Example value |
|---|---|---|
| email | req.user.email | "[email protected]" |
| first_name | req.user.firstName | "Jane" |
| last_name | req.user.lastName | "Doe" |
| groups | [req.user.role] | ["Analyst"] |
| exp | Date.now()/1000 + 600 | 1700000600 |
{middleware} — only authenticated users can obtain a Metabase JWTMETABASE_JWT_SHARED_SECRET must be kept secret and match the value in Metabase adminDoc fetching:
llms-embedding-full.txt returns 404, verify the Metabase version number and retry. If still failing, mark Step 1 ❌ blocked.Validation:
tools
Drive a Metabase instance from the terminal via the `mb` CLI. Authenticate with named profiles; inspect databases (list, get, full metadata rollup, schemas, tables in a schema) and trigger manual schema sync / field-values rescan; inspect tables, fields; list/get/create/update/archive cards (questions, models, metrics) and run them as JSON/CSV/XLSX; list/get/create/update dashboards and patch dashcards; list/get/create collections and traverse the hierarchy by id, entity_id, or "root"/"trash" (with items and recursive tree); list/get/create/update/archive native query snippets, segments, and measures; author/update/run transforms and schedule transform-jobs; read/update settings; search content (cards, dashboards, collections, transforms, metrics); manage Enterprise workspaces; git-sync to/from a git remote (status, dirty, import, export, branches, stash, add/remove a collection from sync). Use whenever the user wants to interact with a Metabase from the terminal — "log into metabase", "what profiles do I have", "list cards", "run card 42 as CSV", "create a transform", "list dashboards", "move a dashcard", "list collections", "what's in collection 4", "show the collection tree", "list snippets", "create a segment", "archive a measure", "search metabase for X", "spin up a workspace", "import the latest changes", "add a directory to git sync", "set a setting", "what schemas are in this database", "trigger a sync", "rescan field values", or anything hitting `mb <verb>`.
development
Runs the Metabase semantic checker against a tree of Representation Format YAML files to verify that all references resolve — cross-entity references (collection_id, dashboard_id, parent_id, parameter source cards, snippet references, transform tags, etc.) and references to columns inside MBQL and native queries. Slow (≥1 min per run). Only use when the user explicitly asks to verify entity references or column references in MBQL/SQL queries; in most cases this runs as a CI step, not locally. Requires database metadata on disk (by default `.metadata/table_metadata.json`).
development
Understands the Metabase Database Metadata Format — a YAML-based on-disk representation of databases, tables, and fields synced from a Metabase instance. Use when the user needs to read, edit, or understand metadata files produced by `@metabase/database-metadata`, or when reasoning about a project's schema (columns, types, FK relationships) through the `.metadata/databases` folder.
development
Understands the Metabase Representation Format — a YAML-based serialization format for Metabase content (collections, cards, dashboards, documents, segments, measures, snippets, transforms). Use when the user needs to create, edit, understand, or validate Metabase representation YAML files, or when working with Metabase serialization/deserialization (serdes). Covers entity schemas, MBQL and native queries, visualization settings, parameters, and folder structure.