Send message from web page to chrome extension

3.9k Views Asked by At

I want to be able to send a message from my web app to my chrome extension so it can easier to use (send the auth token so users dont have to login twice). However after looking at the documentation and reading a bunch of SO questions, I cannot get anything working for me.

Here's some parts my manifest.json:

"content_security_policy": "script-src 'self' https://330218550995.ngrok.io; object-src 'self'",
"background": {
    "scripts": ["background.js"],
    "persistent": false
},
"externally_connectable": {
    "matches": [
        "*://localhost/*",
        "*://*.ngrok.io/*"
    ]
},
"content_scripts": [
  {
    "matches": ["<all_urls>"],
    "exclude_matches": [
      "*://*.olympiatasks.com/*",
      "https://app.olympiatasks.com/*",
      "https://*.olympiatasks.com/*"
  ],
    "css": ["/static/css/main.css"],
    "js": [
      "/static/js/runtime-main.js", 
      "/static/js/2.js", 
      "/static/js/main.js"
    ]
  }
],

Inside of the content script I do this:

const ExtensionID = process.env.REACT_APP_EXTENSION_ID || '';
chrome?.runtime.connect(ExtensionID, { name: 'example' });
chrome?.runtime?.sendMessage('Hi from content script')

Inside of the web page I do this:

const ExtensionID = process.env.REACT_APP_EXTENSION_ID || "";
chrome.runtime.connect(ExtensionID, { name: "example" });
chrome?.runtime?.sendMessage(ExtensionID, "Hi from app");

Then here is the listener in the background.js:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    console.log({ request })
});

chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => {
    console.log("Received message from " + sender + ": ", request);
    sendResponse({ received: true }); //respond however you like
});

When I open the website, the extension is successfully injected and in the dev console of the background.js page I get the following:

Hello world from background.js

{request: "Hi from content script"}

The "hi from app" is missing which means it's not being sent/received. I used ngrok to setup forwarding to my app thinking that either:

  • The domain being localhost
  • The protocol not being https

could be the problem but as you guess, no luck.

So far I have done the following:

  • Setup my externally_connectable inside my manifest.json
  • Setup the onMessageExternal listener in the background.js
  • Call runtime.sendMessage with the Extension ID like shown in the docs
  • Used an https website for secure connection

Any help on this issue is greatly appreciated

2

There are 2 best solutions below

0
On BEST ANSWER

Thanks to @wOxxOm comment I was able to resolve the issue.

I quote:

Tentatively, since ngrok.io is in public suffix list it means it's basically like com which is forbidden in externally_connectable. Try using a more specific pattern for the site.

I changed the URL to use one more specific https://330218550995.ngrok.io/* and it now works

2
On

We can synchronize the authentication token between the website and chrome extension using content-script and background js file.

Inside web page I do this ...

I am not sure how you did it on the web page

Here is what I did to send access token from web page to extension.

Here is my content-script file.

/* eslint-disable no-undef */

const hostScript = () => {
  // ----------------- Function Declarations -----------------

  let listenGetTokenResponseFromWindow = () => {};

  const sendMessagesToExtension = (msg, callback = null) => {
    chrome.runtime.sendMessage(msg);
  };

  const sendMessagesToWindow = (msg, callback = null) => {
    const { type } = msg;
    
    switch (type) {
      case ExtensionMessagesTypes.GetTokenFromWindow:
        // Can not pass the function with window.postMessage. Only JSON object can be passed.
        // So reset the listener
        window.postMessage(msg, document.location.origin);

        window.removeEventListener("message", listenGetTokenResponseFromWindow);
        listenGetTokenResponseFromWindow = event => {
          if (event.source !== window) return;
          if (event.data.type === ExtensionMessagesTypes.GetTokenFromWindowResponse) {
            const { payload } = event.data;
            !!callback && callback(payload.token);
          }
        };
        window.addEventListener("message", listenGetTokenResponseFromWindow);
        break;
      case ExtensionMessagesTypes.SetWindowToken:
        window.postMessage(msg, document.location.origin);
        !!callback && callback();
        break;
      default:
        break;
    }
  };

  const listenMessagesFromWindow = event => {
    if (event.source !== window) return;

    const msg = event.data;
    
    Object.values(ExtensionMessagesTypes).includes(msg.type) && sendMessagesToExtension(msg);
  };

  const listenMessagesFromExtension = (msg, sender, response) => {
    sendMessagesToWindow(msg, response);
    return true; // means response is async
  };

  // ----------------- Listener Definitions -----------------

  window.removeEventListener("message", listenMessagesFromWindow);
  window.addEventListener("message", listenMessagesFromWindow);

  chrome.runtime.onMessage.removeListener(listenMessagesFromExtension);
  chrome.runtime.onMessage.addListener(listenMessagesFromExtension);

  // Reset extension token as windows token
  sendMessagesToWindow({ type: ExtensionMessagesTypes.GetTokenFromWindow }, token => {
    sendMessagesToExtension({
      type: ExtensionMessagesTypes.SetExtensionToken,
      payload: { token }
    });
  });
};

Here is the background.js file.

(() => {
        chrome.runtime.onMessage.addListener((msg, sender, response) => {
            switch (msg.type) {
                case ExtensionMessagesTypes.GetTokens:
                    getAccessTokens(response);
                    break;
                case ExtensionMessagesTypes.SetExtensionToken:
                    !!msg.payload && !!msg.payload.token && setAccessTokenToExtensionLocalStorage(msg.payload.token, response);
                    break;
                case ExtensionMessagesTypes.SetWindowToken:
                    !!msg.payload && !!msg.payload.token && sendMessageFromExtensionToWindow(msg, response);
                    break;
            }
    });
})();

const sendMessageFromExtensionToWindow = (msg, callback = null) => {
    chrome.tabs.query({ currentWindow: true, url: `${HostURL}/*` }, tabs => {
        if (tabs.length < 1) {
            !!callback && callback(null);
            return;
        }

        if (msg.type === ExtensionMessagesTypes.GetTokenFromWindow) {
            chrome.tabs.sendMessage(tabs[0].id, msg, token => {
                !!callback && callback(token);
            });
        } else if (msg.type === ExtensionMessagesTypes.SetWindowToken) {
            chrome.tabs.sendMessage(tabs[0].id, msg, () => {
                !!callback && callback();
            });
        }
    });
};

// Authentication

const setAccessTokenToExtensionLocalStorage = (access_token, callback = null) => {
    chrome.storage.local.set({ access_token }, () => {
        !!callback && callback();
    });
};

const getAccessTokenFromChromeStorage = callback => {
    chrome.storage.local.get(['access_token'], result => {
        !!callback && callback(result.access_token);
    });
};

const getAccessTokens = callback => {
    getAccessTokenFromChromeStorage(accessTokenFromExtension => {
        sendMessageFromExtensionToWindow({ type: ExtensionMessagesTypes.GetTokenFromWindow }, accessTokenFromWindow => {
            callback({
                accessTokenFromExtension: accessTokenFromExtension || '',
                accessTokenFromWindow: accessTokenFromWindow || ''
            });
        });
    });
};

const handleLogin = payload => {
    //TODO: Handling refresh token
    
    const { token } = payload;
    if (!token) return;

    setAccessTokenToExtensionLocalStorage(token);
};

const handleLogout = () => {
    setAccessTokenToExtensionLocalStorage(null);
};

PS: You don't need externally_connectable option in the manifest.json file.