woocommerce webhooks verification always fail laravel php - php

anyone here knows how to obtain the woocommerce webhook secret or verify the incoming webhook request programmatically?
The problem I encountered is that my generated signature does not match with the signature passing in. According to the doc, the default secret should be the md5 hash of the current user id and the code I use to verify is as below:
$secret = md5(user_id);
$secret = $request->getContent();
$calculated_hmac = base64_encode(hash_hmac('sha256', $payload, $secret, true));
if ($signature != $calculated_hmac) {
return response(['Invalid key'], 401);
}
the calculated hmac is always wrong, which part did I do wrong or understand wrongly?

Related

How to verify a Paypal webhook notification DIY style (without using Paypal SDK)

Upon integrating the smart button of Paypal I have issues to verify webhook notifications sent by Paypal. The examples I have found are either outdated or do not work.
Is there a way to verify the webhook notifications, ideally in a DIY way (ie. without having to use the bulky and complex Paypal API)?
To the best of my knowledge, this code is only one that actually works. All other examples I have found on stack overflow will not work because instead of passing the ID of the webhook itself when composing the signature string, they use the ID of the webhook event, thus the verify will fail.
The webhook ID will be generated once you add the webhook in the developer backend of Paypal. After creation of the webhook you will see its id in the list of installed webhooks.
The rest is pretty straight forward: We get the headers and the HTTP body and compose the signature using Paypal's recipe:
To generate the signature, PayPal concatenates and separates these
items with the pipe (|) character.
"These items" are: The transmission id, the transmission date, the webhook id and a CRC over the HTTP body. The first two can be found in the header of the request, the webhook id in the developer backend (of course, that id will never change), the CRC is calculated like shown below.
The certificate's location is in the header, too, so we load it and extract the private key.
Last thing to watch out for: The name of the algorithm provided by Paypal (again in a header field) is not exactly the same as understood by PHP. Paypal calls it "sha256WithRSA" but openssl_verify will expect "sha256WithRSAEncryption".
// get request headers
$headers=apache_request_headers();
// get http payload
$body=file_get_contents('php://input');
// compose signature string: The third part is the ID of the webhook ITSELF(!),
// NOT the ID of the webhook event sent. You find the ID of the webhook
// in Paypal's developer backend where you have created the webhook
$data=
$headers['Paypal-Transmission-Id'].'|'.
$headers['Paypal-Transmission-Time'].'|'.
'[THE_ID_OF_THE_WEBHOOK_ACCORDING_TO_DEVELOPER_BACKEND]'.'|'.
crc32($body);
// load certificate and extract public key
$pubKey=openssl_pkey_get_public(file_get_contents($headers['Paypal-Cert-Url']));
$key=openssl_pkey_get_details($pubKey)['key'];
// verify data against provided signature
$result=openssl_verify(
$data,
base64_decode($headers['Paypal-Transmission-Sig']),
$key,
'sha256WithRSAEncryption'
);
if ($result==1) {
// webhook notification is verified
...
}
elseif ($result==0) {
// webhook notification is NOT verified
...
}
else {
// there was an error verifying this
...
}
Answering this for nodejs, as there are subtle security issues and some missing logic in original (but very helpful) answer. This answer addresses the following issues:
Someone putting in their own URL and thereby getting authentication of their own requests
CRC needs to be an unsigned integer, not a signed integer.
NodeJs < 17.0 is missing some built in X509 functionality.
Ideally one should validate the signing cert with the built in cert chain
but NodeJS < 17.0 can't do this easily AFAICT. The trust model relies on TLS and the built in nodejs trust chain for the cert fetch URL and not the returned cert from cert URL , which is probably good enough.
const forge = require('node-forge');
const crypto = require('crypto')
const CRC32 = require('crc-32');
const axios = require('axios');
const transmissionId = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-ID'];
const transmissionTime = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-TIME'];
const signature = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-SIG'];
const webhookId = '<your webhook ID from your paypal dev. account>';
const url = paypalSubsEvent.headers['PAYPAL-CERT-URL'];
const bodyCrc32 = CRC32.str(paypalSubsEvent.body);
const unsigned_crc = bodyCrc32 >>> 0; // found by trial and error
// verify domain is actually paypal.com, or else someone
// could spoof in their own cert
const urlObj = new URL(url);
if (!urlObj.hostname.endsWith('.paypal.com')) {
throw new Error(
`URL ${certUrl} is not in the domain paypal.com, refusing to fetch cert for security reasons`);
}
const validationString =
transmissionId + '|'
+ transmissionTime + '|'
+ webhookId + '|'
+ unsigned_crc;
const certResult = await axios.get(url); // Trust TLS to check the URL is really from *.paypal.com
const cert = forge.pki.certificateFromPem(certResult.data);
const publicKey = forge.pki.publicKeyToPem(cert.publicKey)
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(validationString);
verifier.end();
const result = verifier.verify(publicKey, signature, 'base64');
console.log(result);
You can use the following steps with Paypal API's
Create App and get the Client ID and Secret from the Developer dashboard
Create Webhook inside App and get a webhook ID
Implementation PayPal API's
https://www.postman.com/paypal/workspace/paypal-public-api-workspace/collection/19024122-92a85d0e-51e7-47da-9f83-c45dcb1cdf24?action=share&creator=22959279
Get the new Access token with help of Client ID and Secret, every time connect with PayPal.
4.Use the webhook Id, Access Token, and request Headers to verify the Webhook
try{
$json = file_get_contents('php://input');
$data = json_decode($json);
$paypalmode = ($this->dev_mode == 0) ? '' : '.sandbox';
$API_Endpoint = 'https://api-m' . $paypalmode . '.paypal.com/v1/';
//step-01 get token
$res_token = getToken($API_Endpoint);//get Token mention in above postman link
//step-02 validate webhook
$webhook_id = 'XXXXXX';
$post_data = array(
"webhook_id" => $webhook_id ,
"transmission_id" => $_SERVER['HTTP_PAYPAL_TRANSMISSION_ID'],
"transmission_time" => $_SERVER['HTTP_PAYPAL_TRANSMISSION_TIME'],
"cert_url" => $_SERVER['HTTP_PAYPAL_CERT_URL'],
"auth_algo" => $_SERVER['HTTP_PAYPAL_AUTH_ALGO'],
"transmission_sig" => $_SERVER['HTTP_PAYPAL_TRANSMISSION_SIG'],
"webhook_event" => $data
);
$res = verifyWebhook($API_Endpoint . 'notifications/verify-webhook-signature',
$res_token['access_token'], $post_data);//use postman 'verify-webhook-signature' api mention in webhook section
if (isset($res->verification_status) && $res->verification_status == 'SUCCESS') {
//success
}else{
//failure
}
} catch (Exception $ex) {
//error
}
Responding to this to save potential headaches but the above example does not work because an authentication token is needed to be sent along with your get request for the cert file "file_get_contents($header['Paypal-Cert-Url'])" will not work on its own.
Simply include your authentication token in the header and it'll work.

Oauth generate signature without any package

I have to integrate a web application with an API. The API uses OAuth 1.0 to authorize the requests. I have gone through the documentation at http://oauth.net/core/1.0a/. I have followed all the steps to generate the signature But the server always returns the message "invalid signature".
However, if I put my consumer key, consumer secret, access token and access token secret in POSTMAN. Then the signature that is generated by the POSTMAN is accepted by the server.
That means there is nothing wrong with the server. The problem is in the code that I am using to generate the signature.
Here is the code that I have written after reading the documentation to generate the tokens.
$consumer_key = '__MY_CONSUMER_KEY__';
$secret = '__MY__SECRET_KEY__';
$url = 'http://oauth.example.com/oauth/initiate';
$parameters = 'oauth_callback=oob&oauth_consumer_key='.$consumer_key.'&oauth_nonce='.dechex(time()).'&oauth_signature_method=HMAC-SHA1&oauth_timestamp='.time().'&oauth_version=1.0';
$signature = base64_encode(hash_hmac('sha1', 'GET&'.rawurlencode($url).'&'.rawurlencode($parameters), $secret, true));
$signed_request = $url.'?'.$parameters.'&oauth_signature='.$signature.PHP_EOL;
I know there are some predefined the packages available for this But I want to know what is the actual logic to generate the signature for OAuth 1.0.

Android InApp purchase verification

I have a mobile app with some inapp items that users can purchase. Once users buy some inapp product the app sends the JSON receipt to my server for online verification against my Google developer public key (stored in the server).
The app sends signature and data (aka the receipt) to the server:
$signature = 'E2dxlmSe0d45eJpN4FKSUxNPYXM5A1zohpVL60Hd+5jd43j4YMhBlVRLwFeDaBKZnkJ39rYYesWoOu8Z5ysczAIiQO7Myko7UJYVYKvB5GqM8a0iEDjCdCpSRSqLUmaEHKwUJFfjcgw1K5L2gM/m3u8l7Jy25IB+HFVIikO50jiy8SMRh7S+s6PgEAXqG6K6vTpuTC5ECweuQ45VTdb0jNyWOzEW/I1nA5fAB/mmp5j3B6k7nN81NMh/3oUJHba/wWGlbkWtItmDU6/jMdpd1CVViNBhKe0ktwnSRz3XF607/AfZM6JteOKhC6TquWhVNuWpKJWdJbP7Q+RVS0YKog==';
$data = '{"orderId":"GPA.xxxx-xxxx-xxxx-xxxxx","packageName":"xxx.xxx.xxx","productId":"xxx","purchaseTime":1508881024560,"purchaseState":0,"purchaseToken":"didpmjkaldaddakgfabdohdj.AO-J1Ozqb8hZAa-_FLd-sQJgXhwruU3tVEYU0sqhlgXHb8I9wI35xDeQFgFI0Zpoaurw4Ry7zahymvge1U0WlEqqvvAKvwAo0Wk1MtawzAiqVdy2RTvwFGo"}';
Here's the PHP code I'm using for signature verification:
$pkey = "...";
$apkey = "-----BEGIN PUBLIC KEY-----\n".chunk_split($pkey, 64, "\n")."-----END PUBLIC KEY-----";
$pubkeyid = openssl_get_publickey($apkey);
$ok = openssl_verify($data, $signature, $pubkeyid);
openssl_free_key($pubkeyid);
echo $ok;
and of course it doesn't work. The OpenSSL function returns 0 (instead of 1 for OK). According to the online documentation https://developer.android.com/google/play/billing/billing_integrate.html
the thing I have to check is INAPP_PURCHASE_DATA which is receipt. Here an example from the doc:
'{
"orderId":"GPA.1234-5678-9012-34567",
"packageName":"com.example.app",
"productId":"exampleSku",
"purchaseTime":1345678900000,
"purchaseState":0,
"developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
"purchaseToken":"opaque-token-up-to-1000-characters"
}'
Which is exactly what my app sends. Now, since signature verification requires bit perfect data, how I'm supposed to "send" such string to the OpenSSL function? On the doc the data has line breaks and indentation, mine is the very same JSON structure but recorded as a plain string without line breaks nor indentation. It's the same data JSON-wise but very different from a cryptographic signature verification point of view. Can someone please explain how to do it?
Solved the issue:
The data has to be a plain string with no indentation nor line breaks
signature has to be base64 decoded before passing it to the OpenSSL function
So instead of this:
$ok = openssl_verify($data, $signature, $pubkeyid);
I have to do this:
$ok = openssl_verify($data, base64_decode($signature), $pubkeyid);

Reverse JWT in PHP

I had created JWT in php.I had found the way to create JWT from following link.
JWT (JSON Web Token) in PHP without using 3rd-party library. How to sign?
<?php
//build the headers
$headers = ['alg'=>'HS256','typ'=>'JWT'];
$headers_encoded = base64_encode(json_encode($headers));
//build the payload
$payload = ['sub'=>'1234567890','name'=>'John Doe', 'admin'=>true];
$payload_encoded = base64_encode(json_encode($payload));
//build the signature
$key = 'secret';
$signature = hash_hmac('SHA256',"$headers_encoded.$payload_encoded",$key,true);
$signature_encoded = base64_encode($signature);
//build and return the token
$token = "$headers_encoded.$payload_encoded.$signature_encoded";
echo $token;
?>
Now how can i authenticate it. I am sending token from Android but i want to validate that this is proper token or not. So how can i do it in code before fulfilling the request.
Should i store token in database?
And is it proper way to give security to api?
I highly recommend using a well known JWT library for this. This is cryptography, and rolling your own crypto is usually dangerous. There are a few packages around with widespread adoption that have been vetted by security professionals.
If you are going to do this manually, at least take inspiration from one of these packages to ensure that you're doing it correctly: https://github.com/firebase/php-jwt/blob/master/src/JWT.php#L69-L138
The linked code is pretty easy to follow. Essentially you're:
Decoding the token by splitting on ., base64_decodeing, and then json_decodeing.
Checking the signature of the provided JWT against one that is computed again from the decoded header and payload. The alg header property in your example will tell you what algorithm to use to check the signature.
Short solution for your example :
public function verify(string $token, string $secret): bool
{
[$headerEncoded, $bodyEncoded, $signatureEncoded] = explode('.', $token);
$signature = base64_decode($signatureEncoded);
$hash = hash_hmac('sha256', implode('.', [$headerEncoded, $bodyEncoded]), $secret, true);
return \hash_equals($signature, $hash);
}
But you sohuld encode and decode string with URL-Safe Base64.

The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method

My test request for Amazon Web Service API is returning with the following error:
<ItemSearchErrorResponse xmlns="http://ecs.amazonaws.com/doc/2005-10-05/">
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>
The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
</Message>
</Error>
<RequestId>ebe90459-1abd-403e-bfec-6916ebe07e1f</RequestId>
</ItemSearchErrorResponse>
Here is how I arrived at this:
<?php
$private_key = "XXXXXXXXXXXXXXXXXXXXXX";
$date = urlencode(date('Y-m-d\TH:i:s.Z\Z', time()));
$string_to_sign = "GET webservices.amazon.com /onca/xml/ AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXXXXXA&Keywords=Rocket&Operation=ItemSearch&SearchIndex=Toys&Service=AWSECommerceService&Timestamp=".$date."";
$signature = urlencode(base64_encode(hash_hmac("sha256",$string_to_sign, $private_key, True)));
$url = "http://webservices.amazon.com/onca/xml?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXXXXXX&Keywords=Rocket&Operation=ItemSearch&SearchIndex=Toys&Service=AWSECommerceService&Timestamp=".$date."&Signature=".$signature."";
header("Location: ".$url."");
?>
I basically wanted a signed REST request directly in the url (hence the header function) but left me stuck with the error. I double checked my access id and secret key so I know they are not the one causing the problem. Is this not the right way to produce the signature?
It seems you're trying to use a V2 signature to call the REST API of AWS.
Please refer to the complete AWS REST API call documentation.
My bet is the problem is the absence of SignatureVersion, SignatureMethod and Version in the string before signing. Step five of the documentation linked above shows and example of the string to be signed:
GET\n
elasticmapreduce.amazonaws.com\n
/\n
AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Action=DescribeJobFlows&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-10-03T15%3A19%3A30&Version=2009-03-31
This is AWS talk for: "Your credentials are incorrect".

Categories