SHA256 webhook signature from WooCommerce never verifies
Asked Answered
P

8

9

I am receiving webhooks from a woocommerce site into a nodejs/express application. I am trying to verify the webhook's signature to prove authenticity, yet the hash I compute never corresponds to the signature that woocommerce reports in the hook's signature header.

Here is the code I am using to verify the authenticity:

function verifySignature(signature, payload, key){     
    var computedSignature = crypto.createHmac("sha256", key).update(payload).digest('base64');
    debug('computed signature: %s', computedSignature);
    return computedSignature === signature;
  }

This function is being called with the following parameters:

var signature = req.headers['x-wc-webhook-signature'];
verifySignature(signature, JSON.stringify(req.body), config.wooCommence.accounts.api[config.env].webhookSecret)

The webhook's signature headers reports the signature as BewIV/zZMbmuJkHaUwaQxjX8yR6jRktPZQN9j2+67Oo=. The result of the above operation, however, is S34YqftH1R8F4uH4Ya2BSM1rn0H9NiqEA2Nr7W1CWZs=

I have manually configured the secret on the webhook, and as you see in the code above, this same secret is also hardcoded in the express application. So either I am taking the wrong payload to compute the signature, or there is something else fishy that prevents me from verifying these signature.

Would appreciate any pointers to help me solve this issue.

Pleinair answered 20/8, 2015 at 15:47 Comment(1)
The answer by Johannes below should be accepted. Following his method worked perfectly for me. I'm going to post an ASP.net solution to this once I get it cleaned up.Teirtza
B
7

Since this is the top Google result for this question and there isn't a complete answer out there, here's a Python version using Flask that validates the WooCommerce webhook signature. It took a bit of trial and error, hope it helps someone out there:

import json
import base64
import hmac
import hashlib

from flask import Flask, request, Response

app = Flask(__name__)

# The WooCommerce webhook secret
WEBHOOK_SECRET = 'abc123456'

# Function that compares the computed signature to the one in the request
def verify_woocommerce_signature(body, signature, secret):
    digest = hmac.new(bytes(secret, 'utf-8'), body, hashlib.sha256).digest()
    encoded = base64.b64encode(digest).decode()

    return encoded == signature

# WooCommerce Order Creation Event
@app.route('/webhooks/woocommerce/order_created', methods=['POST'])
def webhooks_woocommerce_order_created():
    # Get raw request body
    body = request.get_data()
    
    # Get request signature
    signature = request.headers['X-WC-WEBHOOK-SIGNATURE']
    
    # Verify webhook signature and handle mismatch
    if verify_woocommerce_signature(body, signature, WEBHOOK_SECRET) is False:
        msg = {"success": False}
        return Response(json.dumps(msg), status=400, mimetype='application/json')

    # Signatures match, process the payload
Boehmite answered 30/6, 2020 at 8:12 Comment(2)
Works like a charm. Thank you!Venery
Thank you! It saved me many hours for me! Just to mention, for Django you can get a raw request body with request.body. The rest of the code is similar.Metrics
D
6

For people using node, this should do the trick.

var processWebHookSignature = function (secret, body, signature) {
  signatureComputed = crypto.createHmac('SHA256', secret).update(
    new Buffer(JSON.stringify(body), 'utf8')).digest('base64');

  return ( signatureComputed === signature ) ? true : false;
}
Donor answered 11/2, 2017 at 17:56 Comment(1)
this solution worked for me after some minor changes located here #38933995Mireyamiriam
M
4

Old question but maybe it helps some poor soul out there. The signature needs to be checked against the body and not the JSON it contains. i.e. the raw bytes of the body.

pseudo:

        byte[] body = request.Body;
        string signature = request.Header["X-WC-Webhook-Signature"];

        byte[] secretUtf8 = GetUtf8Bytes("yoursecrethere");
        byte[] hash = HMAC_SHA256.ComputeHash(body, secretUtf8);
        string hashBase64 = ToBase64String(hash);

        bool isValid = hashBase64 == signature;
Martinamartindale answered 25/7, 2016 at 19:54 Comment(0)
T
3

I stumbled upon this while searching for a solution to have an Asp.NET application check signature of the Woocommerce web hook. My answer is based on the pseudo code Johannes provided which worked great. I implemented a custom controller attribute to intercept the request and check the signature before it hits the API controller method:

public class HmacSignatureFilter : ActionFilterAttribute
{

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var requestContent = actionContext.Request.Content;
        var jsonContent = requestContent.ReadAsStringAsync().Result;
        var byteContent = requestContent.ReadAsByteArrayAsync().Result;

        //if the request contains this, it's the verification request from Woocommerce
        //when the webhook is created so let it pass through so it can be verified
        if (!jsonContent.Contains("webhook_id"))
        {
            var requestSignature = actionContext.Request.Headers;

            var bodyHash = HashHMAC("test", byteContent); //this is the shared key between Woo and custom API.  should be from config or database table.

            var signature = actionContext.Request.Headers.GetValues("x-wc-webhook-signature");

            if (bodyHash != signature.FirstOrDefault())
            {
                throw new HttpResponseException(HttpStatusCode.Forbidden);
            }
        }

        base.OnActionExecuting(actionContext);
    }


    private static string HashHMAC(string key, byte[] message)
    {
        var keyBytes = Encoding.UTF8.GetBytes(key);
        var hash = new HMACSHA256(keyBytes);

        var computedHash = hash.ComputeHash(message);
        return Convert.ToBase64String(computedHash);
    }
}

Then to use the filter in your Api controller:

[RoutePrefix("api/woo")]
public class WooController : ApiController
{

    public SomeService _service;

    public WooController()
    {
        this._service = new SomeService();
    }

    // POST api/values
    [Route("orderCreated")]
    [HttpPost]
    [HmacSignatureFilter]
    public string Post()
    {
        var requestContent = Request.Content;
        var jsonContent = requestContent.ReadAsStringAsync().Result;

        //this is the test request from Woocommerce.  Don't do anything but 
        //respond so it can verify the endpoint
        if (jsonContent.Contains("webhook_id"))
        {
            return "Webhook Test Success";
        }

        var wooOrder = JsonConvert.DeserializeObject<WooOrderModel>(jsonContent);

        //call a service to use the order data provided by WooCommerce
        _service.AddOrder(wooOrder);

        return "Success";
    }

}

Note: Hashing code was referenced from this SO post.

Teirtza answered 10/2, 2017 at 19:30 Comment(1)
I had to do this to read the content: string jsonContent; byte[] byteContent = null; using (var stream = new StreamReader(actionContext.Request.Content.ReadAsStreamAsync().Result)) { stream.BaseStream.Position = 0; jsonContent = stream.ReadToEnd(); stream.BaseStream.Position = 0; using (var memstream = new MemoryStream()) { stream.BaseStream.CopyTo(memstream); byteContent = memstream.ToArray(); } }Through
T
1

SOLVED in TypeScript. I added this in the server.ts:

this.app.use(bodyParser.json({
            verify: function(req, res, buf) {
                (req as any).rawBody = buf;
            }
        }));

and than:

 const computedSignature = crypto.createHmac("sha256", process.env.WOOCOMMERCE_SECRET).update((req as any).rawBody).digest("base64");
Tendril answered 7/6, 2021 at 8:24 Comment(1)
bodyParser.json is deprecated, now it should be express.json({...})Pinwork
P
0

Hash must be calculated over the 'raw body'. When used in an 'express application' and using JSON bodyParser middleware 'raw body' is lost, see How to access the raw body of the request before bodyparser? to hold-on to the 'raw body'.

For example:

// 'misuse' verify option  
app.use(bodyParser.json({
  verify: function(req,res,buf) { 
    req.rawBody=buf; 
  }
}));

var wcSignature = req.get('X-Wc-Webhook-Signature');
debug('wc signature: %s', wcSignature);
var calculatedSignature = crypto.createHmac('SHA256', secret)
  .update(req.rawBody, 'utf8')
  .digest('base64');
debug('calculated signature: %s', calculatedSignature);
Palate answered 5/8, 2017 at 17:30 Comment(0)
D
0

Hope to save someone time, below works for me.

// Make sure to add a WISTIA_SECRET_KEY in your Environment Variables
// See https://docs.pipedream.com/environment-variables/
const secret = process.env.SELF_AUTOMATE_KEY;
const signature = event.headers["x-wc-webhook-signature"];
const body = steps.trigger.raw_event["body_b64"];
const clean_Body = body.replace("body_b64: ", "");
//const body = steps.trigger.raw_event;
console.log(event.headers["x-wc-webhook-signature"]);

console.log("Print Body", clean_Body);

if (process.env.SELF_AUTOMATE_KEY === undefined) {
  $end("No WISTIA_SECRET_KEY environment variable defined. Exiting.")
}

if (!("x-wc-webhook-signature" in event.headers)) {
  $end("No x-wc-webhook-signature header present in the request. Exiting.")
}

// Once we've confirmed we have a signature, we want to 
// validate it by generating an HMAC SHA-256 hexdigest
const crypto = require('crypto');

const hash = crypto.createHmac('sha256',
  secret).update(JSON.stringify(clean_Body), 'base64').digest('base64');



console.log(hash);
// $end() ends the execution of a pipeline, presenting a nice message in the "Messages"
// column in the inspector above. See https://docs.pipedream.com/notebook/code/#end
if (hash !== signature) {
  $end("The correct secret key was not passed in the event. Exiting!")
}
Duntson answered 18/4, 2020 at 9:20 Comment(0)
H
0

SOLVED IN C# API. For people using it as an Attribute in a C# API for a controller, I made a good solution with multiple threads on StackOverflow:

[AttributeUsage(AttributeTargets.Method)]
public class ShaKeyAttribute : Attribute, IAuthorizationFilter
{
    private readonly ShaAuthOptions _options;
    private readonly ILogger<ShaKeyAttribute> _log;

    public ShaKeyAttribute(IOptions<ShaAuthOptions> options, ILogger<ShaKeyAttribute> log)
    {
        this._options = options.Value;
        this._log = log;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        this._log.LogInformation("Entering in Sha Auth...");
        if (!context.HttpContext.Request.Headers.TryGetValue("x-wc-webhook-signature", out var extractedSignature))
        {
            this._log.LogError("Sha Auth failed. Signature was not provided");

            context.Result = new ContentResult()
            {
                StatusCode = 401,
                Content = "Signature was not provided",
            };
            return;
        }

        // This logic for body rewind comes from: https://mcmap.net/q/125653/-how-to-read-request-body-in-an-asp-net-core-webapi-controller
        string bodyString;
        var req = context.HttpContext.Request;

        // Allows using several time the stream in ASP.Net Core
        req.EnableBuffering();

        // Arguments: Stream, Encoding, detect encoding, buffer size
        // AND, the most important: keep stream opened
        using (StreamReader reader
            = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
        {
            bodyString = reader.ReadToEnd();
        }

        // Rewind, so the core is not lost when it looks the body for the request
        req.Body.Position = 0;

        // From this point, DON'T TOUCH THE REQUEST BODY. Instead, use bodyString
        // https://mcmap.net/q/1171550/-woocommerce-webhook-c-compare-hash
        byte[] requestData = Encoding.UTF8.GetBytes(bodyString);
        var encoding = new UTF8Encoding();
        var key = this._options.Secret;
        var keyBytes = encoding.GetBytes(key);
        var hash = new HMACSHA256(keyBytes);
        var computedHash = hash.ComputeHash(requestData);
        var computedHashString = Convert.ToBase64String(computedHash);

        if (extractedSignature != computedHashString)
        {
            this._log.LogError("Sha Auth failed. Signature is not valid");

            context.Result = new ContentResult()
            {
                StatusCode = 401,
                Content = "Signature is not valid",
            };
        }

        this._log.LogInformation("Successfully passed Sha Auth.");
    }
}

You also need to add this in your startup.cs:

// https://mcmap.net/q/102341/-asp-net-core-synchronous-operations-are-disallowed-call-writeasync-or-set-allowsynchronousio-to-true-instead
// Needed for ShaAuth
services.Configure<KestrelServerOptions>(options =>
  {
       options.AllowSynchronousIO = true;
   });

Here is my controller:

    [AllowAnonymous]
    [HttpPost]
    [ServiceFilter(typeof(ShaKeyAttribute))]
    public async Task<IActionResult> Create([FromBody] WooCommerceRequest request)
    {
        this._log.LogInformation("Executing Create of Subscription controller...");

        var response = await this._mediator.Send(new AddSubscriptionCommand() { OrderId = request.OrderId });

        this._log.LogInformation("Finished executing Create of Subscription controller.");

        return this.Ok();
    }

Hope this help somebody.

Heilner answered 24/8, 2021 at 16:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.