πŸ” [Tutorial Part 2] Securing Your Chrome Extension β†’ n8n (HMAC & Native Messaging)

:waving_hand: Welcome to the second installment of our Browser-to-Automation series!

Before You Start


Why This Matters

In Part 1, the extension called an n8n webhook directly.

That meant anyone who found the URL could trigger the workflow and consume AI credits.

There is another important rule here: secrets should not live in extension JavaScript, because the client can inspect them.

To solve this, we use:

  1. Chrome Native Messaging

  2. A local Python signer

  3. HMAC verification in n8n

The secret never leaves your machine.


Step 1: UI Evolution (Options and History)

This version adds an Options page for configuration and a History view for previous translations.

That makes the extension feel more complete, but it also means the background script now plays a bigger role. It becomes the coordinator for settings, request signing, and data flow before anything reaches n8n.


Step 2: Native Messaging Host (Python)

This script runs locally and signs requests using a local shared secret.

1. Create a secret file

Create a file named secret with no extension in your script folder.

Inside it, paste your secret: <your_shared_secret>

2. Create secure_signer.py

import os
import sys
import json
import struct
import hmac
import hashlib

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET_PATH = os.path.join(BASE_DIR, "secret")


def get_shared_secret():
    if not os.path.exists(SECRET_PATH):
        return None
    with open(SECRET_PATH, "r", encoding="utf-8") as f:
        return f.read().strip()


SHARED_SECRET = get_shared_secret()


def get_message():
    raw_length = sys.stdin.buffer.read(4)
    if not raw_length:
        sys.exit(0)
    message_length = struct.unpack("=I", raw_length)[0]
    return json.loads(sys.stdin.buffer.read(message_length).decode("utf-8"))


def send_message(message):
    content = json.dumps(message).encode("utf-8")
    sys.stdout.buffer.write(struct.pack("=I", len(content)))
    sys.stdout.buffer.write(content)
    sys.stdout.buffer.flush()


def main():
    if not SHARED_SECRET:
        return

    while True:
        try:
            msg = get_message()
            p = msg.get("payload", {})

            # Canonical signing format (JSON array for consistency)
            message_to_sign = json.dumps(
                [p.get("timestamp", ""), p.get("language", ""), p.get("text", "")],
                ensure_ascii=False,
                separators=(",", ":")
            )

            signature = hmac.new(
                SHARED_SECRET.encode("utf-8"),
                message_to_sign.encode("utf-8"),
                hashlib.sha256
            ).hexdigest()

            send_message({
                "signature": signature,
                "timestamp": p.get("timestamp")
            })
        except EOFError:
            break


if __name__ == "__main__":
    main()

Step 3: Chrome Extension (background.js)

The extension asks Python for a signature, then sends the signed request to n8n.

async function handleTranslation(payload, sendResponse) {
  try {
    const timestamp = Date.now().toString();

    // 1. Ask Python to sign the payload
    const sigResponse = await new Promise((resolve) => {
      chrome.runtime.sendNativeMessage('com.secure.n8n.signer', {
        payload: { ...payload, timestamp: timestamp }
      }, resolve);
    });

    if (!sigResponse?.signature) {
      throw new Error("Signing failed");
    }

    // 2. Send signed request to n8n
    const response = await fetch("http://localhost:5678/webhook/secure-translate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Signature": sigResponse.signature,
        "X-Timestamp": timestamp
      },
      body: JSON.stringify(payload)
    });

    const result = await response.json();
    sendResponse({ success: true, translatedText: result.translatedText });
  } catch (err) {
    sendResponse({ success: false, error: err.message });
  }
}

Step 4: n8n Backend (Verification)

1. Webhook (CORS)

  • Allowed Origins: chrome-extension://<YOUR_EXTENSION_ID>

This ensures only your extension can call the webhook from the browser.

2. Crypto Node (HMAC Verification)

Both sides must sign the exact same string.

We use a JSON array for consistency.

  • Value:
={{ JSON.stringify([$json.headers["x-timestamp"], $json.body.language, $json.body.text]) }}
  • Secret: Same as your Python secret file

3. IF Node (Gatekeeper)

Compare the request signature with the computed signature.

4. Respond to Webhook

Always return clean JSON to avoid β€œUnexpected end of JSON” errors:

{
  "translatedText": "={{ $node['AI Translator'].json.text }}"
}


What Actually Secures This?

It is not just CORS. Your real protection is:

  1. The secret is never exposed in the frontend.

  2. The payload is signed locally through Native Messaging.

  3. n8n verifies the signature before any AI processing happens.


What’s Next?

In Part 3, we will build:

  • Error handling

  • Retry logic

  • Failure-safe UX

Drop a comment if you run into any issues with the Native Messaging setup.

1 Like