Topics covering webhook creation & management, event handling, Pub/Sub, and Eventbridge, in Shopify apps.
Hi,
I have been looking for documentation on how to verify webhooks for public apps and I am unable to find any. From, what I have gathered, private apps use a WEBHOOK_SIGNED_KEY available at the store admin level. I thought using API Key / API Secret could be an alternative for public apps, but that doesn't seem to be the case. The below code is what I have tried up till now. Thank you.
def verify_hmac(secret, body, shopify_hmac):
hash_code = hmac.new(secret.encode('utf-8'), body, hashlib.sha256)
computed_hmac = base64.b64encode(hash_code.digest()).decode()
return computed_hmac == shopify_hmac
@csrf_exempt
def save_webook_payload(request):
if request.method == 'POST':
shopify_hmac = request.headers.get('X-Shopify-Hmac-Sha256')
if verify_hmac(SHOPIFY_API_SECRET, request.body, shopify_hmac):
return JsonResponse( { 'data': 'Payload Recieved'}, safe = False )
else:
raise Http404("No such Page")
raise Http404("No such Page")
Solved! Go to the solution
This is an accepted solution.
If you refer to this link, there's a Ruby version of the routine to validate the HMAC signature sent with the webhook request --> https://shopify.dev/tutorials/manage-webhooks. You should be able to use that as a basis for your Python code. Hope this helps!
This is an accepted solution.
If you refer to this link, there's a Ruby version of the routine to validate the HMAC signature sent with the webhook request --> https://shopify.dev/tutorials/manage-webhooks. You should be able to use that as a basis for your Python code. Hope this helps!
Thanks, still can't get computed_hmac and shopify_hmac to match. I have tried the app API secret key.
Hi, i have same issue, can you resolve it ?
Tks in advance.
Sure, let me know what your problem is.
have you managed to get the computed_hmac and the shopify_hmac to match?
I've been looking for a solution to this problem as well without anyone giving me an answer
cheers,
Gary
I was able to in my projects. I can provide sample code for this, although it's in C#. Also have a JavaScript version somewhere around I can look for...
Here is sample code in C# that compares the provided HMAC against the calculated one. Works like a charm in production for our app proxy deployment.
var query = "path_prefix=" + path_prefix
+ "shop=" + shop
+ "timestamp=" + timestamp;
var secret_key = "MY_SECRET_KEY";
var ascii = new ASCIIEncoding();
var keyBytes = ascii.GetBytes(secret_key);
using (var hmac = new HMACSHA256(keyBytes))
{
var queryBytes = ascii.GetBytes(query);
var hashedQuery = hmac.ComputeHash(queryBytes);
var computedHash = ToHexString(hashedQuery);
if (computedHash == signature)
{
// Hooray!!!
}
else
{
// Boo!!!
}
}
private static string ToHexString(byte[] array)
{
StringBuilder hex = new StringBuilder(array.Length * 2);
foreach (byte b in array)
{
hex.AppendFormat("{0:x2}", b);
}
return hex.ToString();
}
Hey Gary,
Try this. Ignore some of the imports.
Sorry, as my post was for verifying the app proxy calls, not the webhooks. This is directly from the Shopify GitHub example page. Ruby code...
def validate_hmac(hmac,request)
h = request.params.reject{|k,_| k == 'hmac' || k == 'signature'}
query = URI.escape(h.sort.collect{|k,v| "#{k}=#{v}"}.join('&'))
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), API_SECRET, query)
unless (hmac == digest)
return [403, "Authentication failed. Digest provided was: #{digest}"]
end
end
def verify_webhook(hmac, data)
digest = OpenSSL::Digest.new('sha256')
calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, API_SECRET, data)).strip
hmac == calculated_hmac
end
Hi Greg,
WTF they aren't checking the body contents of the webhook but only the URL params minus the hmac and/or signature param? I will give it a try tomorrow, if it works the documentation is total BS.
thanks for the reply!
Yes, please keep us posted. As I said, the app proxy call is what provides the HMAC validation elements in the actual URL as query parameters. The webhook provides the HMAC value in the header, which you have to factor the actual request body in order to compute things. Different ways to handle, but the last code I posted demonstrates how to work with either scenario,
Could you have to solve my same issue on NodeJS ?
I tried to use shared key or signed webhook key but hmac and verified mac are never not matched.
Please help. This is my code:
// MY CODEconst hmac = req.header('X-Shopify-Hmac-Sha256')const data = JSON.stringify(req.body)console.log(data)let secretKey = SHOPIFY_SECRET_KEY// secretKey = SIGNED_WEBHOOK_KEYconst verifiedHmac = crypto.createHmac('sha256', secretKey).update(data).digest('base64')console.log(verifiedHmac)console.log(hmac)// HMAC OUPUT:yrFvoapW6QJpRRPinNshHRKIj2zUvJYrFpJEIZkKXEg=slF4b174ph+yRruzJAw/KiqaA7SUYOM8iL93FjFKB6g=
// DATA:{"id":820982911946154500,"email":"jon@doe.ca","closed_at":null,"created_at":"2021-07-17T16:45:52+07:00","updated_at":"2021-07-17T16:45:52+07:00","number":234,"note":null,"token":"123456abcd","gateway":null,"test":true,"total_price":"255","subtotal_price":"245","total_weight":0,"total_tax":"0","taxes_included":false,"currency":"VND","financial_status":"voided","confirmed":false,"total_discounts":"5","total_line_items_price":"250","cart_token":null,"buyer_accepts_marketing":true,"name":"#9999","referring_site":null,"landing_site":null,"cancelled_at":"2021-07-17T16:45:52+07:00","cancel_reason":"customer","total_price_usd":null,"checkout_token":null,"reference":null,"user_id":null,"location_id":null,"source_identifier":null,"source_url":null,"processed_at":null,"device_id":null,"phone":null,"customer_locale":"en","app_id":null,"browser_ip":null,"landing_site_ref":null,"order_number":1234,"discount_applications":[{"type":"manual","value":"5.0","value_type":"fixed_amount","allocation_method":"each","target_selection":"explicit","target_type":"line_item","description":"Discount","title":"Discount"}],"discount_codes":[],"note_attributes":[],"payment_gateway_names":["visa","bogus"],"processing_method":"","checkout_id":null,"source_name":"web","fulfillment_status":"pending","tax_lines":[],"tags":"","contact_email":"jon@doe.ca","order_status_url":"https://ducstore3.myshopify.com/55670145208/orders/123456abcd/authenticate?key=abcdefg","presentment_currency":"VND","total_line_items_price_set":{"shop_money":{"amount":"250","currency_code":"VND"},"presentment_money":{"amount":"250","currency_code":"VND"}},"total_discounts_set":{"shop_money":{"amount":"5","currency_code":"VND"},"presentment_money":{"amount":"5","currency_code":"VND"}},"total_shipping_price_set":{"shop_money":{"amount":"10","currency_code":"VND"},"presentment_money":{"amount":"10","currency_code":"VND"}},"subtotal_price_set":{"shop_money":{"amount":"245","currency_code":"VND"},"presentment_money":{"amount":"245","currency_code":"VND"}},"total_price_set":{"shop_money":{"amount":"255","currency_code":"VND"},"presentment_money":{"amount":"255","currency_code":"VND"}},"total_tax_set":{"shop_money":{"amount":"0","currency_code":"VND"},"presentment_money":{"amount":"0","currency_code":"VND"}},"line_items":[{"id":487817672276298560,"variant_id":null,"title":"Aviator sunglasses","quantity":1,"sku":"SKU2006-001","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":788032119674292900,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"Aviator sunglasses","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"grams":100,"price":"90","total_discount":"0","fulfillment_status":null,"price_set":{"shop_money":{"amount":"90","currency_code":"VND"},"presentment_money":{"amount":"90","currency_code":"VND"}},"total_discount_set":{"shop_money":{"amount":"0","currency_code":"VND"},"presentment_money":{"amount":"0","currency_code":"VND"}},"discount_allocations":[],"duties":[],"admin_graphql_api_id":"gid://shopify/LineItem/487817672276298554","tax_lines":[]},{"id":976318377106520300,"variant_id":null,"title":"Mid-century lounger","quantity":1,"sku":"SKU2006-020","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":788032119674292900,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"Mid-century lounger","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"grams":1000,"price":"160","total_discount":"5","fulfillment_status":null,"price_set":{"shop_money":{"amount":"160","currency_code":"VND"},"presentment_money":{"amount":"160","currency_code":"VND"}},"total_discount_set":{"shop_money":{"amount":"5","currency_code":"VND"},"presentment_money":{"amount":"5","currency_code":"VND"}},"discount_allocations":[{"amount":"5","discount_application_index":0,"amount_set":{"shop_money":{"amount":"5","currency_code":"VND"},"presentment_money":{"amount":"5","currency_code":"VND"}}}],"duties":[],"admin_graphql_api_id":"gid://shopify/LineItem/976318377106520349","tax_lines":[]}],"fulfillments":[],"refunds":[],"total_tip_received":"0.0","original_total_duties_set":null,"current_total_duties_set":null,"admin_graphql_api_id":"gid://shopify/Order/820982911946154508","shipping_lines":[{"id":271878346596884000,"title":"Generic Shipping","price":"10","code":null,"source":"shopify","phone":null,"requested_fulfillment_service_id":null,"delivery_category":null,"carrier_identifier":null,"discounted_price":"10","price_set":{"shop_money":{"amount":"10","currency_code":"VND"},"presentment_money":{"amount":"10","currency_code":"VND"}},"discounted_price_set":{"shop_money":{"amount":"10","currency_code":"VND"},"presentment_money":{"amount":"10","currency_code":"VND"}},"discount_allocations":[],"tax_lines":[]}],"billing_address":{"first_name":"Bob","address1":"123 Billing Street","phone":"555-555-BILL","city":"Billtown","zip":"K2P0B0","province":"Kentucky","country":"United States","last_name":"Biller","address2":null,"company":"My Company","latitude":null,"longitude":null,"name":"Bob Biller","country_code":"US","province_code":"KY"},"shipping_address":{"first_name":"Steve","address1":"123 Shipping Street","phone":"555-555-SHIP","city":"Shippington","zip":"40003","province":"Kentucky","country":"United States","last_name":"Shipper","address2":null,"company":"Shipping Company","latitude":null,"longitude":null,"name":"Steve Shipper","country_code":"US","province_code":"KY"},"customer":{"id":115310627314723950,"email":"john@test.com","accepts_marketing":false,"created_at":null,"updated_at":null,"first_name":"John","last_name":"Smith","orders_count":0,"state":"disabled","total_spent":"0.00","last_order_id":null,"note":null,"verified_email":true,"multipass_identifier":null,"tax_exempt":false,"phone":null,"tags":"","last_order_name":null,"currency":"VND","accepts_marketing_updated_at":null,"marketing_opt_in_level":null,"admin_graphql_api_id":"gid://shopify/Customer/115310627314723954","default_address":{"id":715243470612851200,"customer_id":115310627314723950,"first_name":null,"last_name":null,"company":null,"address1":"123 Elm St.","address2":null,"city":"Ottawa","province":"Ontario","country":"Canada","zip":"K2H7A8","phone":"123-123-1234","name":"","province_code":"ON","country_code":"CA","country_name":"Canada","default":true}}}
For your "data" variable there, you're going to need to get the "raw body" instead. Getting this will vary by whichever body parsing package you're using, but this should point you in the right direction.
Ive tried every manner and I can't get it to work with webhooks created through the API.. verifying webhooks created for private apps works without issue using the example methods provided on https://shopify.dev/apps/webhooks.
e.g. in java using the described method
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class Main {
public static void main(String[] args) {
try {
String secret = "shpss_xxx";
String message = "raw_json_body";
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] hash = sha256_HMAC.doFinal(message.getBytes());
System.out.println(Base64.getEncoder().encodeToString(hash));
}
catch (Exception e){
System.out.println("Error");
}
}
}
This never returns the correct hash.. I would really finally like to have someone from shopify or another developer show me a working example for validating webhooks created via the API for public apps.
Cheers and thanks
Replace
System.out.println(Base64.getEncoder().encodeToString(hash));
with
System.out.println(encodeHexString(hash));
and add the following methods to your class
public static String byteToHex(byte num) {
char[] hexDigits = new char[2];
hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16);
hexDigits[1] = Character.forDigit((num & 0xF), 16);
return new String(hexDigits);
}
public static String encodeHexString(byte[] byteArray) {
StringBuffer hexStringBuffer = new StringBuffer();
for (int i = 0; i < byteArray.length; i++) {
hexStringBuffer.append(byteToHex(byteArray[i]));
}
return hexStringBuffer.toString();
}