そこに仁義はあるのか(仮)

略してそこ仁!

AsanaのWebhookからタスクの追加を検知するbotを作る

タスク管理サービスのAsanaでタスクを追加した時にslackに通知するbotを作りました。
通常は以下の画像のようにAsanaとIntegrationできるアプリがたくさんあるので1からコードを書く必要はほぼ無いのですが、会社Asanaと会社slackにて追加できるアプリにそれぞれ制限があり、今回わざわざコードを書くことになりました。
Webhookをやろうとした時に、ちゃんとセキュリティを担保したいけれどその辺りの情報が全然ない…!ということで困ったので、今後何かAsanaのWebhookでアプリを作る人のお役に立てば幸いです。

f:id:syobochim:20201022005123p:plain:w500


認証用のPersonal Access Tokenを作成する

最初に認証のためのPersonal Access Tokenを作成します。

まず、AsanaのDeveloper App Consoleにアクセスします。
このURL「 https://app.asana.com/0/developer-console 」にアクセスするか、 Asanaでログインした画面の右上にあるプロフィール画像から「My Profile Settings」をクリックし、ポップアップの「Apps」タブから「Manage Developer Apps」をクリックしても同じ画面に遷移します。

「+ New Access Token」をクリックしToken名を入力すると以下のような形式のトークンを取得できます。プログラムやcurlからのアクセスにはこのTokenを使っていきます。 1/1234567890123456:bive3xuos5Ohcavei4yie7ha0Oovaph8

Webhookを登録する

Webhookを登録するにはTaskが登録されるProjectのIDと、イベントが発生した時に情報を送信するURLが必要です。
ProjectのIDはAsanaのURLから確認できます。タスクを登録するプロジェクトのリストやボードを確認した時のURLに含まれている数字がProject IDを示します。
以下の例であれば 1234567890123456 がProject IDです。
https://app.asana.com/0/1234567890123456/board

先ほど取得したAccess Tokenを{access-token}、Project IDを{project-id}、Webhookイベントの送信先URLを{target-url}に置き換えてcurlコマンドを実行します。
action : added でタスクの追加イベントを検知します。addedの他にもchanged removed deleted undeletedなどが指定できるようです。

$ curl -X POST https://app.asana.com/api/1.0/webhooks \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {access-token}' \
  -d '{
  "data": {
    "filters": [
      {
        "action": "added",
        "resource_type": "task"
      }
    ],
    "resource": "{project-id}",
    "target": "https://{target-url}"
  }
}'

Handshakeをする

一番最初にWebhookのイベントが発生すると、Handshake用にX-Hook-SecretをHTTPヘッダーに含めたリクエストがtarget-urlに設定したURLへ送られます。
このリクエストに対して 200 OK もしくは 204 No Content のレスポンスを返すことで登録が完了します。
以下が初期登録時のNode.jsでのAWS Lambdaコードです。2行目でHTTPヘッダーの X-Hook-Secret をログ出力します。
このX-Hook-Secretの値は後ほど使うのでMEMOしておいてください。 また、このコードは初回登録時の1回のみ利用します。

exports.handler = async (event) => {
    console.log('this is secret header ----->' + event.headers['X-Hook-Secret'])
    const response = {
        statusCode: 200,
        headers: {"X-Hook-Secret": event.headers['X-Hook-Secret']},
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

X-Hook-Secretja0ge6aChaiPhee7ioph7cucoongaht3 のような形式の文字列です。

また、このコードが正常に動作しているかどうかはこちらのサイトを利用すると簡単に確認できます。
https://asana-webhook-tool.herokuapp.com/

Webhookで受け取ったリクエストを検証する

次に、実際にWebhookが送られてきたデータを受け取ります。
先ほど得た X-Hook-Secretを利用することで、target urlへ送られてきたリクエストがAsanaから送られたものであることを確認でき、安全にリクエストを処理できます。
以下はAWS Lambdaのコードです。{X-Hook-Secret} は先ほど得た値に置き換えてください。
AsanaからはX-Hook-Signature ヘッダーを持つリクエストが送られてきます。このX-Hook-Signatureの値はリクエストのbodyをX-Hook-Secretを使ってSHA 256 HMACでHash化したものになります。
Hash化した値とX-Hook-Signatureが同じ値かどうか確認し、同じ値であれば正常なリクエストとして処理します。

var crypto = require("crypto");

exports.handler = async (event) => {
    const signature = event.headers['X-Hook-Signature'];
    const hash = crypto.createHmac('sha256', '{X-Hook-Secret}')
        .update(String(event.body))
        .digest('hex');

    // ヘッダーのシークレットをチェックする
    if (signature != hash) {
        console.error('Calculated digest does not match digest from API. This event is not trusted. : ' + signature);
        return response = {
            statusCode: 401
        };
    }

    // Webhookのデータを処理するコードを書く

    const response = {
        statusCode: 200
    };
    return response;
};

以上で基本的なWebhookの設定は完了です。
ヘッダーのチェックをした後は、HTTPリクエストのbodyを処理していけば安全にWebhookのリクエストを操作できます。

参考リンク

公式ドキュメント

Node.js
Crypto | Node.js v15.0.0 Documentation