A public /exec URL is easy to leak, so your doPost shouldn't trust whoever calls it. requireSharedSecret(e, propertyKey) reads the expected secret from Script Properties, compares it with ?secret= on the URL (or a secret field in a JSON body), and returns { ok, reason }. Your doPost checks it first and returns an error before writing anything.
One honest limitation: Apps Script's doPost(e) event doesn't give you the request headers, so an X-Webhook-Secret header can't be checked here. Put the secret in the query string or the body.
When to use
- Webhook receivers (Stripe-style, Typeform, Zapier, your own scripts)
- Small JSON APIs you expose to a partner or another script
- Any web app where “anyone with the link” is too generous
How to use this snippet
/**
* Check a shared secret on a web app request.
* Accepts ?secret=... (e.parameter.secret) or a "secret" field in a JSON body.
* doPost(e) has no request headers, so header secrets aren't supported.
*
* @param {Object} e the doPost / doGet event
* @param {string=} propertyKey Script Property holding the secret (default WEBHOOK_SECRET)
* @return {{ok: boolean, reason?: string}}
*/
function requireSharedSecret(e, propertyKey) {
var expected = PropertiesService.getScriptProperties()
.getProperty(propertyKey || 'WEBHOOK_SECRET');
if (!expected) return { ok: false, reason: 'Server secret not configured' };
var got = (e && e.parameter && e.parameter.secret) || '';
if (!got && e && e.postData && /json/i.test(e.postData.type || '')) {
try { got = JSON.parse(e.postData.contents).secret || ''; } catch (x) {}
}
if (!got) return { ok: false, reason: 'Missing secret' };
return got === expected ? { ok: true } : { ok: false, reason: 'Bad secret' };
}
Example
function doPost(e) {
var auth = requireSharedSecret(e, 'WEBHOOK_SECRET');
if (!auth.ok) return jsonErr(auth.reason, 401); // nothing written
var payload = JSON.parse(e.postData.contents);
delete payload.secret; // don't store it
SpreadsheetApp.getActive().getSheetByName('Inbox')
.appendRow([new Date(), JSON.stringify(payload)]);
return jsonOk({ received: true });
}
jsonOk / jsonErr come from the JSON OK / ERR helpers. Apps Script still answers HTTP 200, so the caller sees the error in the body.
Tips:
- Use a long random secret (32+ characters) and rotate it by changing the Script Property, not the code.
- Never log
e.parameteror the raw body as-is: the secret is in there. Mask it first (mask-secret). - A query-string secret can end up in the sender's logs. If the sender supports a JSON body field, prefer that.
Tip: NitroGAS Co-Pilot can add the secret check to the top of an existing doPost and move the secret into Script Properties. Paste your doPost.
Happy Coding!
