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.
Related
There is some kind of encoding issue when I try to verify the signature for a Stripe Webhook. I know it’s not an issue with the Stripe package itself because I get different signatures when trying to manually hash the body data and compare the HMAC-256SHA signature with the signature from Stripe in the headers. I have tried so many different things to so many different parts, that it’s possible I have had multiple mistakes.
You’re not able to see here, but the IBM Cloud Function has been set to pass raw HTTP data, and that’s why you the decoding function being used.
The webhook is successful without verifying the signatures.
The error generated by the Stripe event function is, “No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe?”
Note: Errors are not handled correctly here while trying to debug this issue.
const stripe = require('stripe')('sk_test_********’);
var crypto = require('crypto');
// tried with the CLI secret and the one from the dashboard.
const endpointSecret = 'whsec_****’;
// Convert the stripe signature in string format to a JSON object
function sig_conversion(data){
var sig_obj = {}
var data_list = data.split(",").map((x)=>x.split("="));
var data_json = data_list.map((x)=>{sig_obj[x[0]] = x[1]})
return sig_obj
}
function decode(args) {
var decoded = new Buffer.from(args.__ow_body, 'base64')//.toString('utf-8')
return {body: decoded}
}
function main(params){
//let sig = sig_conversion(params.__ow_headers['stripe-signature']);
let sig = params.__ow_headers['stripe-signature']
let signature = sig_conversion(params.__ow_headers['stripe-signature']);
//console.log(222, signature);
var data = decode(params);
let event;
// Trying to see the result from manually checking the signatures.
var signed_payload = data.body + "." + signature.t
var hmac = crypto.createHmac('sha256', endpointSecret);
var hmac_sig = hmac.update(signed_payload);
var gen_hmac= hmac_sig.digest('hex');
console.log(gen_hmac, 222, signature, 444)
try {
event = stripe.webhooks.constructEvent(JSON.parse(data.body), sig, endpointSecret);
//event = JSON.parse(data.body);
}
Here are some steps to help people trying the same thing (some of the steps are general steps not directly related to the problem from above).
Ensure that web actions are enabled under the Endpoints menu.
Check the option for Raw HTTP handling under the same screen (Most of the documentation you will see is in relation to using Node.js + Express. The error that people experience in Express is the same, which is that the raw signature data in the header and the body data is needed for the verifying the signature. This applies to regardless of whether you are using Stripe’s package or manually verifying the signatures.)
Process the body data from ‘base64’ encoding.
If the endpoint secret from the Stripe CLI tool doesn’t work, try the one from the dashboard; and vice-versa.
Note: People using Google Cloud Functions or Pub-sub will likely have similar issues with signature verification.
function decode(args) {
var decoded = new Buffer.from(args.__ow_body, 'base64')
return {body: decoded}
}
// Match the raw body to content type application/json
function main(params){
let sig = params.__ow_headers['stripe-signature']
var data = decode(params);
let event;
try {
event = stripe.webhooks.constructEvent(data.body, sig, endpointSecret);
}
// The rest is the same as the stripe boilerplate code.
catch (err) {
return {
body: {payload:''},
statusCode:200,
headers:{ 'Content-Type': 'application/json'}
};
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('PaymentIntent was successful!')
break;
case 'payment_method.attached':
const paymentMethod = event.data.object;
console.log('PaymentMethod was attached to a Customer!')
break;
// ... handle other event types
default:
// Unexpected event type
return {
body: {payload:''},
statusCode:200,
headers:{ 'Content-Type': 'application/json'}
};
}
// Return a response to acknowledge receipt of the event
return {
body: {payload:''},
statusCode:200,
headers:{ 'Content-Type': 'application/json'}
};
};
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 }
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())
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) {
// ...
}
I'm working on authentication for my JSON-RPC API and my current working strategy is using signed requests sent via POST over SSL.
I'm wondering if anyone can see any vulnerabilities that I haven't taken into consideration with the following signature method.
All communication between the client and the server is done via POST requests sent over SSL. Insecure http requests are denied outright by the API server.
Dependencies
var uuid = require('node-uuid');
var crypto = require('crypto');
var moment = require('moment');
var MyAPI = require('request-json').newClient('https://api.myappdomain.com');
Dependency Links: node-uuid, crypto, moment, request-json
Vars
var apiVersion = '1.0';
var publicKey = 'MY_PUBLIC_KEY_UUID';
var secretKey = 'MY_SECRET_KEY_UUID';
Request Object
var request = {
requestID : uuid.v4(),
apiVersion : apiVersion,
nonce : uuid.v4(),
timestamp : moment.utc( new Date() ),
params : params
}
Signature
var signature = crypto.createHmac('sha512',secretKey).update(JSON.stringify(request)).digest('hex');
Payload Packaging (Sent as cleartext via POST over SSL)
var payload = {
request: request,
publicKey : publicKey,
signature : signature
}
Resultant Payload JSON Document
{
"request" : {
"requestID" : "687de6b4-bb02-4d2c-8d3a-adeacd2d183e",
"apiVersion" : "1.0",
"nonce" : "eb7e4171-9e23-408a-aa2b-cd437a78af22",
"timestamp" : "2014-05-23T01:36:52.225Z",
"params" : {
"class" : "User"
"method" : "getProfile",
"data" : {
"id" : "SOME_USER_ID"
}
}
},
"publicKey" : "PUBLIC_KEY",
"signature" : "7e0a06b560220c24f8eefda1fda792e428abb0057998d5925cf77563a20ec7b645dacdf96da3fc57e1918950719a7da70a042b44eb27eabc889adef95ea994d1",
}
POST Request
MyAPI.post('/', payload, function(response){
/// Handle any errors ...
/// Do something with the result ...
/// Inspect the request you sent ...
});
Server-Side
And then on the server-side the following occurs to authenticate the request:
PUBLIC_KEY is used to lookup the SECRET_KEY in the DB.
SECRET_KEY is used to create an HMAC of the request object from the payload.
The signature hash sent in the payload is compared to the hash of the request object created on the server. If they match, we move on to authenticating the timestamp.
Given that we can now trust the timestamp sent in the cleartext request object since it was included in the signature hash sent from the client, the timestamp is evaluated and the authentication is rejected if the request is too old. Otherwise, the request is authenticated.
So far as I understand, this is a secure method for signing and authentication requests sent over SSL. Is this correct?
Thanks in advance for any help.
Update on JSON Property Order
The order of properties when using JSON.stringify is essentially random, which could cause signature mis-matches.
Using this signing process over the past few weeks I haven't run into any hash mis-match issues due to the order of the properties in the JSON request object. I believe it's because I only stringify the request object literal once, right before the client-side hash is calculated. Then, the request object is in JSON format as part of the payload. Once received by the server, the hash is created directly from the JSON object received in the payload, there's no second JSON.stringify method invoked, so the signature always matches because the order of the properties is determined once, by the client. I'll keep looking into this though as it seems like a weak point, if not a security concern.
JSON.stringify does not guarantee order of properties. For example, object
{
a: 1,
b: 2
}
could be serialized in two ways: {"a":1,"b":2} or {"b":2,"a":1}. They are the same from JSON point of view but they will result it different HMACs.
Imaging, that for signings your JSON.stringify produced first form, but for checking signature second one. Your signature check will fail although signature was valid.
The only fishy thing I see here would be the JSON.stringify as posted in other comments, but you can use:
https://www.npmjs.com/package/json-stable-stringify
That way you can have a deterministic hash for you signs.