Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.docker.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
PREMIUM_EMBEDDING_TOKEN="<your_enterprise_token>"
METABASE_JWT_SHARED_SECRET="ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
METABASE_STATIC_EMBEDDING_SECRET="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
METABASE_ADMIN_API_KEY="mb_NgLv2g7CFm1VDIhfensgdMhLPYvvKIz3cNxrW99/Cro="
METABASE_ADMIN_EMAIL="rene@example.com"
METABASE_ADMIN_FIRST_NAME="Rene"
METABASE_ADMIN_LAST_NAME="Descartes"
METABASE_ADMIN_PASSWORD="foobarbaz"
METABASE_DASHBOARD_ID_TO_EMBED=1

MB_PORT=4300
SERVER_PORT=4400
Expand Down
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ PORT=3100
METABASE_INSTANCE_URL="http://localhost:3000"
METABASE_JWT_SHARED_SECRET="<shared_secret>"
METABASE_STATIC_EMBEDDING_SECRET="<static_embedding_secret>"
METABASE_ADMIN_EMAIL="rene@example.com"
METABASE_ADMIN_FIRST_NAME="Rene"
METABASE_ADMIN_LAST_NAME="Descartes"
METABASE_ADMIN_PASSWORD="foobarbaz"
METABASE_DASHBOARD_ID_TO_EMBED=1
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ COPY server.js ./

RUN npm ci

CMD ["npm", "run", "start"]
CMD ["node", "server.js"]
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,13 @@ npm run docker:up

### Running locally with an existing Metabase instance

Before running, ensure your Metabase instance is configured:

- Guest and modular embedding must be enabled in Admin > Embedding settings
- Static embedding secret key must be set
- JWT authentication must be enabled with a shared secret
- A dashboard must be published via the Embed JS editor
Before running, ensure your Metabase instance is configured following the [guest embedding setup guide](https://www.metabase.com/docs/latest/embedding/guest-embedding) and [SSO authentication guide](https://www.metabase.com/docs/latest/embedding/authentication).

1. Copy the environment file:

```bash
cp .env.example .env
# Edit .env with your Metabase instance URL and JWT secrets
# Edit .env with your Metabase instance URL, JWT secrets, and dashboard ID
```

2. Install dependencies and start the server:
Expand All @@ -56,9 +51,8 @@ cp .env.example .env
npm install
npm start
```
3. On your instance open any dashboard and publish it by going through EmbedJS Wizard => Guest embed setup

4. Open http://localhost:3100 in your browser
3. Open http://localhost:3100 in your browser

## Project Structure

Expand All @@ -76,7 +70,7 @@ npm start

### Guest Embed (Signed JWT)

Guest embeds use signed JWT tokens to provide anonymous access to specific dashboards or questions. The server signs a token containing the resource ID and parameters, which is then passed to the `<metabase-dashboard>` component.
Guest embeds use signed JWT tokens to provide anonymous access to specific dashboards or questions. The server signs a token containing the resource ID and parameters, which is then passed to the `<metabase-dashboard>` component. See the [guest embedding docs](https://www.metabase.com/docs/latest/embedding/guest-embedding) for more details.

```javascript
// Server generates a signed token
Expand All @@ -102,7 +96,7 @@ const token = jwt.sign(payload, METABASE_STATIC_EMBEDDING_SECRET);

### SSO Embed (JWT Authentication)

SSO embeds authenticate users via JWT, creating a full user session in Metabase. The server returns a JWT containing user information when the embed.js client requests authentication.
SSO embeds authenticate users via JWT, creating a full user session in Metabase. The server returns a JWT containing user information when the embed.js client requests authentication. See the [authentication docs](https://www.metabase.com/docs/latest/embedding/authentication) for more details.

```javascript
// Server returns user JWT at /auth/sso endpoint
Expand Down Expand Up @@ -140,6 +134,7 @@ res.json({ jwt: ssoToken });
| `METABASE_INSTANCE_URL` | Metabase URL | `http://localhost:3000` |
| `METABASE_JWT_SHARED_SECRET` | JWT signing secret for SSO authentication | - |
| `METABASE_STATIC_EMBEDDING_SECRET` | JWT signing secret for guest embeds | - |
| `METABASE_DASHBOARD_ID_TO_EMBED` | ID of the dashboard to embed | `1` |
| `PREMIUM_EMBEDDING_TOKEN` | Metabase Enterprise license | - |

## Running E2E Tests
Expand Down
12 changes: 11 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ services:
MB_RUN_MODE: "${MB_RUN_MODE}"
METASTORE_DEV_SERVER_URL: "${METASTORE_DEV_SERVER_URL}"
MB_JWT_IDENTITY_PROVIDER_URI: "http://localhost:${SERVER_PORT}/auth/sso"
METABASE_ADMIN_EMAIL: "${METABASE_ADMIN_EMAIL}"
METABASE_ADMIN_FIRST_NAME: "${METABASE_ADMIN_FIRST_NAME}"
METABASE_ADMIN_LAST_NAME: "${METABASE_ADMIN_LAST_NAME}"
METABASE_ADMIN_PASSWORD: "${METABASE_ADMIN_PASSWORD}"
healthcheck:
test: curl --fail -X GET -I "http://localhost:${MB_PORT}/api/health" || exit 1
interval: 15s
Expand All @@ -30,7 +34,9 @@ services:
condition: service_healthy
environment:
METABASE_INSTANCE_URL: "http://metabase:${MB_PORT}"
METABASE_ADMIN_API_KEY: "${METABASE_ADMIN_API_KEY}"
METABASE_ADMIN_EMAIL: "${METABASE_ADMIN_EMAIL}"
METABASE_ADMIN_PASSWORD: "${METABASE_ADMIN_PASSWORD}"
METABASE_DASHBOARD_ID_TO_EMBED: "${METABASE_DASHBOARD_ID_TO_EMBED}"
volumes:
- ./metabase/setup.sh:/setup.sh:ro
entrypoint: ["/bin/sh", "/setup.sh"]
Expand All @@ -49,6 +55,10 @@ services:
METABASE_INSTANCE_URL: "http://localhost:${MB_PORT}"
METABASE_JWT_SHARED_SECRET: "${METABASE_JWT_SHARED_SECRET}"
METABASE_STATIC_EMBEDDING_SECRET: "${METABASE_STATIC_EMBEDDING_SECRET}"
METABASE_DASHBOARD_ID_TO_EMBED: "${METABASE_DASHBOARD_ID_TO_EMBED}"
METABASE_ADMIN_EMAIL: "${METABASE_ADMIN_EMAIL}"
METABASE_ADMIN_FIRST_NAME: "${METABASE_ADMIN_FIRST_NAME}"
METABASE_ADMIN_LAST_NAME: "${METABASE_ADMIN_LAST_NAME}"
healthcheck:
test: curl --fail -X GET -I "http://localhost:${SERVER_PORT}/" || exit 1
interval: 2s
Expand Down
3 changes: 2 additions & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"version": "0.0.0",
"devDependencies": {
"@testing-library/cypress": "^10.0.3",
"cypress": "^14.0.3"
"cypress": "^14.0.3",
"env-cmd": "^10.1.0"
},
"scripts": {
"cypress:open": "env-cmd -f ../.env.docker cypress open --config-file ./support/cypress.config.js --e2e",
Expand Down
13 changes: 4 additions & 9 deletions metabase/config.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
version: 1
config:
users:
- first_name: Rene
last_name: Descartes
password: foobarbaz
email: rene@example.com
api-keys:
- name: main
key: "mb_NgLv2g7CFm1VDIhfensgdMhLPYvvKIz3cNxrW99/Cro="
group: admin
creator: rene@example.com
- first_name: "{{ env METABASE_ADMIN_FIRST_NAME }}"
last_name: "{{ env METABASE_ADMIN_LAST_NAME }}"
password: "{{ env METABASE_ADMIN_PASSWORD }}"
email: "{{ env METABASE_ADMIN_EMAIL }}"
settings:
enable-embedding-simple: true
enable-embedding-static: true
Expand Down
31 changes: 23 additions & 8 deletions metabase/setup.sh
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
#!/bin/bash
set -e

METABASE_INSTANCE_URL="${METABASE_INSTANCE_URL}"
METABASE_ADMIN_API_KEY="${METABASE_ADMIN_API_KEY}"

echo "Waiting for Metabase to be ready..."
until curl -sf "${METABASE_INSTANCE_URL}/api/health" > /dev/null 2>&1; do
sleep 2
done
echo "Metabase is ready!"

echo "Enabling static embedding for dashboard 1..."
curl -sf -X PUT "${METABASE_INSTANCE_URL}/api/dashboard/1" \
echo "Getting session token..."
SESSION_RESPONSE=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "{\"username\": \"${METABASE_ADMIN_EMAIL}\", \"password\": \"${METABASE_ADMIN_PASSWORD}\"}" \
"${METABASE_INSTANCE_URL}/api/session")

SESSION_ID=$(echo "$SESSION_RESPONSE" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')

if [ -z "$SESSION_ID" ]; then
echo "Failed to get session token. Response: $SESSION_RESPONSE"
exit 1
fi

echo "Enabling static embedding for dashboard ${METABASE_DASHBOARD_ID_TO_EMBED}..."
RESPONSE=$(curl -s -X PUT "${METABASE_INSTANCE_URL}/api/dashboard/${METABASE_DASHBOARD_ID_TO_EMBED}" \
-H "Content-Type: application/json" \
-H "X-Api-Key: ${METABASE_ADMIN_API_KEY}" \
-d '{"enable_embedding": true}'
-H "X-Metabase-Session: ${SESSION_ID}" \
-d '{"enable_embedding": true}')

if echo "$RESPONSE" | grep -q '"error"'; then
echo "Failed to enable embedding. Response: $RESPONSE"
exit 1
fi

echo "Static embedding enabled for dashboard 1!"
echo "Static embedding enabled for dashboard ${METABASE_DASHBOARD_ID_TO_EMBED}!"
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"docker:rm": "yarn docker:down --rmi all --volumes"
},
"dependencies": {
"cors": "^2.8.5",
"env-cmd": "^10.1.0",
"express": "^4.21.0",
"jsonwebtoken": "^9.0.2"
Expand Down
38 changes: 24 additions & 14 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import express from "express";
import cors from "cors";
import jwt from "jsonwebtoken";

const app = express();
const port = process.env.PORT;

app.use(cors({ credentials: true, origin: true }));

const METABASE_JWT_SHARED_SECRET = process.env.METABASE_JWT_SHARED_SECRET;
const METABASE_STATIC_EMBEDDING_SECRET =
process.env.METABASE_STATIC_EMBEDDING_SECRET;

const METABASE_SITE_URL =
process.env.METABASE_INSTANCE_URL;
const METABASE_SITE_URL = process.env.METABASE_INSTANCE_URL;
const METABASE_DASHBOARD_ID_TO_EMBED = process.env.METABASE_DASHBOARD_ID_TO_EMBED
? parseInt(process.env.METABASE_DASHBOARD_ID_TO_EMBED)
: 1;

const METABASE_ADMIN_EMAIL = process.env.METABASE_ADMIN_EMAIL || "rene@example.com";
const METABASE_ADMIN_FIRST_NAME = process.env.METABASE_ADMIN_FIRST_NAME || "Rene";
const METABASE_ADMIN_LAST_NAME = process.env.METABASE_ADMIN_LAST_NAME || "Descartes";

const JWT_PROVIDER_URI = `http://localhost:${port}/auth/sso`;

Expand Down Expand Up @@ -59,12 +68,19 @@ app.get("/auth/sso", (req, res) => {
// Example:
// const { user } = req.session;
const user = {
email: "rene@example.com",
firstName: "Rene",
lastName: "Descartes",
group: "Customer",
email: METABASE_ADMIN_EMAIL,
firstName: METABASE_ADMIN_FIRST_NAME,
lastName: METABASE_ADMIN_LAST_NAME,
group: 'Customer',
};

if (!user) {
return res.status(401).json({
status: "error",
message: "Not authenticated",
});
}

const ssoPayload = {
email: user.email,
first_name: user.firstName,
Expand All @@ -74,20 +90,14 @@ app.get("/auth/sso", (req, res) => {
};

const ssoToken = jwt.sign(ssoPayload, METABASE_JWT_SHARED_SECRET);
const origin = req.headers.origin || "*";

res.set({
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
});

res.json({ jwt: ssoToken });
});

// Guest Embed - uses signed JWT tokens for anonymous access
app.get(["/", "/guest-embed"], (req, res) => {
const payload = {
resource: { dashboard: 1 },
resource: { dashboard: METABASE_DASHBOARD_ID_TO_EMBED },
params: {},
exp: Math.round(Date.now() / 1000) + 10 * 60, // 10 minutes
};
Expand All @@ -111,7 +121,7 @@ app.get("/sso-embed", (req, res) => {
jwtProviderUri: JWT_PROVIDER_URI,
enableInternalNavigation: true,
},
`<metabase-dashboard dashboard-id="1" with-title="true" with-downloads="false"></metabase-dashboard>`,
`<metabase-dashboard dashboard-id="${METABASE_DASHBOARD_ID_TO_EMBED}" with-title="true" with-downloads="false"></metabase-dashboard>`,
"/sso-embed",
),
);
Expand Down