1. Home
  2. /
  3. Blog
  4. /
  5. HighLevel: Utilizing SSO sessions from inside Custom Menu Links
Wednesday, November 22, 2023

HighLevel: Utilizing SSO sessions from inside Custom Menu Links

You can use SSO sessions from inside Custom Menu Links, not just custom pages

I'm working on an extensive custom integration with HighLevel, now possible because they recently built a single-sign-on process that allows web applications embedded in iFrames to validate that they are being loaded within the HL interface, as well as retrieve information about the current user and account viewing the application.

Unfortunately, the SSO functionality is currently only available on a secondary tab of the App. In addition, there can only be one Custom Page. This seems useful for a settings page, but there is so much more potential here.

Using some custom JavaScript, you can recreate the SSO functionality from any Custom Menu Link. If you haven't already learned how to implement this from inside the iFrame, I suggest you head over to Sergio Leon's video explaining the SSO process.

Setup

To start this, you will need an Application set up and installed, and an SSO Key generated for it.

You will need to know the Application ID and Location ID. You can insert the location ID into the link dynamically. From the tooltip on the Menu Link edit screen:

You may use Location values and Custom Values in here. (These will only work on the Location side bar, when inside an account) Example: http://url.com/{{location.id}}?value={{custom_values.my_value}}

In your Settings > Company page, there is a Custom Javascript box. This code will go in that box.

Event Listener

We need to re-create the event listener that GHL provides on the Application Custom Page tab. This is a standard event listener attatched to the window. This listens for a message event and executes the code. It is important that we set up an async function, since this will handle Promises. We're using destructuring to pull just the data object out of the event.

window.addEventListener("message", async function({ data }){ ... });

Since the message event is used by many functions, you want to make sure this is the event we're looking for. The name used by the official GHL implementation is REQUEST_USER_DATA. If you change this, it needs to match on both the custom listener and the app loaded inside the iFrame.

if (data.message === "REQUEST_USER_DATA") {
//...
}

Authenticated API requests

We'll need to be able to make an authenticated request for the session details. All API requests from the browser require these headers:

const requestHeaders = new Headers();
requestHeaders.append("source", "WEB_USER");
requestHeaders.append("version", "2021-07-28");
requestHeaders.append("accept", "application/json, text/plain, */*");
requestHeaders.append("channel", "APP");

Then this header is what provides the current user's token. window.getToken() is provided by the primary GHL application.

requestHeaders.append("token-id", await window.getToken());

For this SSO request, you'll also need to provide the location ID in a header.

requestHeaders.append("location", locationId);

Retrieving Session Details

We can get the details by making a request to the session-details endpoint with those headers and the App ID.

const sessionDetailsResponse = fetch(`https://services.leadconnectorhq.com/oauth/sso/${appId}/session-details`, {
method: "GET",
headers: requestHeaders,
});

Get the JSON payload from request

const sessionDetails = await sessionDetailsResponse.json();

Response Event

Now that we have the encrypted session data, when we want to reply to the iFrame. We'll need to have a target DOM element to send the message to. In this case, its the iFrame inside the .custom-link container.

const frame = document.querySelector(".custom-link iframe");

We're looking for the contentWindow property of the iFrame. This gives us the window object inside the iFrame. Now we can post a message inside the iFrame with the session details. The official implementation uses the message name REQUEST_USER_DATA_RESPONSE, again if you change that here, you must change it on the app loaded inside the iFrame.

frame.contentWindow.postMessage(
{
message: "REQUEST_USER_DATA_RESPONSE",
payload: sessionDetails.data,
},
"*"
);

Now your app needs to do the same request dance as the Custom Page. You can view that process here. Remember if you have changed the REQUEST_USER_DATA or REQUEST_USER_DATA_RESPONSE message names here, they need to be changed in your app implementation too.

The Code

Here's the full code

const appId = "123";
const locationId = "abc";
window.addEventListener("message", async ({ data }) => {
// Make sure this is a request for user data
if (data.message === "REQUEST_USER_DATA") {
// Build request headers
const requestHeaders = new Headers();
requestHeaders.append("source", "WEB_USER");
requestHeaders.append("version", "2021-07-28");
requestHeaders.append("accept", "application/json, text/plain, */*");
requestHeaders.append("channel", "APP");
requestHeaders.append("location", locationId);
requestHeaders.append("token-id", await window.getToken());
// Request session key from GHL endpoint
const sessionDetailsResponse = await fetch(`https://services.leadconnectorhq.com/oauth/sso/${appId}/session-details`, {
method: "GET",
headers: requestHeaders,
});
// Get JSON payload from request
const sessionDetails = await sessionDetailsResponse.json();
// Find the iFrame
const frame = document.querySelector(".custom-link iframe");
// Respond to iFrame with data
frame.contentWindow.postMessage(
{
message: "REQUEST_USER_DATA_RESPONSE",
payload: sessionDetails.data,
},
"*"
);
}
});