Python における Microsoft Teams Outgoing Webhook の HMAC 認証の通し方（ついでにオウム返し Bot も）

# はじめに
はじめまして。私は大学院修士課程の学生です。所属する大学の研究で Microsoft Teams に Bot を作成するにあたって、Azure や AWS などを使わず自分でバックエンドサーバを建てて運用しています。サーバを立てたり Python のコードを書いたりするのが大変苦労したので、その詰まったところを自分のメモ・他の方のお役に立てればと思い投稿します。

## 今後記事化したいこと
 - Linux (Ubuntu) に MeCab + mecab-ipadic-NEologd の導入
 - Linux (Ubuntu) mecab-ipadic-NEologd に加えて自作の辞書を導入
 - [Ubuntu + Apache2.4 で 443 ポート以外で SSL 通信を有効化する方法](https://qiita.com/maru_ocu/items/23960ab4f25cc330a287)
 - [Ubuntu + Apache2.4 + Flask + uWSGI の導入およびデーモン化](https://qiita.com/maru_ocu/items/08c99fb3a9d3d7927b98)
 - ~~Microsoft Teams でオウム返し Bot を作成 (Python + Outgoing Webhook)~~ この記事でやることにしました

## 実装環境
 - (Ubuntu 20.04 LTS)
 - Python 3.8.5

# 本題の Python における Microsoft Teams Outgoing Webhook の HMAC 認証の通し方
以下の手順でできます。
## 1. チームを管理 > アプリ > 送信 Webhook を作成 を押し、各情報を記入

* 名前 : チャットボットを呼び出す際などで使用される名前です。私は testbot としました。
* コールバック URL : Flask などで POST API を受け取る場所を設定します。下記のように https://example.com/api/app.py ファイルで、 https://example.com/api/post/ というディレクトリで POST を受けるように Python プログラムを設置した場合は、コールバック URL は http://example.com/api/post/ になります。
* 説明 : 何でも大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1652242/4c38ec8c-5750-347e-024c-849e36041192.png)

```python:app.py

@app.route('/post/', methods=["POST"])
def teams():

```
## 2. シークレット値を記入
手順1で得たシークレット値を Python のファイルに記入します。シークレット値が AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= だとすると、以下のようにバイナリとして扱うために b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" と記入します。

```python
BUFSECRET = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
```

## 3. HMAC の計算方法
私が詰まったのは、2.をバイナリとして扱わずに文字列として扱い、それをハッシュ値にしていたことが問題でした。Microsoft の公式文書にはちゃんと「Teams によって提供されたセキュリティ トークンのバイト配列からハッシュを計算します」ってちゃんと「バイト列」って書いていたんですが、完全に見落としていたせいで何日も悩んでいました。

HMAC 値の計算方法は、トークンのバイト列から、POST の data 全体を sha256 でハッシュ化し、それを base64 エンコードするという手順です。
以下のように書くと、HTTP ヘッダの "authorization" 値と一致します。

```python
    h = hmac.new(base64.b64decode(BUFSECRET), msg = msgBuf, digestmod = hashlib.sha256).digest()
    h_base64 = base64.b64encode(h).decode()
    msgHash = "HMAC " + h_base64
```


# まとめ
機械学習の人気の高まりによって Python の人気も高まりつつある世の中で、Teams のバックエンドプログラムが Python で書かれているものがあまりなかったため、私はかなり苦労しました。
最後にオウム返し Bot のソースコードをまとめて書いておきます。
先述の通り、app.py の設置場所は https://example.com/api/app.py で、API を投げる先は https://example.com/api/post/ の場合です。

**Flask 向けのコードになっていますのでご注意下さい。Flask を使ってなかったら HTTP ヘッダの受け取り方とか、JSON の受け取り方等が違います（要するに下で言うところの payload とか auth とか jsonify とか）。**

```python:app.py
from flask import Flask, jsonify, request, url_for, abort, Response
import base64
import hashlib
import hmac
import json

BUFSECRET = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

@app.route('/post/', methods=["POST"])
def teams():
    payload = request.get_data() # 投げられた POST データ全体を取得
    auth = request.headers.get('authorization')  # HTTP ヘッダのうち、"authorization" 値のみを取得
    h = hmac.new(base64.b64decode(BUFSECRET), msg = payload, digestmod = hashlib.sha256).digest() # シークレットキーを用いて、payload 値を sha256 でハッシュ化
    h_base64 = base64.b64encode(h).decode() # 先ほどのハッシュ化したデータを文字列にデコード
    msgHash = "HMAC " + h_base64

    if (msgHash == auth):
        json = request.get_json() # 投げられた json データを取得（これは平文）
        receivedMsg = json['text'] # json データのうち、'text'（ユーザが入力したメッセージ部分を取得）
        # print(receivedMsg)
        return jsonify(type = "message", text = receivedMsg) # メッセージタイプと返事する内容を json 化して return
    else:
        return jsonify(type = "message", text = "ハッシュ値と authorization 値が異なります")

```

生データでは、"&lt;at&gt;testbot&lt;/at&gt;&amp;nbsp;danke" となっているので、上記 if 文を以下のようにしました。

```python
if (msgHash == auth):
    json = request.get_json()
    receivedMsg_1 = json['text'].lstrip("<at>testbot</at>") # <at>testbot</at> を削除
    receivedMsg_2 = receivedMsg_1.replace("&nbsp;", " ") # 文中の &nbsp; を実際のスペースに変換
    receivedMsg = receivedMsg_2_space.lstrip() # 文頭のスペースを全て削除
    return jsonify(type = "message", text = receivedMsg)
```

ボットにメンションをすると、こんな感じで動きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1652242/26e2d1f9-6213-2152-9b94-fe8d158ca127.png)


## 参考文献
- [Verifying HMAC from Microsoft Teams bot in Python Flask - Stack Overflow](https://stackoverflow.com/questions/43278132/verifying-hmac-from-microsoft-teams-bot-in-python-flask)