Verify Line Events Webhook บน Express.js จะได้ประมาณนี้ [TH]
ถ้าไม่อยากเหนื่อย, จบปัญหาเรื่อง Emoji และยังเอาแนวทางนี้ไปใช้กับ Facebook ได้อีกด้วย มาดูกันครับ
ขอไม่เล่าว่าทำไมเราจึงต้องทำ Line Signature Validation นะครับ เพราะเชื่อว่า ใครหลงเข้ามาตรงนี้ ก็น่าจะรู้อยู่แล้วว่าทำไมจึงต้องทำ
ถ้าอยากจะรู้เรื่องราวให้กระจ่าง เราก็พร้อมที่จะแถลงไข
ในแง่ของ Developer เรามักจะคิดกันว่า Endpoint ที่เราเตรียมไว้รับ Webhook จาก Line นั่น ไม่น่าจะมีคนอื่นรู้ จึงอาจจะไม่ระวัง ไม่ทันได้เตรียมการป้องกันการรับข้อมูลอันไม่พึงประสงค์
ในแง่ของ Security เค้าจะถามแน่ ๆ ว่า คุณจะรู้ได้อย่างไร ว่า ข้อมูลที่คุณรับมา มาจาก Line จริง ๆ
เอาละ ไม่ต้องกังวลไป เพราะทาง Line ก็ได้เตรียมวิธีการยืนยันไว้ให้เราแล้วครับ นั่นก็คือการทำ Signature Validation (หรือก็คือการเอาข้อมูลมา Hash ด้วย Key แล้วใช้ตัว Hash นี้เป็นตัวยืนยัน)
เมื่อพูดถึงเรื่องการความปลอดภัยข้อมูล นอกจากการทำ Signature Validation แล้ว ก็ยังมีวิธีอื่น ๆ ที่หลาย ๆ คนอาจจะรู้จักกันดี เช่น
- Site-to-Site VPN
- IP Whitelist
- และอีกหลาย ๆ วิธี
แต่พอลองนึกภาพจากทางฝั่งของ Line ด้วย Site-to-Site VPN นี่ตัดไปได้เลย เพราะทำไม่ไหวแน่นอน ส่วน IP Whitelist เอาจริง ๆ มันปลอมได้ ฉะนั้นมันก็เหลือแนวทางเดียว คือการ Validate ตัวข้อมูลที่รับส่งกัน ด้วย Key ที่ถืออยู่ด้วยกันทั้งคนรับ/ส่งข้อมูลนั่นเองครับ ซึ่งในกรณีนี้ก็คือ Line Signature Validation นั่นเองครับ
สรุปแล้ว Signature Validation คืออะไร
โดยทั่วไปแล้ว หลักการของ Signature Validation คือ เอาข้อมูลที่ต้องการการตรวจสอบ มาทำ Hash ด้วย Key / Secret เพื่อให้ได้ค่าที่สามารถนำไปเปรียบเทียบได้ โดยจะทราบได้หากตัวข้อมูลมีการเปลี่ยนแปลงไประหว่างทางหรือไม่ นอกจากนี้ยังสามารถใช้ Hash นี้ เป็นตัวยืนยันได้ด้วยว่าข้อมูลชุดนี้มาจากต้นทางที่ถูกต้องจริง ๆ
เข้าเนื้อหาเลย ไม่อ้อมโลก
สิ่งที่เราต้องทำจริง ๆ คือเอา Request body มา Hash ด้วย HMAC-SHA256 เพื่อเทียบกับ x-line-signature ที่มากับ Request Header ว่าตรงกันหรือไม่
แต่ถ้า เราเอา Request body ที่ผ่าน JSON parser มาแล้ว มาพยายามแปลงกลับเป็น String เราอาจจะเจอปัญหา เช่น UTF-8 encoding ที่มาจากพวก Emoji ทั้งหลาย
ดังนั้นสิ่งที่เราควรทำคือ เอา Request body แบบ UTF-8 จริง ๆ ก่อนที่จะถูกแปลงโดย JSON parser มาใช้เลย
Note: ตัวอย่างนี้จะใช้ Express.js มีทั้ง JS และ TS นะครับ
ใช้ Express.js Middleware ธรรมดา ๆ นี่แหละ [Version สั้น แบบใช้ JS]
ขั้นแรกเราก็ใส่ Middleware ให้กับ Express.js ก่อนครับ
const express = require('express');
const app = express();app.use(function (req, res, next) {
req.rawBody = '';
req.on('data', function (chunk) {
req.rawBody += chunk;
});
next();
});
Middleware ตัวนี้จะเพิ่ม req.rawBody ที่เป็น String UTF-8 ตรงไปตรงมาครับ
จากนั้น ก็จะเป็น Middleware สำหรับ Verify signature จริง ๆ แล้ว
const crypto = require('crypto');function lineSignatureValidationMiddleWare (req, res, next) {
const signature = crypto
.createHmac('SHA256', 'LineChannelSecret')
.update(req.rawBody)
.digest('base64'); // Compare x-line-signature with the signature
const headerXLineSignature = req.headers['x-line-signature'];
if (headerXLineSignature === signature) {
next();
} else {
res.sendStatus(401);
}
}
จากนี้ ก็สามารถเอา Middleware lineSignatureValidationMiddleWare ไปใช้กับ Route ที่รับ Webhook จาก Line ได้เลยครับ
Note: วิธีนี้ใช้กับ Facebook Webhook ได้ด้วยนะครับ
ใช้ Express.js Middleware ธรรมดา ๆ นี่แหละ [Version เต็ม แบบใช้ TS]
อธิบาย Code
บรรทัดที่ 8-14 อันนี้เป็นเรื่องของ Type Script ครับ เป็นอีกหนึ่งในวิธีที่ใช้เพิ่ม Member ให้กับ Interface ตรงนี้ผมจะเพิ่ม Member ที่ชื่อว่า rawBody กำหนด Type เป็น string เพื่อเอาไว้ใช้เก็บ body payload ในสภาพดั่งเดิมครับบรรทัดที่ 16-22 สร้าง Middleware ให้กับ Express.js ทำหน้าที่ เก็บ body payload ในสภาพดั่งเดิม ใส่ใน Request.rawBody ที่เตรียมไว้ในขั้นตอนก่อนหน้าครับบรรทัดที่ 28-43 สร้าง Middleware ที่ทำหน้าที่เปรียบเทียบ Signature ของ body payload ที่ได้มาจาก Request ที่ได้รับมาครับ
ผลที่ได้
เราจะโฟกัสกันที่การเปรียบเทียบ Signature ที่ได้มาจาก Request Header กับที่ได้จากการคำนวนเองนะครับ โดยสังเกตุจาก บรรทัดที่ขึ้นต้นด้วย i MDW signature:
ครับ
สรุป
การทำ Signature Validation ด้วยวิธีนี้ ครอบคลุมทั้งภาษา และ Emoji แน่นอน
ในด้าน Security เราจำเป็นต้องตรวจสอบสิ่งที่เราได้รับมา ว่ามาจากที่ ๆ ควรจะมา และไม่ถูกเปลี่ยนแปลงระหว่างทางครับ
สรุปในแบบ Dev2Dev
เรื่อง Security เป็นหนึ่งในเรื่องที่เรามักจะพลาดกัน เพราะเราไม่รู้จริง ๆ แต่ตอนนี้รู้แล้ว ฉะนั้นก็ไม่น่าจะพลาดแล้วนะครับ 👌
Special Thanks to Jirawatee