Embedding Looker content in your own application is one of the platform's strongest use cases and one of the easiest to get wrong. This tutorial covers the three embedding modes, the current Embed SDK (2.x), using user attributes for row-level security, and cookieless embedding for browsers that no longer accept third-party cookies.
The three modes
Private embedding puts a Looker URL in an iframe and relies on the viewer already being logged into Looker. It is fine for an internal portal where everyone has a Looker account; it is not for customers.
Signed embedding (often still called SSO embedding) is the standard for customer-facing analytics. Your server generates a signed URL containing the embed user's identity, permissions, models, group and user attributes, signed with the instance's embed secret. Looker creates or updates the embed user on the fly and renders the content. No Looker login, no Looker account provisioning.
OIDC or SAML SSO embedding is a variant where the embed iframe authenticates through your identity provider. Useful when embed users are also regular Looker users; most product embeds use signed embedding instead.
Turn on embedding in Admin > Platform > Embed: enable Embed SSO Authentication, generate the embed secret (store it in your secret manager; it is shown once), and add your application's origin to the Embed Domain Allowlist.
Generating a signed embed URL
The signed URL is built server side. Google publishes reference implementations in several languages; the Python one is in the looker-open-source/embed-sdk repository. The parameters that matter:
# server.py (abbreviated; see the official reference implementation for the full signer)
from looker_embed_signer import sign_embed_url # your copy of the reference signer
url = sign_embed_url(
host="looker.example.com",
secret=EMBED_SECRET,
external_user_id=f"customer-{customer.id}-user-{user.id}",
first_name=user.first_name,
last_name=user.last_name,
permissions=["access_data", "see_looks", "see_user_dashboards", "see_lookml_dashboards",
"explore", "download_with_limit"],
models=["customer_analytics"],
group_ids=[],
external_group_id=f"customer-{customer.id}",
user_attributes={"customer_id": str(customer.id), "locale": user.locale},
session_length=3600,
force_logout_login=True,
embed_url="/embed/dashboards/42",
)
Key decisions:
external_user_idmust be stable per person; it is how Looker maps repeat visits to the same embed user.external_group_idgroups embed users per customer, which gives each tenant its own shared folder.permissionsshould be the minimum. Leave outexploreanddownload_without_limitunless the product needs them.user_attributescarry the values row-level security depends on (next section). Never let the browser supply these; they come from your server's session.force_logout_login: trueprevents a previous user's Looker session in the same browser from being reused.
Row-level security with user attributes
The embed user's customer_id attribute does nothing until the model enforces it. In the explore:
explore: orders {
access_filter: {
field: orders.customer_id
user_attribute: customer_id
}
}
Every query through this explore gains WHERE orders.customer_id = <attribute value>. Define the user attribute in Admin > Users > User Attributes with User Access: None so embed users cannot see or edit it, and with no default value, so a missing attribute yields no rows rather than all rows. For stricter cases add sql_always_where referencing {{ _user_attributes['customer_id'] }}, and hide sensitive fields behind required_access_grants. The full set of patterns is in Row-Level Security Patterns.
Test the boundary explicitly: generate an embed URL for customer A and try to reach customer B's dashboard and data. The dashboard URL might load (content access is by folder and group), but the data must be empty.
The Embed SDK 2.x in the browser
You can drop the signed URL straight into an iframe, but the Embed SDK (@looker/embed-sdk) handles iframe creation, sizing, events, and two-way communication (filters, theming, navigation) for you. Version 2 changed the initialization API: getEmbedSDK() replaces the static LookerEmbedSDK and a single connection can load dashboards, looks and explores.
npm install @looker/embed-sdk
import { getEmbedSDK } from "@looker/embed-sdk";
const sdk = getEmbedSDK();
// The auth URL is YOUR server endpoint that returns a signed embed URL for the logged-in user.
sdk.init("looker.example.com", "/api/looker/auth");
const connection = await sdk
.createDashboardWithId("42")
.appendTo("#analytics")
.withTheme("customer_portal")
.withFilters({ "Date Range": "30 days" })
.on("dashboard:loaded", () => console.log("loaded"))
.on("dashboard:run:complete", (e) => console.log("queries finished", e))
.build()
.connect();
// Later, without creating a new iframe:
await connection.loadDashboard("57");
connection.updateFilters({ "Date Range": "90 days" });
Your /api/looker/auth endpoint receives the SDK's request (which includes the intended embed path), checks your application session, calls the signer from the previous section, and returns the signed URL. The secret never reaches the browser. The SDK also exposes withDynamicIFrameHeight() so the iframe grows with the dashboard, and withAllowAttr("fullscreen") when you need it.
Cookieless embedding
Signed embedding relies on Looker setting a session cookie inside the iframe. Safari has blocked third-party cookies for years and Chrome has shipped its own restrictions, so on many browsers a classic signed embed on a different domain from Looker simply fails to keep a session. Cookieless embedding replaces the cookie with short-lived tokens that your server acquires from Looker and hands to the SDK.
Server side, two endpoints: one that acquires a session (acquire_embed_cookieless_session, with the same user payload as a signed URL) and one that refreshes tokens (generate_tokens_for_cookieless_session). With the Python SDK:
import looker_sdk
from looker_sdk import models40 as models
sdk = looker_sdk.init40()
def acquire_session(user):
body = models.EmbedCookielessSessionAcquire(
external_user_id=f"customer-{user.customer_id}-user-{user.id}",
first_name=user.first_name,
last_name=user.last_name,
permissions=["access_data", "see_looks", "see_user_dashboards"],
models=["customer_analytics"],
external_group_id=f"customer-{user.customer_id}",
user_attributes={"customer_id": str(user.customer_id)},
session_length=3600,
embed_domain="https://app.example.com",
)
return sdk.acquire_embed_cookieless_session(body) # returns tokens + session_reference_token
def generate_tokens(session_reference_token, api_token, navigation_token):
return sdk.generate_tokens_for_cookieless_session(
models.EmbedCookielessSessionGenerateTokens(
session_reference_token=session_reference_token,
api_token=api_token,
navigation_token=navigation_token,
)
)
Store the session_reference_token in your server session, never in the browser. In the browser, initialize the SDK with the two endpoints instead of a signed-URL endpoint:
const sdk = getEmbedSDK();
sdk.initCookieless("looker.example.com", "/api/looker/acquire-session", "/api/looker/generate-tokens");
await sdk.createDashboardWithId("42").appendTo("#analytics").build().connect();
The SDK calls the acquire endpoint once, then the generate endpoint whenever tokens are about to expire. Cookieless embedding must be enabled on the instance (Admin > Embed) and it requires the Embed SDK; plain iframes cannot do it.
Operational checklist
- Embed secret and API keys in a secret manager; rotate on staff changes.
- Minimum permissions; no
exploreunless the product sells it. access_filteron every explore the embed models expose; user attributes locked.- Per-tenant
external_group_id, and content in group folders, not in Shared. - A dedicated Looker theme for the product's look and feel.
- Cookieless for cross-domain embeds; test in Safari first.
- Monitor embed user counts and query volume in System Activity; embed users are often the biggest query source on an instance.
Building customer-facing analytics and want it secure and fast from day one? This is a core service for us; see our Looker development company page or contact us.