How to use the "hub.secret" with web hooks?

I’m trying to use the hub.secret as part of my web hook request. I specify the secret, that I wish to use, when I request a subscription. It’s my understanding that web hook event messages will be “signed” with this secret. I’m unclear what it means by signed, but my assumption has been that the event messages will simply echo back to me my secret that I specified as part of the event data payload. I don’t see my secret anywhere, though.

Even in the example web hook workflow, it shows that a secret is specified but I see no further mention or reference to it.

Where is my secret supposed to show up?

Thanks!

I actually fought with this for a hour or two yesterday, because it isn’t directly documentated anywhere (i had to dig through the specification https://www.w3.org/TR/websub/#signature-validation).

What you have to do, when you receive the notification request from twitch (after your request was verified). Is to get the raw payload of the request and sign this one with your secret key, in the request there is also an additional header call x-hub-signature which also contains the signed payload (+ additionally the info which hash was used to create the signature). In twitches case the used hash algorithm is sha-256, if you compare the sha-256 hash of your received payload (using your secret key to hash it) with the x-hub-signature value in the header then the value should be the same to verify that it was hashed with the secret you provided initially.

I hope that helps a bit :slight_smile: and maybe twitch can adapt their documentation to offer more infos here (especially that they are using sha-256 would have been nice to know)

NodeJS/Javascript/express

var incoming = req.headers['x-hub-signature'].split('=');

var hash = crypto.createHmac(incoming[0], secret)
   .update(JSON.stringify(req.body))
   .digest('hex');

if (incoming[1] != hash) {
    console.log('Reject');
} else {
    console.log('Payload OK');// do other stuff
}

The above is out of date/incorrect. This middleware is more accurate/correct

router.use(bodyParser.json({
    verify: function(req, res, buf, encoding) {
        // is there a hub to verify against
        req.twitch_hub = false;
        if (req.headers && req.headers['x-hub-signature']) {
            req.twitch_hub = true;

            var xHub = req.headers['x-hub-signature'].split('=');

            req.twitch_hex = crypto.createHmac(xHub[0], hub_secret)
                .update(buf)
                .digest('hex');
            req.twitch_signature = xHub[1];
        }
    }
}));

router.route('/:type').post((req,res) => {
    res.send('OK');
    if (req.twitch_hub && req.twitch_hex == req.twitch_signature) {
        // tis good
    } else {
       // unverified
    }
});

Arg!

I’m writing this in C# and understand the required steps to verify the message body, thanks to @BarryCarlyon and @DoctorLoktor.

I’m having trouble getting the hashes to match up. The C# implementation of the HMAC SHA256 says that it pads the key if it’s not 64 bytes. I think I’m just having issues getting my hashes to match up due to possible implementation differences between twitch’s likely Node or Go implementation and this C# implementation.

Here is my Java Implementation of the Hashing it’s based on githubs webhook signature verification (maybe it helps you, did you encode the rawHmac before comparing it?):

public static String createSignatureWithSHA256(String secret, String payload)
        throws NoSuchAlgorithmException, InvalidKeyException {
    Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
    SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), HMAC_SHA256_ALGORITHM);
    mac.init(signingKey);
    byte[] rawHmac = mac.doFinal(payload.getBytes());
    return new String(encode(rawHmac));
}

private static char[] encode(byte[] bytes) {
    final int amount = bytes.length;
    char[] result = new char[2 * amount];

    int j = 0;
    for (int i = 0; i < amount; i++) {
        result[j++] = HEX[(0xF0 & bytes[i]) >>> 4];
        result[j++] = HEX[(0x0F & bytes[i])];
    }

    return result;
}

Thought I’d show how I’ve done it too.

const verifyNotice = (req, res, buf, encoding) => {
    const expected = req.headers['x-hub-signature'];
    const calculated = 'sha256=' + crypto.createHmac('sha256', secret).update(buf).digest('hex');
    
    req.verified = expected === calculated;
};

app.use(bodyParser.json({ verify: verifyNotice }));

Pretty much the same as Barry’s, I just do it as part of the body-parser middleware so that when I get a notification it’ll simply have a req.verified boolean value already available to me.

1 Like

Thanks for the code samples! Ya’ll helped me figure out what I was missing. I was missing the hex encoding at the end.

Here’s what I wrote for this. It’s a C# AspNet Web API authorization filter, which is the same concept as an ExpressJS middleware, etc, except that it runs explicitly as part of the authentication / authorization pipeline. This action filter looks for the twitch-provided hash header and allows or rejects the request …

public class TwitchSecretAuthorizationAttribute : AuthorizeAttribute
{
    #region Methods

    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        IEnumerable<string> headers;

        // if there is no secret header, we can reject right away
        if (actionContext.Request.Headers.TryGetValues("x-hub-signature", out headers) == false)
        {
            return false;
        }

        var header = headers.Single();

        // we will need to compare the provided hash with the hash that we
        // generate, using our secret
        var providedHash = header.Split('=').Last();

        // we need to convert our secret text to a byte array prior to
        // being used the hmac key
        var secretArray = Encoding.ASCII.GetBytes(PersistantRuntimeData.Guid);

        using (var hmac = new HMACSHA256(secretArray))
        {
            // we need to use the request body as the hash payload
            var body = actionContext.Request.Content.ReadAsStringAsync().Result;

            // now we can generate the resulting hash
            var result = hmac.ComputeHash(Encoding.ASCII.GetBytes(body));

            var stringed = BitConverter.ToString(result).Replace("-", "").ToLower();

            // we are authorized if both of the hashes match
            return providedHash.Equals(stringed, StringComparison.OrdinalIgnoreCase);
        }
    }

    #endregion
}
3 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.