Skip to content

Instantly share code, notes, and snippets.

@jirawatee
Last active February 5, 2025 11:35
Show Gist options
  • Save jirawatee/366d6bef98b137131ab53dfa079bd0a4 to your computer and use it in GitHub Desktop.
Save jirawatee/366d6bef98b137131ab53dfa079bd0a4 to your computer and use it in GitHub Desktop.
Code sample for verifying signature from LINE webhook
let text = JSON.stringify(req.body)
text = text.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, (e) => {
return "\\u" + e.charCodeAt(0).toString(16).toUpperCase() + "\\u" + e.charCodeAt(1).toString(16).toUpperCase()
})
const signature = crypto.createHmac('SHA256', LINE_CHANNEL_SECRET).update(text).digest('base64').toString()
if (signature !== req.headers['x-line-signature']) {
return res.status(401).send('Unauthorized')
}
@stephenhand
Copy link

Thanks for the video & this gist, it helped solve my issue

The above code is a little broken though, it tries to escape 2 characters in all cases, even though the regex can match single unicode characters. This would result in NaNs polluting the string and causing the incorrect signature to be generated.

This replacer arrow function should work better:

(emojiChars) =>
    emojiChars
      .split('')
      .map((c) => `\\u${c.charCodeAt(0).toString(16).toUpperCase()}`)
      .join('')

Also, I'm not sure exactly what characters you specifically seek to match, but if it's emojis, using the built in unicode matchers might be cover more cases, i.e. /\p{Emoji_Presentation}/gu

@Yang-33
Copy link

Yang-33 commented Aug 13, 2024

When validating a webhook payload with x-line-signature, you should not parse original request and use JSON#stringify to get original request from object again🙅. The signature validation must be done without making any changes to the received data.

LINE's webhook does not escape data on LINE Platform, nor does it force the webhook server to use such workarounds. If signature validation fails, it could be due to one of the following reasons: (your webhook server has issue or there are malicious requests)

  1. A malicious request is sent to your server.
  2. The webhook is incorrectly transformed before verification.
  3. The channel secret has expired.

At least, this issue has not been observed when I create a Node.js server on Cloudflare. This suggests that the problem might be specific to certain serverless environments or just application.

Incorrect Transformation with JSON.stringify

For example, if the received webhook data is "{\"a\": \"b✊🤨\"}", parsing and then stringifying may not restore the exact same string(=original request payload). In other words, "{\"a\": \"b✊🤨\"}" and JSON.stringify({"a": "b✊🤨"}) may not match. Additionally, if the serverless environment transforms the request before your application starts requests, it can lead to signature validation failure.

In a Node.js environment, using Express(https://github.com/expressjs/express) along with body-parser(https://github.com/expressjs/body-parser) is one option to solve this issue. You need to verify the received webhook payload without transforming it. This can be achieved with Express, Hono(https://github.com/honojs/hono), or other libraries(or without any library) as long as the webhook is not transformed.

@Yang-33
Copy link

Yang-33 commented Aug 23, 2024

@jirawatee
Copy link
Author

jirawatee commented Feb 5, 2025

@Yang-33 I already found the problem. The problem is the type of request.body from Cloud Functions for Firebase is not the Buffer but it is the Object instead.

After exploring I figure out in how to fix it by asking Gemini. It tell me that in Cloud Functions for Firebase I can get the Buffer from req.rawBody.

Therefore, the code above should be modified to

const signature = crypto.createHmac('SHA256', CHANNEL_SECRET.value()).update(req.rawBody).digest('base64').toString();
if (signature !== req.headers['x-line-signature']) {
return res.status(401).send('Unauthorized')
}

Thank you so much for your comment. I appreciate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment