【Node.js】LINE Messaging API SDKを用いてWebhook署名検証を行う方法

2024.06.11

はじめに

先日、AWS Lambdaを用いてユーザーのメッセージをおうむ返しするLINE BOTを作成する機会がありました。その際、本当にLINEからのリクエストかどうかを検証する必要があったので、その方法について解説します。

LINE Messaging APIを用いてLINE BOTを作成する際には、サーバーへのリクエストはLINEプラットフォームのwebhookを通してリクエストが送られます。公式によると、

ボットサーバーが受信したHTTP POSTリクエストは、LINEプラットフォームから送信されていない危険なリクエストの可能性があります。必ず署名を検証してから、Webhookイベントオブジェクトを処理してください。

とのことで、アプリケーションサーバー上で署名検証を行う必要があるそうです。

今回は、Node.jsでLINE Messaging API SDKを使用して署名検証を行う方法を解説します。

LINE Messaging API SDKとは

LINE Messaging API SDKは、LINEプラットフォームとの連携を行うためのSDKです。Node.jsをはじめとする各種プログラミング言語に対応しており、LINEユーザーとのメッセージのやり取りや、LINEのWebhookイベントの受信・処理などを簡単に実装できます。

今回は、この中のLINE Messaging API SDK for nodejsを使用します。こちらから公式ドキュメントを確認することができ、使い方やAPI Referenceを確認することができます。

実装

まず、LINE Messaging API SDK for nodejsをインストールします。

npm install @line/bot-sdk

次に実際にsdkを用いて署名検証を行います。

import { LINE_SIGNATURE_HTTP_HEADER_NAME, validateSignature } from '@line/bot-sdk';

export const handler = async (event) => {

  // LINEからのリクエストの署名検証
  const signature = event.headers[LINE_SIGNATURE_HTTP_HEADER_NAME]
  if (!validateSignature(event.body!, process.env.CHANNEL_SECRET ?? "", signature!)) {
    return {
      statusCode: 403,
      body: 'Invalid signature',
    };
  }

  // メインロジック(省略)

};

解説

まず、6行目について

const signature = event.headers[LINE_SIGNATURE_HTTP_HEADER_NAME]

eventには、Webhookから取得されたJSONオブジェクトが渡ってきます。リクエストヘッダーからx-line-signatureを取り出し、signature変数に代入しています。headerの指定には、x-line-signatureという文字列をそのまま入れてもいいですが、せっかくなのでsdkが用意しているLINE_SIGNATURE_HTTP_HEADER_NAME変数を使用しています。

次に、後続のif文について、

 if (!validateSignature(event.body!, process.env.CHANNEL_SECRET ?? "", signature!)) {
    return {
      statusCode: 403,
      body: 'Invalid signature',
    };
  }

ここではsdkが用意しているvalidateSignature関数を使用して署名を検証します。引数は、bodychannelSecretsignatureです。 validateSignature関数は、line-bot-sdk-nodejs/lib/validate-signature.tsで定義されており、内部で検証を行なっていることがわかります。 そして、もし署名検証が失敗した場合、403を返すようにします。

このようにすることで、不正なリクエストを防ぐことができます。

おうむ返しBOT

最後に、以下の記事を参考に、実際に署名検証を行なって作成したおうむ返しBOTのLambda関数がこちらです。

import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
import { Message } from '@line/bot-sdk/lib/types';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { LINE_SIGNATURE_HTTP_HEADER_NAME, WebhookRequestBody, messagingApi, validateSignature } from '@line/bot-sdk';

// Systems manager から取得するための諸々
const ssmClient = new SSMClient();
const ssmGetChannelSecretCommand = new GetParameterCommand({
  Name: "/LineAccessInformation/CHANNEL_SECRET",
  WithDecryption: true,
});

const ssmGetAccessTokenCommand = new GetParameterCommand({
  Name: "/LineAccessInformation/ACCESS_TOKEN",
  WithDecryption: true,
});

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
  const [channelSecret, channelAccessToken] = await Promise.all([
    ssmClient.send(ssmGetChannelSecretCommand),
    ssmClient.send(ssmGetAccessTokenCommand),
  ]);

  // create LINE SDK client
  const client = new messagingApi.MessagingApiClient({
    channelAccessToken: channelAccessToken.Parameter!.Value ?? "",
  });

  // LINEからのリクエストの署名検証
  const signature = event.headers[LINE_SIGNATURE_HTTP_HEADER_NAME]
  if (!validateSignature(event.body!, channelSecret.Parameter?.Value ?? "", signature!)) {
    return {
      statusCode: 403,
      body: 'Invalid signature',
    };
  }

  // 文面の解析
  const bodyRequest: WebhookRequestBody = JSON.parse(event.body!);
  if (typeof bodyRequest.events[0] === "undefined") {
    // LINE Developer による Webhook の検証は events が空配列の body で来るのでその場合は 200 を返す
    return {
      statusCode: 200,
      body: "OK"
    }
  }

  if (bodyRequest.events[0].type !== "message" || bodyRequest.events[0].message.type !== "text") {
    return {
      statusCode: 500,
      body: 'Error',
    };
  } else {
    // 文面をそのままオウム返しする
    const messageReply: Message = {
      "type": "text",
      "text": bodyRequest.events[0].message.text
    }
    await client.replyMessage({ replyToken: bodyRequest.events[0].replyToken, messages: [messageReply] });
    // OK 返信をセット
    return {
      statusCode: 200,
      body: "OK"
    };
  }
};

channelSecretとAccessTokenは環境変数からではなく、SSMパラメーターから取得するようにしました。

無事、おうむ返しBOTを作成することができました。

最後に

今回は、LINE Messaging API SDKを使用してNode.jsで署名検証を行いました。

LINE Messaging API SDKにはNode.js以外にも様々なプログラミング言語に対応しているので、公式ドキュメントを参考に、各種プログラミング言語に合わせて今回のように実装してみてください。

参考になりましたら幸いです。

参考