Welcome to the second installment of our Browser-to-Automation series!
Before You Start
-
Prerequisite: This is Part 2. If you have not already, please review
Part 1: Build an AI Translator Chrome Extension with n8n. -
Architecture: I recommend reading the
SRS first to understand the Zero-Trust design behind this approach. -
Scope: This tutorial focuses on payload signing, verification, and CORS.
-
Next Part: Error handling, retry logic, and testing.
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:
-
Chrome Native Messaging
-
A local Python signer
-
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:
-
The secret is never exposed in the frontend.
-
The payload is signed locally through Native Messaging.
-
n8n verifies the signature before any AI processing happens.
Design Considerations: Choosing the Right Security Model
When designing this solution, I intentionally prioritized Zero-Trust security principles over ease of installation. Like most engineering decisions, this comes with trade-offs. Understanding those trade-offs helps you decide when the Chrome Native Messaging + n8n HMAC model is the right choice.
Why this model is strong (Security First Approach)
Secrets stay outside the browser environment
By using a local Python Native Host, your API keys, signing secrets, and sensitive logic do not live inside the extension itself.
That means they are far less exposed to risks such as:
- XSS (Cross-Site Scripting)
- malicious browser extensions
- local storage scraping
- accidental credential leakage in client side code
Strong trust boundaries
The browser extension becomes a lightweight interface, while trust-sensitive operations happen in the local host process and your n8n backend.
This follows a cleaner separation of responsibilities:
- Extension: UI, trigger layer
- Native Host: secure local bridge
- n8n: automation engine
Well suited for internal or enterprise tools
This model can be an excellent fit for:
- internal company tools
- DevOps utilities
- admin workflows
- regulated environments
- teams handling sensitive credentials
IT teams can deploy the Native Host and extension in a controlled way, allowing staff to trigger automations without directly accessing secrets.
The Trade off (User Friction)
Extra installation steps
Unlike a standard one click extension, this setup usually requires:
- installing Python or a packaged host app
- placing the Native Messaging manifest
- OS-level registration
- occasional update management
Smaller target audience
For public consumer facing extensions, this creates a higher barrier to entry. Non technical users may abandon setup before reaching value.
The Verdict
If you are building for:
- yourself
- technical users
- internal teams
- security sensitive environments
then the Native Messaging + HMAC model is a robust and professional choice.
If you are building for the general public at scale, a Cloud Proxy / Middleware model is often better for onboarding and distribution, while keeping secrets server-side.
Final Engineering Perspective
Good architecture is rarely about βbestβ in absolute terms. It is about choosing the right balance between:
- security
- usability
- maintenance
- scale
- audience expectations
In this case, I chose security first.
Whatβs Next?
In Part 3, we will build:
-
Error handling
-
Retry logic
-
Failure-safe UX



