Solved

Verifying Shopify Webhooks for Public apps

shahrukhAhmed
Shopify Partner
30 0 3

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")

 

Accepted Solution (1)

Gregarican
Shopify Partner
1033 86 285

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!

View solution in original post

Replies 15 (15)

Gregarican
Shopify Partner
1033 86 285

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!

shahrukhAhmed
Shopify Partner
30 0 3

Thanks, still can't get computed_hmac and shopify_hmac to match. I have tried the app API secret key.  

syscon3
Visitor
2 0 0

Hi, i have same issue, can you resolve it ?

Tks in advance.

shahrukhAhmed
Shopify Partner
30 0 3

Sure, let me know what your problem is. 

garyrgilbert
Shopify Partner
388 40 159

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

- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
Gregarican
Shopify Partner
1033 86 285

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...

Gregarican
Shopify Partner
1033 86 285

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();
}
shahrukhAhmed
Shopify Partner
30 0 3

Hey Gary, 

Try this. Ignore some of the imports. 

 

carbon (1).png

Gregarican
Shopify Partner
1033 86 285

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
garyrgilbert
Shopify Partner
388 40 159

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!

- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
Gregarican
Shopify Partner
1033 86 285

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, 

dustinvn
Visitor
2 0 0

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 CODE
const hmac = req.header('X-Shopify-Hmac-Sha256')
const data = JSON.stringify(req.body)
console.log(data)
let secretKey = SHOPIFY_SECRET_KEY
// secretKey = SIGNED_WEBHOOK_KEY
const 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}}}


 

limeforadime
Shopify Partner
5 0 4

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.

 

garyrgilbert
Shopify Partner
388 40 159

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

- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
ano11
Shopify Partner
1 0 0

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();
	}