Embedded analytics SDK - authentication

Embedded analytics SDK is only available on Pro and Enterprise plans (both self-hosted and on Metabase Cloud). You can, however, play around with the SDK on your local machine without a license by using API keys to authenticate your embeds.

For using the SDK in production, you’ll need to set up authentication with SSO.

If you’re developing locally, you can also set up authentication with API keys.

Setting up JWT SSO

To set up JWT SSO, you’ll need a Metabase Pro or Enterprise license (If you don’t have a license, check out this quickstart)

Here’s a high-level overview:

  1. Enable JWT SSO in your Metabase
  2. Add a new endpoint to your backend to handle authentication
  3. Wire the SDK in your frontend to your new endpoint

1. Enable JWT SSO in your Metabase

  1. Configure JWT by going to Admin Settings > Settings > Authentication and clicking on Setup
  2. Generate a key and copy it to your clipboard.

2. Add a new endpoint to your backend to handle authentication

You’ll need add a library to your backend to sign your JSON Web Tokens.

For Node.js, we recommend jsonwebtoken:

npm install jsonwebtoken --save

Next, set up an endpoint on your backend (e.g., /sso/metabase) that uses your Metabase JWT shared secret to generate a JWT for the authenticated user. This endpoint must return a JSON object with a jwt property containing the signed JWT. For example: { "jwt": "your-signed-jwt" }.

This example code for Node.js sets up an endpoint using Express:

import express from "express";
import cors from "cors";
import session from "express-session";
import jwt from "jsonwebtoken";
import fetch from "node-fetch";

// Replace this with your Metabase URL
const METABASE_INSTANCE_URL = "YOUR_METABASE_URL_HERE";
// Replace this with the JWT signing secret you generated when enabling
// JWT SSO in your Metabase.
const METABASE_JWT_SHARED_SECRET = "YOUR_SECRET_HERE";

const app = express();

app.get("/sso/metabase", async (req, res) => {
  // Usually, you would grab the user from the current session
  // Here it's hardcoded for demonstration purposes
  // Example:
  // const { user } = req.session;
  const user = {
    email: "rene@example.com",
    firstName: "Rene",
    lastName: "Descartes",
    group: "Customer",
  };

  if (!user) {
    console.log("no user");
    res.status(401).json({
      status: "error",
      message: "not authenticated",
    });

    return;
  }

  const token = jwt.sign(
    {
      email: user.email,
      first_name: user.firstName,
      last_name: user.lastName,
      groups: [user.group],
      exp: Math.round(Date.now() / 1000) + 60 * 10, // 10 minutes expiration
    },
    METABASE_JWT_SHARED_SECRET,
  );
  // The user backend should return a JSON object with the JWT.
  res.status(200).json({ jwt: token });
});

Example using Next.js App Router:

import jwt from "jsonwebtoken";

const user = {
  email: "rene@example.com",
  firstName: "Rene",
  lastName: "Descartes",
  group: "Customer",
};

const METABASE_JWT_SHARED_SECRET = process.env.METABASE_JWT_SHARED_SECRET || "";
const METABASE_INSTANCE_URL = process.env.METABASE_INSTANCE_URL || "";

export async function GET() {
  const token = jwt.sign(
    {
      email: user.email,
      first_name: user.firstName,
      last_name: user.lastName,
      groups: [user.group],
      exp: Math.round(Date.now() / 1000) + 60 * 10, // 10 minutes expiration
    },
    // This is the JWT signing secret in your Metabase JWT authentication setting
    METABASE_JWT_SHARED_SECRET,
  );
  // The user backend should return a JSON object with the JWT.
  return Response.json({ jwt: token });
}

Example using Next.js Pages Router:

import type { NextApiRequest, NextApiResponse } from "next";
import jwt from "jsonwebtoken";

const user = {
  email: "rene@example.com",
  firstName: "Rene",
  lastName: "Descartes",
  group: "Customer",
};

const METABASE_JWT_SHARED_SECRET = process.env.METABASE_JWT_SHARED_SECRET || "";
const METABASE_INSTANCE_URL = process.env.METABASE_INSTANCE_URL || "";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  const token = jwt.sign(
    {
      email: user.email,
      first_name: user.firstName,
      last_name: user.lastName,
      groups: [user.group],
      exp: Math.round(Date.now() / 1000) + 60 * 10, // 10 minutes expiration
    },
    // This is the JWT signing secret in your Metabase JWT authentication setting
    METABASE_JWT_SHARED_SECRET,
  );
  // The user backend should return a JSON object with the JWT.
  res.status(200).json({ jwt: token });
}

Handling interactive and SDK embeds with the same endpoint

If you have an existing backend endpoint configured for interactive embedding and want to use the same endpoint for SDK embedding, you can differentiate between the requests by checking for the response=json query parameter that the SDK adds to its requests.

  • For SDK requests, you should return a JSON object with the JWT ({ jwt: string }).
  • For interactive embedding requests, you would proceed with the redirect.

Here’s an example of an Express.js endpoint that handles both:

import express from "express";
import jwt from "jsonwebtoken";

// Replace this with your Metabase URL
const METABASE_INSTANCE_URL = "YOUR_METABASE_URL_HERE";
// Replace this with the JWT signing secret you generated when enabling
// JWT SSO in your Metabase.
const METABASE_JWT_SHARED_SECRET = "YOUR_SECRET_HERE";

const app = express();

app.get("/sso/metabase", async (req, res) => {
  // This is an example endpoint that can handle both traditional interactive
  // embedding requests and SDK embedding requests.

  // Detect if the request is coming from the SDK by checking for the
  // 'response=json' query parameter added by the SDK.
  const isSdkRequest = req.query.response === "json";

  // Usually, you would grab the user from the current session
  // Here it's hardcoded for demonstration purposes
  // Example:
  // const { user } = req.session;
  const user = {
    email: "rene@example.com",
    firstName: "Rene",
    lastName: "Descartes",
    group: "Customer",
  };

  // Generate the JWT
  const token = jwt.sign(
    {
      email: user.email,
      first_name: user.firstName,
      last_name: user.lastName,
      groups: [user.group],
      exp: Math.round(Date.now() / 1000) + 60 * 10, // 10 minutes expiration
    },
    METABASE_JWT_SHARED_SECRET,
  );

  if (isSdkRequest) {
    // For SDK requests, return a JSON object with the JWT.
    res.status(200).json({ jwt: token });
  } else {
    // For interactive embedding, construct the Metabase SSO URL
    // and redirect the user's browser to it.
    const ssoUrl = `${METABASE_INSTANCE_URL}/auth/sso?token=true&jwt=${token}`;
    res.redirect(ssoUrl);
  }
});

3. Wire the SDK in your frontend to your new endpoint

Update the SDK config in your frontend code to point your backend’s authentication endpoint.

const authConfig = defineMetabaseAuthConfig({
  metabaseInstanceUrl: "https://your-metabase.example.com", // Required: Your Metabase instance URL
});

(Optional) If you use headers instead of cookies to authenticate calls from your frontend to your backend, you’ll need to use a custom fetch function.

If your frontend and backend don’t share a domain, you need to enable CORS

You can add some middleware in your backend to handle cross-domain requests.

// Middleware

// If your FE application is on a different domain from your BE, you need to enable CORS
// by setting Access-Control-Allow-Credentials to true and Access-Control-Allow-Origin
// to your FE application URL.
//
// Limitation: We currently only support setting one origin in Authorized Origins in Metabase for CORS.
app.use(
  cors({
    credentials: true,
  }),
);

app.use(
  session({
    secret: SESSION_SECRET,
    resave: false,
    saveUninitialized: true,
    cookie: { secure: false },
  }),
);

app.use(express.json());

// routes
app.get("/sso/metabase", metabaseAuthHandler);
app.listen(PORT, () => {
  console.log(`API running at http://localhost:${PORT}`);
});

Customizing JWT authentication

You can customize how the SDK fetches the refresh token by specifying the fetchRefreshToken function with the defineMetabaseAuthConfig function:

// Pass this configuration to MetabaseProvider.
// Wrap the fetchRequestToken function in useCallback if it has dependencies to prevent re-renders.
const authConfig = defineMetabaseAuthConfig({
  fetchRequestToken: async () => {
    const response = await fetch(
      "https://{{ YOUR_CLIENT_HOST }}/api/metabase/auth",
      {
        method: "GET",
        headers: { Authorization: `Bearer ${yourToken}` },
      },
    );

    // The backend should return a JSON object with the shape { jwt: string }
    return await response.json();
  },
  metabaseInstanceUrl: "http://localhost:3000",
});

The response should be in the form of { jwt: "{JWT_TOKEN}" }

Authenticating with SAML SSO

SAML authentication is only available on Pro and Enterprise plans (both self-hosted and on Metabase Cloud). You can, however, play around with the SDK on your local machine without a license by using API keys to authenticate your embeds.

To use SAML single sign-on with the Embedded analytics SDK, you’ll need to set up SAML in both your Metabase and your Identity Provider (IdP). See the docs on SAML-based authentication.

Once SAML is configured in Metabase and your IdP, you can configure the SDK to use SAML by setting the preferredAuthMethod in your MetabaseAuthConfig to "saml":

// Pass this configuration to MetabaseProvider.
const authConfig = defineMetabaseAuthConfig({
  metabaseInstanceUrl: "http://localhost:3000",
  preferredAuthMethod: "saml",
});

Using SAML authentication with the Embedded analytics SDK will typically involve redirecting people to a popup with your Identity Provider’s login page for authentication. After successful authentication, the person will be redirected back to the embedded content.

Due to the nature of redirects and popups involved in the SAML flow, SAML authentication with the SDK may not work seamlessly in all embedding contexts, particularly within iframes, depending on browser security policies and your IdP’s configuration. We recommend testing auth flows in your target environments.

Unlike JWT authentication, you won’t be able to implement a custom fetchRequestToken function on your backend when pairing SAML with the SDK.

If both SAML and JWT are enabled, the SDK will default to SAML

You can override this default behavior to prefer the JWT authentication method by setting preferredAuthMethod="jwt" in your authentication config:

authConfig: {
  metabaseInstanceUrl: "...",
  preferredAuthMethod: "jwt",
  // other JWT config...
}

Getting Metabase authentication status

You can query the Metabase authentication status using the useMetabaseAuthStatus hook. This is useful if you want to completely hide Metabase components when the user is not authenticated.

This hook can only be used within components wrapped by MetabaseProvider.

const auth = useMetabaseAuthStatus();

if (auth.status === "error") {
  return <div>Failed to authenticate: {auth.error.message}</div>;
}

if (auth.status === "success") {
  return <InteractiveQuestion questionId={110} />;
}

Authenticating locally with API keys

The Embedded analytics SDK only supports JWT authentication in production. Authentication with API keys is only supported for local development and evaluation purposes.

For developing locally to try out the SDK, you can authenticate using an API key.

First, create an API key.

Then you can then use the API key to authenticate with Metabase in your application. All you need to do is include your API key in the config object using the key: apiKey.

import {
  MetabaseProvider,
  defineMetabaseAuthConfig,
} from "@metabase/embedding-sdk-react";

const authConfigApiKey = defineMetabaseAuthConfig({
  metabaseInstanceUrl: "https://metabase.example.com",
  apiKey: "YOUR_API_KEY",
});

export default function App() {
  return (
    <MetabaseProvider authConfig={authConfigApiKey} className="optional-class">
      Hello World!
    </MetabaseProvider>
  );
}

Security warning: each end-user must have their own Metabase account

Each end-user must have their own Metabase account.

The problem with having end-users share a Metabase account is that, even if you filter data on the client side via the SDK, all end-users will still have access to the session token, which they could use to access Metabase directly via the API to get data they’re not supposed to see.

If each end-user has their own Metabase account, however, you can configure permissions in Metabase and everyone will only have access to the data they should.

In addition to this, we consider shared accounts to be unfair usage. Fair usage of the SDK involves giving each end-user of the embedded analytics their own Metabase account.

Upgrade guide for JWT SSO setups on SDK version 54 or below

If you’re upgrading from an SDK version 1.54.x or below and you’re using JWT SSO, you’ll need to make the following changes.

Frontend changes:

Backend changes:

Additionally, if you have SAML set up, but you’d prefer to use JWT SSO, you’ll need to set a preferred authentication method.

Remove authProviderUri from your auth config

defineMetabaseAuthConfig no longer accepts an authProviderUri parameter, so you’ll need to remove it.

Admin setting changes in Metabase:

In Admin Settings > Authentication > JWT SSO, set the JWT Identity Provider URI to the URL of your JWT SSO endpoint, e.g., http://localhost:9090/sso/metabase.

Before:

const authConfig = defineMetabaseAuthConfig({
  metabaseInstanceUrl: "https://your-metabase.example.com",
  authProviderUri: "http://localhost:9090/sso/metabase", // Remove this line
});

After:

const authConfig = defineMetabaseAuthConfig({
  metabaseInstanceUrl: "https://your-metabase.example.com",
});

The SDK now uses the JWT Identity Provider URI setting configured in your Metabase admin settings (Admin > Settings > Authentication > JWT).

Update the fetchRequestToken function signature

The fetchRequestToken function no longer receives a URL parameter. You must now specify your authentication endpoint directly in the function.

Before:

const authConfig = defineMetabaseAuthConfig({
  fetchRequestToken: async (url) => {
    // Remove url parameter
    const response = await fetch(url, {
      method: "GET",
      headers: { Authorization: `Bearer ${yourToken}` },
    });
    return await response.json();
  },
  metabaseInstanceUrl: "http://localhost:3000",
  authProviderUri: "http://localhost:9090/sso/metabase", // Remove this line
});

After:

const authConfig = defineMetabaseAuthConfig({
  fetchRequestToken: async () => {
    // No parameters
    const response = await fetch("http://localhost:9090/sso/metabase", {
      // Hardcode your endpoint URL
      method: "GET",
      headers: { Authorization: `Bearer ${yourToken}` },
    });
    return await response.json();
  },
  metabaseInstanceUrl: "http://localhost:3000",
});

Update your JWT endpoint to handle SDK requests

Your JWT endpoint must now handle both SDK requests and interactive embedding requests. The SDK adds a response=json query parameter to distinguish its requests. For SDK requests, return a JSON object with the JWT. For interactive embedding, continue redirecting as before.

If you were using a custom fetchRequestToken, you’ll need to update the endpoint to detect req.query.response === "json" for SDK requests.

app.get("/sso/metabase", async (req, res) => {
  // SDK requests include 'response=json' query parameter
  const isSdkRequest = req.query.response === "json";

  const user = getCurrentUser(req);

  const token = jwt.sign(
    {
      email: user.email,
      first_name: user.firstName,
      last_name: user.lastName,
      groups: [user.group],
      exp: Math.round(Date.now() / 1000) + 60 * 10,
    },
    METABASE_JWT_SHARED_SECRET,
  );

  if (isSdkRequest) {
    // For SDK requests, return JSON object with jwt property
    res.status(200).json({ jwt: token });
  } else {
    // For interactive embedding, redirect as before
    const ssoUrl = `${METABASE_INSTANCE_URL}/auth/sso?token=true&jwt=${token}`;
    res.redirect(ssoUrl);
  }
});

Read docs for other versions of Metabase.