Skip to content

Instantly share code, notes, and snippets.

@Zenithar
Last active June 28, 2023 12:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zenithar/7081bff477eb4e8042fad6c31bd0208e to your computer and use it in GitHub Desktop.
Save Zenithar/7081bff477eb4e8042fad6c31bd0208e to your computer and use it in GitHub Desktop.
Use HTTP Signature with Google Forms and a custom webhook.

HTTP Signature authenticated remote webhook with Google Forms

Why not just a JWT assertion? Because a JWT assertion doesn't protect the request body. It's a proof of authentication of the private key owner, but doesn't authenticate the request content.

Generate the private key

$ openssl rsa -in private.pem -outform PEM -pubout -out public.pem
$ openssl pkcs8 -topk8 -inform pem -in private.pem -outform pem -nocrypt -out newPrivate.pem

Copy the newPrivate.pem content and replace all newline characters by "\n" to ensure that the content can hold in one line. Replace the signingKey content by the inlined private key content.

const WEBHOOK_HOST = "<replace with your host>";
const WEBHOOK_PATH = "<replace with your path>";
function signAndSend(body) {
const signingKey = "-----BEGIN PRIVATE KEY-----\nREPLACE WITH YOUR PKCS8 ENCODED PRIVATE RSA KEY \n-----END PRIVATE KEY-----"
// Serialize payload request
const payload = JSON.stringify(body)
// Pack signature parameters
const now = Math.floor((new Date).getTime() / 1000);
const nonce = Utilities.getUuid();
const sigParams = '("@method" "@authority" "@path" "content-type" "content-digest" "content-length");created=' + now + ';keyid="<replace with your expected subject>";nonce="' + nonce + '"'
// Compute content digest
const contentDigest = 'sha-512=:' + Utilities.base64Encode(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_512, payload)) + ':';
// Prepare protected content to sign
var protected = '"@method": POST' + "\n";
protected += '"@authority": ' + WEBHOOK_HOST + "\n";
protected += '"@path": ' + WEBHOOK_PATH + "\n";
protected += '"content-type": application/json' + "\n";
protected += '"content-digest": ' + contentDigest + "\n";
protected += '"content-length": ' + payload.length + "\n";
protected += '"@signature-params": ' + sigParams;
// Sign the protected content
const signatureBytes = Utilities.computeRsaSignature(Utilities.RsaAlgorithm.RSA_SHA_256, protected, signingKey);
const signature = Utilities.base64Encode(signatureBytes);
const options = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Content-Digest": contentDigest,
"Signature-Input": "sig=" + sigParams,
"Signature": "sig=:" + signature + ":",
},
"payload": payload,
"muteHttpExceptions": true,
}
const response = UrlFetchApp.fetch("https://"+WEBHOOK_HOST+WEBHOOK_PATH, options);
Logger.log(response);
}
function debugPost() {
signAndSend({"test": true})
}
function doPost(e) {
// http://googleappsdeveloper.blogspot.co.uk/2011/10/concurrency-and-google-apps-script.html
var lock = LockService.getPublicLock();
lock.waitLock(10000); // wait 10 seconds before conceding defeat.
try {
// Retrieve last submission
const form = FormApp.getActiveForm();
const allResponses = form.getResponses();
const latestResponse = allResponses[allResponses.length - 1];
const response = latestResponse.getItemResponses();
// Prepare payload
var items = [];
for (var i = 0; i < response.length; i++) {
const question = response[i].getItem().getTitle();
const answer = response[i].getResponse();
items.push({"question":question, "answer": answer});
}
// Send to webhook endpoint
signAndSend({
"@timestamp": latestResponse.getTimestamp(),
"@id": e.response.getId(),
"@type": "form_posted",
"email": latestResponse.getRespondentEmail(),
"responses": items,
});
} catch (err) {
Logger.log(err);
} finally { //release lock
lock.releaseLock();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment