What is the best practice to secure your facebook chatbot webhook? - webhooks

I am playing around with developing a chatbot on facebook messenger platform. I went through the Facebook document and couldn't find how to protect my webhook from random calls.
For example, if users can buy stuff with my bots, an attacker that knows someone's userId can start placing unauthorized orders by making calls to my webhook.
I have several ideas on how to protect this.
Whitelist my API to only calls from Facebook.
Create something
like CSRF tokens with the postback calls.
Any ideas?

Facebook has of course already implemented a mechanism by which you can check if requests made to your callback URL are genuine (everything else would just be negligence on their part) – see https://developers.facebook.com/docs/graph-api/webhooks/getting-started#validate-payloads
We sign all Event Notification payloads with a SHA256 signature and include the signature in the request's X-Hub-Signature-256 header, preceded with sha256=. You don't have to validate the payload, but you should.
To validate the payload:
Generate a SHA256 signature using the payload and your app's App Secret.
Compare your signature to the signature in the X-Hub-Signature-256 header (everything after sha256=). If the signatures match, the payload is genuine.
Please note that we generate the signature using an escaped unicode version of the payload, with lowercase hex digits. If you just calculate against the decoded bytes, you will end up with a different signature. For example, the string äöå should be escaped to \u00e4\u00f6\u00e5.

In addition to CBroe's answer, the snippet below represents signature verification implementation as NestJS guard.
// src/common/guards/signature-verification.guard.ts
#Injectable()
export class SignatureVerificationGuard implements CanActivate {
constructor(private readonly configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const {
rawBody,
headers: { 'x-hub-signature': signature },
} = context.switchToHttp().getRequest();
const { sha1 } = parse(signature);
if (!sha1) return false;
const appSecret = this.configService.get('MESSENGER_APP_SECRET');
const digest = createHmac('sha1', appSecret).update(rawBody).digest('hex');
const hashBufferFromBody = Buffer.from(`sha1=${digest}`, 'utf-8');
const bufferFromSignature = Buffer.from(signature, 'utf-8');
if (hashBufferFromBody.length !== bufferFromSignature.length)
return false;
return timingSafeEqual(hashBufferFromBody, bufferFromSignature);
}
}
// src/modules/webhook/webhook.controller.ts
#UseGuards(SignatureVerificationGuard)
#Post()
#HttpCode(HttpStatus.OK)
handleWebhook(#Body() data) {
// ...
}

Related

PDFNet Digital Signature in Node JS using Google KMS

I've seen example of signing https://www.pdftron.com/documentation/nodejs/guides/features/signature/sign-pdf
signOnNextSave uses PKCS #12 certificate, but I use Google KMS for asymmetric signing to keep private keys safe.
Here is example of signing and verifying by Google Cloud KMS
I tried to implement custom SignatureHandler but Node.JS API is different from Java or .NET
https://www.pdftron.com/api/pdfnet-node/PDFNet.SignatureHandler.html
How can I implement custom signing and verifying logic?
const data = Buffer.from('pdf data')
// We have 2048 Bit RSA - PSS Padding - SHA256 Digest key in Google Cloud KMS
const signAsymmetric = async () => {
const hash = crypto.createHash('sha256')
hash.update(data)
const digest = hash.digest()
const digestCrc32c = crc32c.calculate(digest)
// Sign the data with Cloud KMS
const [signResponse] = await client.asymmetricSign({
name: locationName,
digest: {
sha256: digest
},
digestCrc32c: {
value: digestCrc32c
}
})
if (signResponse.name !== locationName) {
throw new Error('AsymmetricSign: request corrupted in-transit')
}
if (!signResponse.verifiedDigestCrc32c) {
throw new Error('AsymmetricSign: request corrupted in-transit')
}
if (
crc32c.calculate(signResponse.signature) !==
Number(signResponse.signatureCrc32c.value)
) {
throw new Error('AsymmetricSign: response corrupted in-transit')
}
// Returns signature which is buffer
const encoded = signResponse.signature.toString('base64')
console.log(`Signature: ${encoded}`)
return signResponse.signature
}
// Verify data with public key
const verifyAsymmetricSignatureRsa = async () => {
const signatureBuffer = await signAsymmetric()
const publicKeyPem = await getPublicKey()
const verify = crypto.createVerify('sha256')
verify.update(data)
verify.end()
const key = {
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING
}
// Verify the signature using the public key
const verified = verify.verify(key, signatureBuffer)
return verified
}
At this time, the PDFTron SDK only supports custom handlers on C++, Java, and C# (there are more plans to include additional languages in the future).
On a different platform like C++, you would extend the custom handler functions by putting hash.update(data) into SignatureHandler::AppendData, and the rest of signAsymmetric would go into SignatureHandler::CreateSignature. A name would be given to the custom handler for interoperability like Adobe.PPKLite (we do not yet support custom handler SubFilter entries, only Filter -- see PDF standard for the difference -- but this won't matter so long as you use a verification tool that supports Filter Adobe.PPKLite). Please see the following link for a concrete example:
https://www.pdftron.com/documentation/samples/cpp/DigitalSignaturesTest
As for verification, our code can already do this for you if your signatures fulfill the following conditions:
they use a standard digest algorithm
they use RSA to sign
they use the correct data formats according to the PDF standard (i.e. detached CMS, digital signature dictionary)
If you have more questions or require more details, please feel free to reach out to PDFTron support at support#pdftron.com

How to Validate a Xero webhook payload with HMACSHA256 Node js

I need to validate Xero webhook in my node js project. This is Xero documentation steps to validate: https://developer.xero.com/documentation/webhooks/creating-webhooks#STATUS
var crypto = require("crypto")
function getHmacSha256(message, secret) {
return crypto.createHmac("sha256", secret).update(message).digest("base64")
}
// webhookPayload and signature get from webhook body and header
const webhookPayload = {
events: [],
firstEventSequence: 0,
lastEventSequence: 0,
entropy: 'OSHPXTUSXASRFBBCJFEN'
}
const signature = "OXLaeyZanKI5QDnLkXIVB35XrZygYsPMeK8WfoXUMU8="
const myKey = "1y5VYfv7WbimUQIMXiQCB6W6TKIp+5ZZJNjn3Fsa/veK5X/C8BZ4yzvPkmr7LvuL+yfKwm4imnfAB5tEoJfc4A=="
var hash = getHmacSha256(JSON.stringify(webhookPayload), myKey)
//If the payload is hashed using HMACSHA256 with your webhook signing key and base64 encoded, it should match the signature in the header.
if (signature === hash) {
return res.status(200).end()
}else{
return res.status(401).end()
}
Every time my signature and hash are different so it returns with 401 every time.
So I failed to complete Intent to receive
From what you're describing, my guess is you are unintentionally modifying the request body. You need to accept the raw request body from the webhook event without modification. If this body is modified at all, your code will fail to verify the signature and will fail Xero’s “Intent to receive” validation. Check out this blog post for details.

How do I generate the correct TOTP with Node with correct Headers and SHA512 hashed Token?

A recent school project I was assigned has a coding challenge we have to complete. The challenge has multiple parts, and the final part is uploading to a private GitHub repo and submitting a completion request by making a POST request under certain conditions.
I have successfully completed the other parts of the challenge and am stuck on submitting the request. The submission has to follow these rules:
Build your solution request
First, construct a JSON string like below:
{
"github_url": "https://github.com/YOUR_ACCOUNT/GITHUB_REPOSITORY",
"contact_email": "YOUR_EMAIL"
}
Fill in your email address for YOUR_EMAIL, and the private Github repository with your solution in YOUR_ACCOUNT/GITHUB_REPOSITORY.
Then, make an HTTP POST request to the following URL with the JSON string as the body part.
CHALLENGE_URL
Content type
The Content-Type: of the request must be application/json.
Authorization
The URL is protected by HTTP Basic Authentication, which is explained on Chapter 2 of RFC2617, so you have to provide an Authorization: header field in your POST request.
For the userid of HTTP Basic Authentication, use the same email address you put in the JSON string.
For the password , provide a 10-digit time-based one time password conforming to RFC6238
TOTP.
Authorization password
For generating the TOTP password, you will need to use the following setup:
You have to generate a correct TOTP password according to RFC6238
TOTP's Time Step X is 30 seconds. T0 is 0.
Use HMAC-SHA-512 for the hash function, instead of the default HMAC-SHA-1.
Token shared secret is the userid followed by ASCII string value "APICHALLENGE" (not including double quotations).
Shared secret examples
For example, if the userid is "email#example.com", the token shared secret is "email#example.comAPICHALLENGE" (without quotes).
If your POST request succeeds, the server returns HTTP status code 200 .
I have tried to follow this outline very carefully, and testing my work in different ways. However, it seems I can't get it right. We are supposed to make the request from a Node server backend. This is what I have done so far. I created a new npm project with npm init and installed the dependencies you will see in the code below:
const axios = require('axios');
const base64 = require('base-64');
const utf8 = require('utf8');
const { totp } = require('otplib');
const reqJSON =
{
github_url: GITHUB_URL,
contact_email: MY_EMAIL
}
const stringData = JSON.stringify(reqJSON);
const URL = CHALLENGE_URL;
const sharedSecret = reqJSON.contact_email + "APICHALLENGE";
totp.options = { digits: 10, algorithm: "sha512" }
const myTotp = totp.generate(sharedSecret);
const isValid = totp.check(myTotp, sharedSecret);
console.log("Token Info:", {myTotp, isValid});
const authStringUTF = reqJSON.contact_email + ":" + myTotp;
const bytes = utf8.encode(authStringUTF);
const encoded = base64.encode(bytes);
const createReq = async () =>
{
try
{
// set the headers
const config = {
headers: {
'Content-Type': 'application/json',
"Authorization": "Basic " + encoded
}
};
console.log("Making req", {URL, reqJSON, config});
const res = await axios.post(URL, stringData, config);
console.log(res.data);
}
catch (err)
{
console.error(err.response.data);
}
};
createReq();
As far as I understand, I'm not sure where I'm making a mistake. I have tried to be very careful in my understanding of the requirements. I have briefly looked into all of the documents the challenge outlines, and gathered the necessary requirements needed to correctly generate a TOTP under the given conditions.
I have found the npm package otplib can satisfy these requirements with the options I have passed in.
However, my solution is incorrect. When I try to submit my solution, I get the error message, "Invalid token, wrong code". Can someone please help me see what I'm doing wrong?
I really don't want all my hard work to be for nothing, as this was a lengthy project.
Thank you so much in advance for your time and help on this. I am very grateful.
The Readme of the package otplib states:
// TOTP defaults
{
// ...includes all HOTP defaults
createHmacKey: totpCreateHmacKey,
epoch: Date.now(),
step: 30,
window: 0,
}
So the default value for epoch (T0) is Date.now() which is the RFC standard. The task description defines that T0 is 0.
You need to change the default value for epoch to 0:
totp.options = { digits: 10, algorithm: "sha512", epoch: 0 }

Unable to setup webhook in xero?

I am trying to set up a webhook in Xero. I have created an endpoint which Xero hits and send some header and payload. I extract the hash from the header and match with the hash of payload but i never get the same hash. I am using the below code to do that.
router.post('/weebhook', function(req, res, next) {
console.log(req.headers)
console.dir(req.body);
try {
var xero_signature = req.headers['x-xero-signature']
var encoded_data = encodePayload(req.body)
console.log(xero_signature)
console.log(encoded_data)
if (encoded_data == xero_signature) {
res.status(200).json();
} else {
res.status(401).json();
}
}catch(eror) {
console.log(eror)
}
});
function encodePayload(payload) {
console.log(JSON.stringify(payload))
const secret = 'TbJjeMSPAvJiMiD2WdHbjP20iodKCA3bL5is8vo47/pCcuGCsjtUDb7cBnWo20e0TBwZsQ/lPM41QgypzZE6lQ==';
const hash = crypto.createHmac('sha256',secret,true)
.update(JSON.stringify(payload))
.digest().toString('base64');
return hash
}
Xero hash - NzQOq6yw6W6TKs1sQ1AJtMWX24uzzkyvh92fMxukreE=
my hash - L74zFdcuRsK3zHmzu9K37Y1mAVIAIsDgneAPHaJ+vI4=
Please let me know what is the issue ?
There's a typescript sample application provided by Xero that implements the webhooks signature verification.
Does the code in here help you at all? https://github.com/XeroAPI/XeroWebhooksReceiver-Node/blob/master/src/server/server.ts#L58L59
Also, please delete and recreate your webhook as you've just provided everyone with your secret webhooks key.
Change .update(JSON.stringify(payload)) to .update(payload.toString())

Verify JWT from Google Chat POST request

I have a bot in NodeJS connected to Google Chat using HTTPs endpoints. I am using express to receive requests. I need to verify that all requests come from Google, and want to do this using the Bearer Token that Google Sends with requests.
My problem is that I am struggling to find a way to verify the tokens.
I have captured the token and tried a GET reuqes to https://oauth2.googleapis.com/tokeninfo?id_token=ey... (where ey... is the token start).
Which returns:
"error": "invalid_token",
"error_description": "Invalid Value"
}
I have tried what Google recommends:
var token = req.headers.authorization.split(/[ ]+/);
client.verifyIdToken({
idToken: token[1],
audience: JSON.parse(process.env.valid_client_ids)
}).then((ticket) => {
gchatHandler.handleGChat(req.body, res);
}).catch(console.error);
And get the following error:
Error: No pem found for envelope: {"alg":"RS256","kid":"d...1","typ":"JWT"}
Any idea where I should head from here?
Edit: https://www.googleapis.com/service_accounts/v1/metadata/x509/chat#system.gserviceaccount.com found this, investigating how to use it. The kid matches the one I get.
Worked it out, eventually.
You need to hit: https://www.googleapis.com/service_accounts/v1/metadata/x509/chat#system.gserviceaccount.com to get a JSON file containing the keys linked to their KIDs.
Then when a request arrives, use jsonwebtoken (NPM) to decode the token and extract the KID from the header.
Use the KID to find the matching public key in the response from the website above, then use the verify function to make sure the token matches the public key.
You also need to pass the audience and issuer options to verify, to validate that it is your particular service account hitting the bot.
The solution above maybe the correct for Google Chat, but in my experience Google services (e.g. Google Tasks) use OIDC tokens, which can be validated with verifyIdToken function.
Adding my solution here, since your question/answer was the closest thing I was able to find to my problem
So, In case if you need to sign a request from your own code
on client, send requests with OIDC token
import {URL} from 'url';
import {GoogleAuth} from 'google-auth-library';
// will use default auth or GOOGLE_APPLICATION_CREDENTIALS path to SA file
// you must validate email of this identity on the server!
const auth = new GoogleAuth({});
export const request = async ({url, ...options}) => {
const targetAudience = new URL(url as string).origin;
const client = await auth.getIdTokenClient(targetAudience);
return await client.request({...options, url});
};
await request({ url: 'https://my-domain.com/endpoint1', method: 'POST', data: {} })
on the server, validate OIDC (Id token)
const auth = new OAuth2Client();
const audience = 'https://my-domain.com';
// to validate
const token = req.headers.authorization.split(/[ ]+/)[1];
const ticket = await auth.verifyIdToken({idToken: token, audience });
if (ticket.getPayload().email !== SA_EMAIL) {
throw new Error('request was signed with different SA');
}
// all good
Read more about Google OpenID Connect Tokens

Resources