🍧

TeamsWebhookのCORS制限をLambda経由で回避した

2023/07/31に公開

Webhook の仕様が変わった?

Microsoft Teams Incoming Webhook のペイロード仕様が2023年7月6日あたりから変わったようです。
リクエストヘッダが必須になり、Content-Type: application/x-www-form-urlencodedを渡すとエラーを返却するようになりました。
Content-Typeにはapplication/jsonを渡すのが仕様上は正しいのですが、それだと困ることになりました。
ブラウザのJavaScriptから実行するため、CORS(オリジン間リソース共有)の縛りを受け、application/jsonを渡すとプリフライトリクエストが発生してしまうのです。

Incoming Webhook はブラウザからのリクエストを想定したアプリではないので、Access-Control-Allow-Originレスポンスヘッダをブラウザに返す方法は用意されていません。
クロスドメインのリクエストを許可させることができず、Teams通知に失敗します。

解決策

この問題を解決するには、

  • Teamsのカスタムアプリケーションを作成する
  • リバースプロキシを立てる
  • ブラウザとTeamsの間に中継用のバックエンドを置く

のいずれかになると思います。

現場の運用を変えない前提に立つと中継用バックエンドを用意するのが現実的です。
バックエンドはAWSで構築するとして、従来、AWS Lambda をHTTPで呼ぶには Amazon API GatewayAWS ALB を経由する必要があり、設定が少し面倒でした。
2022年4月から、Lambda のみで Web API を作成できる Lambda Function URL がサポートされたので、エンドポイントを簡単に作ってみましょう。

AWS Lambda で関数URLを作成

AWSコンソールで Lambda の関数を作成します。ランタイムは Python にしました。

関数URLを有効にし、認証タイプをNONE、CORSを設定し、関数URLにアクセスできるオリジン、つまりJavaScriptの実行元URLを入力します。

許可ヘッダーと許可メソッドの設定をし、[保存] をクリックします。

最後に [関数の作成] をクリックすると、関数URLが生成され、コード編集画面になります。

次のPythonコードを貼り付けます。

import json
import requests

def lambda_handler(event, context):
    url = '<TeamsのWebhook URL>'
    headers = {
        'Content-type': 'application/json'
    }
    body = json.loads(event['body'])
    post_data = {
        'text': body['text']
    }
    requests.post(url, headers = headers, json = post_data)
    return {
        'statusCode': 200,
        'body': json.dumps('Success!')
    }

レイヤーの追加

このままコードを実行すると、

のようなエラーになるので、レイヤーを追加します。

ここでは自作せず、有志が公開しているARNを入力します。
https://github.com/keithrozario/Klayers/tree/master/deployments

アクセス元のJavaScriptを修正

アクセス元のJavaScriptを修正します。
FetchのURLをLambdaの関数URLに変更するだけです。

const message = {
    text: 'foobar'
};
fetch(
    'https://foobar.lambda-url.ap-northeast-1.on.aws', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(message)
})
.then((res) => {
    if (!res.ok) {
        throw new Error(`${res.status} ${res.statusText}`);
    }
    return res.json();
})
.then((data) => {
    console.log(data);
})
.catch((reason) => {
    console.log(reason);
});

Discussion