LoginSignup
7
8

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

Last updated at Posted at 2021-06-25

はじめに

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

今後記事化したいこと

実装環境

  • (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

app.py

@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 とか)。

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 値が異なります")

生データでは、"<at>testbot</at>&nbsp;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("&nbsp;", " ") # 文中の &nbsp; を実際のスペースに変換
    receivedMsg = receivedMsg_2_space.lstrip() # 文頭のスペースを全て削除
    return jsonify(type = "message", text = receivedMsg)

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

image.png

参考文献

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8