Supercharge Salesforce: Embedding Looker with Lightning Web Components

Context switching between operational tools and BI platforms can be a productivity killer for teams throughout your organization. By embedding Looker directly into Salesforce, you can eliminate this friction, delivering trusted data and “in-context” analytics where your teams spend their day.

While there are alternative methods, like Visualforce Pages, to embed external content in Salesforce, the modern standard for Salesforce development is Lightning Web Components (LWC). LWCs offer greater flexibility, bi-directional communication, and a better developer experience.

Let’s walk through how to implement Looker Signed Embedding (SSO) using Salesforce Lightning Web Components.


Example adding a Looker dashboard for a 360 degree view of the customer (customer health scoring, support data, product usage, customer feedback).

Salesforce Looker Embedding

Example of adding a self service data exploration to Salesforce (provide the ability to explore data across customers leveraging BigQuery ML for customer churn predictions)

Beyond CRM: The Power of Lightning Web Components (LWC)

A key advantage of using Lightning Web Components is their portability across the Salesforce ecosystem. While often associated with the core CRM, LWCs are compatible with Salesforce Experience Cloud and other Lightning apps.

With LWC, you can build a single Looker embedding component and deploy it, not just to internal sales dashboards, but also external-facing portals for partners and customers, ensuring a consistent analytics experience everywhere.

Why Embed Looker?

Embedding Looker isn’t just about viewing charts; it’s about bringing the full power of a data platform into your operational workflows.

  • Flexible Experiences: You aren’t limited to static dashboards. You can embed Self-Service Explores to let users ask their own questions, full Looker Extensions (custom data applications), or trigger Data Actions to initiate external workflows immediately based on the data they see.

  • Robust Access Management: Looker SSO embedding provides granular control over what users can see and do. You can map Salesforce permissions dynamically to Looker User Attributes, Groups or Roles, ensuring users only access data they are authorized to see without needing to manage a separate layer of permissions.

  • Trusted Data: By leveraging Looker’s semantic model (LookML) as the single source of truth, you eliminate “metric drift” and ensure the data in Salesforce is identical to the data used across the rest of your enterprise.

The Architecture

The integration follows a secure handshake between the client and server:

  1. Client (LWC): Requests a signed URL from the Salesforce backend.
  2. Server (Apex): Generates a secure signature using your Looker Embed Secret.
  3. Looker: Validates the signature and creates a session, returning the secure content.

Step 1: Configure Looker

First, prepare your Looker instance for embedding (admin permissions required).

  1. Navigate to Admin > Platform > Embed and ensure Embed SSO Authentication is Enabled.
  2. Generate and copy your Embed Secret.
  3. Allowlist Salesforce Domains: Add your Salesforce domains (e.g., *.lightning.force.com and *.my.salesforce.com) to the Embedded Domain Allowlist to prevent X-Frame-Options errors.

Step 2: The Apex Controller (Backend)

We need an Apex class to handle the security handshake. This controller uses your Looker Embed Secret to sign a URL, allowing Salesforce users to log in to Looker automatically without a pre-existing session. Alternatively, Looker private embedding can be used, but will require an existing Looker session, or a redirect flow to authenticate. You can use the allow_login_screen=true url parameter in private embed to prevent CORS errors for Looker’s login page, but your required authentication flow (e.g. redirect to SAML provider) will need to be considered as well.

Create a class named LookerEmbedManager. This can be a universal controller to interface with multiple Looker dedicated Lightning Web Components (e.g. one LWC per Looker content type). Additionally, you can design the controller to append different filter values based on the Salesforce context (e.g. on an SFDC Account Page map the current Account Id to a Looker dashboard filter value).

Note: As the example code below shows, store your secret in custom metadata rather than hardcoding it in the script for production.

public with sharing class LookerEmbedManager {

    // Helper wrapper to send data + error info back to LWC cleanly
    public class EmbedResult {
        @AuraEnabled public String url;
        @AuraEnabled public String error;
    }

    /**
     * Generates a Signed Embedding URL for various Looker content types.
     * 
     * @param embedType The type of content to embed: 'dashboard', 'explore', or 'visualization'
     * @param ref1      Primary reference ID (Dashboard ID, Explore Model Name, or Visualization ID)
     * @param ref2      Secondary reference ID (Explore Name, null for others)
     * @param recordId  Context Record ID (for filtering)
     * @return EmbedResult containing the signed URL or error message
     */
    /**
     * Generates a Signed Embedding URL for various Looker content types.
     * 
     * @param embedType The type of content to embed: 'dashboard', 'explore', or 'visualization'
     * @param ref1      Primary reference ID (Dashboard ID, Explore Model Name, or Visualization ID)
     * @param ref2      Secondary reference ID (Explore View Name, null for others)
     * @param recordId  Context Record ID (for filtering)
     * @param objectName Context Object Name (Account/Opportunity) for auto-filtering
     * @return EmbedResult containing the signed URL or error message
     */
    @AuraEnabled(cacheable=true)
    public static EmbedResult getSignedUrl(String embedType, String ref1, String ref2, String recordId, String objectName) {
        EmbedResult result = new EmbedResult();
        try {
            // --- 1. CONFIGURATION ---
            Looker_Configuration__mdt config = [
                SELECT Looker_Host__c, Looker_Secret__c 
                FROM Looker_Configuration__mdt 
                WHERE DeveloperName = '{your_metadata_label}' 
                LIMIT 1
            ];
            String lookerHost = config.Looker_Host__c;
            String lookerSecret = config.Looker_Secret__c;

            // --- 2. VALIDATION & PATH CONSTRUCTION ---
            String embedPath = '';
            
            if (String.isBlank(embedType) || String.isBlank(ref1)) {
                throw new CalloutException('Missing required parameters: embedType and ref1 are mandatory.');
            }

            switch on embedType.toLowerCase() {
                when 'dashboard' {
                    embedPath = '/embed/dashboards/' + ref1;
                }
                when 'explore' {
                    if (String.isBlank(ref2)) {
                        throw new CalloutException('Explore embedding requires both Model (ref1) and View (ref2).');
                    }
                    embedPath = '/embed/explore/' + ref1 + '/' + ref2;
                }
                when 'visualization' {
                    embedPath = '/embed/query-visualization/' + ref1;
                }
                when else {
                    throw new CalloutException('Unsupported embed type: ' + embedType);
                }
            }

            // --- 2b. APPEND FILTERS TO PATH (Dashboard Only) ---
            // If we have an objectName and recordId, and it's a dashboard we append URL filters (adjust based on required parameterization)
            if (String.isNotBlank(recordId) && String.isNotBlank(objectName) && embedType.equalsIgnoreCase('dashboard')) {
                String filterName = '';
                if (objectName.equalsIgnoreCase('Account')) {
                    filterName = 'Account Id';
                } else if (objectName.equalsIgnoreCase('Opportunity')) {
                    filterName = 'Opportunity Id';
                }

                if (String.isNotBlank(filterName)) {
                    String safeKey = EncodingUtil.urlEncode(filterName, 'UTF-8');
                    String safeValue = EncodingUtil.urlEncode(recordId, 'UTF-8');
                    
                    if (embedPath.contains('?')) {
                        embedPath += '&' + safeKey + '=' + safeValue;
                    } else {
                        embedPath += '?' + safeKey + '=' + safeValue;
                    }
                }
            }



            // --- 3. PAYLOAD GENERATION ---
            String userId = UserInfo.getUserId();
            String timeStr = String.valueOf(DateTime.now().getTime() / 1000);
            String nonce = generateRandomString(16);

            Map<String, Object> accessFilters = new Map<String, Object>{};
            
            // JSON Serialize objects
            // Expanded permissions to cover explores/drilling if needed
            String jsonPermissions = JSON.serialize(new List<String> { 
                'access_data', 
                'see_looks', 
                'see_user_dashboards', 
                'see_lookml_dashboards', 
                'see_drill_overlay',
                'explore', // Use for Explore embedding
                'save_content', // Use for embedding saving content
                'embed_browse_spaces' // Use for embedded nav
            });
            
            // Adjust static models and groupIds for content and explore access as necessary
            Set<String> modelsToInclude = new Set<String>{'sfdc_demo'};
            if (embedType.equalsIgnoreCase('explore')) {
                modelsToInclude.add(ref1);
            }
            String jsonModels = JSON.serialize(modelsToInclude);

            String jsonGroupIds = JSON.serialize(new List<String>{'2'});
            String jsonAccessFilters = JSON.serialize(accessFilters);
            String jsonUserAttributes = JSON.serialize(new Map<String, String>());
            String jsonExternalGroupId = JSON.serialize('');
            
            String jsonExternalUserId = JSON.serialize(userId);
            String jsonFirstName = JSON.serialize(UserInfo.getFirstName());
            String jsonLastName = JSON.serialize(UserInfo.getLastName());
            String jsonNonce = JSON.serialize(nonce);

            // --- 4. SIGNATURE ---
            String stringToSign = '';
            stringToSign += lookerHost + '\n';
            stringToSign += '/login/embed/' + EncodingUtil.urlEncode(embedPath, 'UTF-8') + '\n';
            stringToSign += jsonNonce + '\n';
            stringToSign += timeStr + '\n';
            stringToSign += '3600\n';
            stringToSign += jsonExternalUserId + '\n';
            stringToSign += jsonPermissions + '\n';
            stringToSign += jsonModels + '\n';
            stringToSign += jsonGroupIds + '\n';
            stringToSign += jsonExternalGroupId + '\n';
            stringToSign += jsonUserAttributes + '\n';
            stringToSign += jsonAccessFilters;

            String signature = generateHMACSignature(stringToSign, lookerSecret);

            // --- 5. RETURN URL ---
            result.url = 'https://' + lookerHost + 
                   '/login/embed/' + EncodingUtil.urlEncode(embedPath, 'UTF-8') + 
                   '?nonce=' + EncodingUtil.urlEncode(jsonNonce, 'UTF-8') + 
                   '&time=' + EncodingUtil.urlEncode(timeStr, 'UTF-8') + 
                   '&session_length=3600' + 
                   '&external_user_id=' + EncodingUtil.urlEncode(jsonExternalUserId, 'UTF-8') + 
                   '&permissions=' + EncodingUtil.urlEncode(jsonPermissions, 'UTF-8') + 
                   '&models=' + EncodingUtil.urlEncode(jsonModels, 'UTF-8') + 
                   '&access_filters=' + EncodingUtil.urlEncode(jsonAccessFilters, 'UTF-8') + 
                   '&first_name=' + EncodingUtil.urlEncode(jsonFirstName, 'UTF-8') + 
                   '&last_name=' + EncodingUtil.urlEncode(jsonLastName, 'UTF-8') + 
                   '&group_ids=' + EncodingUtil.urlEncode(jsonGroupIds, 'UTF-8') + 
                   '&external_group_id=' + EncodingUtil.urlEncode(jsonExternalGroupId, 'UTF-8') + 
                   '&user_attributes=' + EncodingUtil.urlEncode(jsonUserAttributes, 'UTF-8') + 
                   '&force_logout_login=true' + 
                   '&signature=' + EncodingUtil.urlEncode(signature, 'UTF-8');
                   
        } catch (Exception e) {
            result.error = e.getMessage();
        }
        return result;
    }

    private static String generateRandomString(Integer len) {
        final String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
        String randStr = '';
        while (randStr.length() < len) {
            Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length());
            randStr += chars.substring(idx, idx+1);
        }
        return randStr; 
    }

    private static String generateHMACSignature(String input, String secret) {
        Blob mac = Crypto.generateMac('HmacSHA1', Blob.valueOf(input), Blob.valueOf(secret));
        return EncodingUtil.base64Encode(mac);
    }
}

Step 3: The Lightning Web Component (Frontend)

The LWC acts as the container. It requests the URL from the Apex controller and loads it into an iframe. The example below demonstrates how to create a LWC just to embed dashboards capturing the current SFDC record id (e.g. Account Id) of the page loaded. Additionally, the dashboard id is set as a configurable value to be set when the component is applied to a SFDC Page. This can be adapted to support multiple content types and input parameters.

Example lookerDashboardEmbed.html

<template>
    <div class="container" style={containerStyle}>
        
        <template if:false={iframeUrl}>
            <template if:false={error}>
                <lightning-spinner alternative-text="Loading Looker..." size="medium"></lightning-spinner>
            </template>
        </template>

        <template if:true={error}>
            <div class="slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_error" role="alert">
                <span class="slds-assistive-text">error</span>
                <h2>{error}</h2>
            </div>
        </template>

        <template if:true={iframeUrl}>
            <iframe 
                src={iframeUrl} 
                width="100%" 
                height="100%" 
                frameborder="0" 
                style="border:none;">
            </iframe>
        </template>

    </div>
</template>

Lightning Web Component Javascript (logic supports a configurable dashboard id as input)

import { LightningElement, api, wire } from 'lwc';
import getSignedUrl from '@salesforce/apex/LookerEmbedManager.getSignedUrl';

export default class LookerDashboardEmbed extends LightningElement {
    @api lookerDashboardId; 
    @api height = 800;
    
    // Automatically provided by Salesforce if on a Record Page
    @api recordId;
    @api objectApiName; 

    iframeUrl;
    error;

    // The 'wire' service listens for changes to dashboardId or recordId
    // and automatically calls the Apex method.
    // We pass 'dashboard' as embedType, dashboardId as ref1, null as ref2
    @wire(getSignedUrl, { embedType: 'dashboard', ref1: '$lookerDashboardId', ref2: null, recordId: '$recordId', objectName: '$objectApiName' })
    wiredResult({ error, data }) {
        if (data) {
            // Apex returns { url: "...", error: "..." }
            if (data.url) {
                this.iframeUrl = data.url;
                this.error = undefined;
            } else {
                this.error = 'Apex Error: ' + data.error;
                this.iframeUrl = undefined;
            }
        } else if (error) {
            this.error = 'Network/System Error: ' + (error.body ? error.body.message : error.message);
            this.iframeUrl = undefined;
        }
    }

    // Dynamic getter to set height style
    get containerStyle() {
        return `height: ${this.height}px; background: white; width: 100%;`;
    }
}

Lightning Web component configuration file (controls component configuration and scoped SFDC targets)

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>58.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Looker Dashboard</masterLabel>
    <description>Embeds a Looker Dashboard using SSO.</description>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage,lightning__AppPage,lightning__HomePage">
            <property name="lookerDashboardId" type="String" label="Dashboard ID" description="Looker Dashboard ID (e.g. 2)" />
            <property name="height" type="Integer" label="Height (px)" default="800" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Deploying

Once your code is pushed to your org, you can drag your custom Looker Embed component onto any Account, Opportunity, or Experience Cloud page layout. Simply enter the Looker Dashboard ID in the properties sidebar, and you will see a live, filtered dashboard immediately.

LWC Embedding Setup SFDC

Drag the Looker embed component onto your visual designer and adjust the configuration.

Important Note on Cookies

Looker SSO Embedding in Salesforce requires third-party cookies to be allowed in a standard implementation. If your organization blocks third-party cookies, consider setting up a Custom Domain (CNAME) so your Looker instance matches your Salesforce domain (e.g., both on *.example.com if using Salesforce’s Enhanced Domains feature) or explore Looker Cookieless Embedding.

Connect Looker to Salesforce today

By leveraging Looker powered Lightning Web Components, teams can eliminate productivity-killing context switching and ensure data consistency across the Salesforce ecosystem. This technical foundation allows you to deliver trusted, in-context analytics and custom data applications directly within operational workflows.

To get started, configure your Looker instance for SSO embedding and develop your first Lightning Web Component to create a seamless data experience for your users.

1 Like