背景
LINE WORKSのbotサーバを開発する際,受信したユーザからの応答(callback)が正しくLINE WORKS側から送信されたものなのか(改竄されていないか)を判定する必要があります。LINE WORKSの公式ページにはJavaのサンプルコードはあるのですが,JavaScriptのものは無かったため自分で実装しました。さらにこれをExpressのミドルウェアとして利用できるようにしましたので共有します。
環境
- Node v16.13.0
- npm 8.1.4
- Express 4.15.2
公式ページから
上述の公式ページには以下のような記述があります。
Bot サーバーが受信した HTTP POST リクエストは、LINE WORKS プラットフォームから送信されていない危険なリクエストの可能性があります。 必ず署名を検証してから、イベントオブジェクトを処理してください。
メッセージサーバーから送信されたメッセージの改ざん有無を確認するには、ヘッダーに含まれる X-WORKS-Signature を用います。 確認プロセスは以下を参照してください。
「Developers Console > Bot」Botの詳細情報画面でBot Secretを取得します。
Bot Secretを秘密鍵として利用し、メッセージサーバーから送られた body の内容を HMAC-SHA256 アルゴリズムでエンコードします。
上記の HMAC-SHA256 アルゴリズムでエンコードした結果を BASE64 エンコードします。
X-WORKS-Signature のヘッダー値と比較し、同一であればメッセージは改ざんされていないと判断できます。
では,Node.jsのデフォルトモジュールであるcrypto
モジュールを利用してコードを実装していきます。
コード
まずbotのidとsecretをまとめたJSONを用意しておきます。
const botSecrets = {
12345678: 'abcdefghijklmn',
23456789: 'opqrstuvwxyz'
}
実際にはこのようにハードコーティングするのでなく,.env
ファイル等で管理した方が良いでしょう。
実際のミドルウェアのコードがこちらです。
const crypto = require('crypto')
const worksHeaderCheck = function (req, res, next) {
const body = JSON.stringify(req.body)
const botId = req.header('X-WORKS-BotId')
const signature = req.header('X-WORKS-Signature')
if (!botId || !signature) return next(new CustomError('Header malformed.', 400))
const secret = botSecrets[botId]
if (secret === undefined) return next(new CustomError('Bot secret is undefined', 500))
const hash = crypto.createHmac('sha256', secret).update(body).digest('base64')
if (signature !== hash) return next(new CustomError('Signature is altered.', 401))
next()
}
まずヘッダのX-WORKS-BotId
およびX-WORKS-Signature
に含まれるbotのidと署名を取り出します。どちらかが欠損していればヘッダ情報不良として400
エラーを返します。
次にbotのsecretが登録されているか判定し,登録がなければ500
エラーを返します。
最後に送信されたbodyと登録されていたbot secretからハッシュを計算し,送信されたsignatureと比較します。ハッシュ計算の際には最初からbase64形式で求めることに注意しましょう。合致しなければ改竄されていることになりますので,401
エラーを返します。
なおCustomError
はエラーハンドリングをシンプルにするためのカスタムエラークラスです。azujuuuuuun様のエントリを参考にさせて頂きました。
// エラーハンドリング用カスタムクラス
class CustomError extends Error {
constructor(message, statusCode) {
super(message)
this.statusCode = statusCode
}
}
// エラーハンドリング用ミドルウェア
const errorHandler = function (err, req, res, next) {
console.error(err.message)
res.status(err.statusCode || 500).send(err.message)
}
使用法
express()
.use(express.urlencoded({ extended: true }))
.use(express.json())
.get('/hoge', worksHeaderCheck, (req, res) => {
// 処理
})
.use(errorHandler)
.listen(PORT, () => console.log(`Server started and listening on ${ PORT }`))