はじめに
はじめまして。私は大学院修士課程の学生です。所属する大学の研究で Microsoft Teams に Bot を作成するにあたって、Azure や AWS などを使わず自分でバックエンドサーバを建てて運用しています。サーバを立てたり Python のコードを書いたりするのが大変苦労したので、その詰まったところを自分のメモ・他の方のお役に立てればと思い投稿します。
今後記事化したいこと
- Linux (Ubuntu) に MeCab + mecab-ipadic-NEologd の導入
- Linux (Ubuntu) mecab-ipadic-NEologd に加えて自作の辞書を導入
- Ubuntu + Apache2.4 で 443 ポート以外で SSL 通信を有効化する方法
- Ubuntu + Apache2.4 + Flask + uWSGI の導入およびデーモン化
-
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/ になります。
- 説明 : 何でも大丈夫です。
@app.route('/post/', methods=["POST"])
def teams():
2. シークレット値を記入
手順1で得たシークレット値を Python のファイルに記入します。シークレット値が AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= だとすると、以下のようにバイナリとして扱うために b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" と記入します。
BUFSECRET = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
3. HMAC の計算方法
私が詰まったのは、2.をバイナリとして扱わずに文字列として扱い、それをハッシュ値にしていたことが問題でした。Microsoft の公式文書にはちゃんと「Teams によって提供されたセキュリティ トークンのバイト配列からハッシュを計算します」ってちゃんと「バイト列」って書いていたんですが、完全に見落としていたせいで何日も悩んでいました。
HMAC 値の計算方法は、トークンのバイト列から、POST の data 全体を sha256 でハッシュ化し、それを base64 エンコードするという手順です。
以下のように書くと、HTTP ヘッダの "authorization" 値と一致します。
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 とか)。
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 値が異なります")
生データでは、"<at>testbot</at> danke" となっているので、上記 if 文を以下のようにしました。
if (msgHash == auth):
json = request.get_json()
receivedMsg_1 = json['text'].lstrip("<at>testbot</at>") # <at>testbot</at> を削除
receivedMsg_2 = receivedMsg_1.replace(" ", " ") # 文中の を実際のスペースに変換
receivedMsg = receivedMsg_2_space.lstrip() # 文頭のスペースを全て削除
return jsonify(type = "message", text = receivedMsg)
ボットにメンションをすると、こんな感じで動きます。