こんにちは。@eielhです。 id:cw-hayashiが投稿した記事である 「チャットワークのWebhookの署名検証を各言語で実装してみた」があります。 しかし、自分が利用しているプログラミング言語が網羅されていなかったので、続きがないのか確認してみたところ「自分でやってください」*1と言われてしまいました。 というわけで、早速挑戦してみました。
- 準備するもの
- Elixir
- 参考
- Rust
- 参考
- Haskell
- 参考
- まとめ
*1:正確には遠回しにしか聞いていません
こんにちは。@eielhです。 id:cw-hayashiが投稿した記事である 「チャットワークのWebhookの署名検証を各言語で実装してみた」があります。 しかし、自分が利用しているプログラミング言語が網羅されていなかったので、続きがないのか確認してみたところ「自分でやってください」*1と言われてしまいました。 というわけで、早速挑戦してみました。
*1:正確には遠回しにしか聞いていません
@hayasshi_です。Scalaのプロダクトを開発しています。
Heroku、気軽にWebアプリケーションを公開できて便利ですよね。
先日公開した「チャットワークのWebhookの署名検証を各言語で実装してみた」の記事を書くにあたって、検証用のScalaアプリを Heroku Container Registry をつかってデプロイしました。
もともとDockerでHerokuにデプロイできれば、Scala以外のアプリのときでもデプロイの手順差異が少ないと思って試してみたのですが、公式ドキュメント通りに進めたところ少し嵌ってしまったので、備忘録を兼ねて記録しておきたいと思います。
なおHerokuのContainer Registryを利用したデプロイに関するドキュメントは下記で、基本的には記載通りの手順で問題ありませんでした。
ScalaにおけるDockerイメージ作成用のツールの兼ね合いもあり、Pushing an image(s)
の部分はPushing an existing image
の手順で進めました。
まずはデプロイ対象のアプリケーションが必要です。
今回はサンプルとして、akka-http
をつかってping-pong
サーバーを作ってみたいと思います。
通常のScalaプロジェクトを作成して、下記のファイルを用意しました。
object PingPongServer extends App { implicit val system: ActorSystem = ActorSystem() implicit val materializer: ActorMaterializer = ActorMaterializer() val route = pathEndOrSingleSlash { post { extractRequest { req => val result = req.entity.dataBytes.map(_.utf8String).runWith(Sink.head) onSuccess(result) { body => println(s"Client input is '$body'") val res = body.toLowerCase match { case "ping" => "pong" case _ => body } complete(HttpEntity(ContentTypes.`text/plain(UTF-8)`, res)) } } } } val host = "0.0.0.0" val port = sys.env.getOrElse("PORT", "8080").toInt // $PORT is set by Heroku Http().bindAndHandle(route, host, port) sys.addShutdownHook({ import scala.concurrent.duration._ Await.ready(system.terminate(), 30.seconds) }) }
ChatWorkのScalaプロダクトではDockerイメージの作成は、sbt-native-packager
を利用しています。
今回も同様にsbt-native-packager
を利用しました。
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.1")
import com.typesafe.sbt.packager.docker.ExecCmd lazy val root = (project in file(".")) .settings( inThisBuild(List( organization := "com.chatwork.cwhayashi", scalaVersion := "2.12.3", version := "0.1.0-SNAPSHOT" )), name := "ping-pong", libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.0.10", libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.4.19", // Docker settings defaultLinuxInstallLocation in Docker := "/opt/application", executableScriptName := "app", dockerBaseImage := "openjdk:8u131-jdk-alpine", dockerUpdateLatest := true, mainClass in (Compile, bashScriptDefines) := Some("com.chatwork.cwhayashi.PingPongServer"), packageName in Docker := name.value, // Expose is NOT supported by Heroku // dockerExposedPorts := Seq(8080) // Run the app. CMD is required to run on Heroku dockerCommands := dockerCommands.value.filter { case ExecCmd("CMD", _*) => false case _ => true }.map { case ExecCmd("ENTRYPOINT", args @ _*) => ExecCmd("CMD", args: _*) case other => other } ) .enablePlugins(AshScriptPlugin)
いくつかポイントがあります。
CMD
を利用する
sbt-native-packager
ではENTRYPOINT
を使う形でDockerfileが出力されるので、変換するコードを入れていますEXPOSE
は使えない
PORT
に設定して渡される
PORT
を取得して Listen するようにしています特に一番目のポイントはドキュメントにも記載がなかったので、注意が必要です。
sbt-native-packager
は特別な設定をしなければENTRYPOINT
でDockerfileを作成するのでうまく起動せず、ドキュメントに書いてあるHerokuサンプルのDockerfileをみて気付くことができました。
下記のコマンドを実行し、ScalaアプリのDockerイメージを作成します。
$ cd /path/to/sbt-project
$ sbt docker:publishLocal
...
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE ping-pong 0.1.0-SNAPSHOT 6d789fb48461 42 seconds ago 121MB ping-pong latest 6d789fb48461 42 seconds ago 121MB
続いてHerokuのアプリ領域を作成します。
こちらはドキュメントに記載の通り、CLIツールをつかって簡単に作成できます。
$ heroku login # Input Email and Password Logged in as ... $ heroku apps:create <app-name> Creating ⬢ <app-name>... done https://<app-name>.herokuapp.com/ | https://git.heroku.com/<app-name>.git
すでに存在するDockerイメージをPushする場合は、ドキュメントのPushing an existing image
の手順を行う必要があります。
基本的にdocker
コマンドをそのまま利用します。
# Heroku Container Registryへのログイン $ heroku container:login # Heroku Container Registry用にタグ付け $ docker tag ping-pong registry.heroku.com/<app-name>/web # Heroku Container RegistryへPush $ docker push registry.heroku.com/<app-name>/web
Pushが完了した時点でアプリケーションはデプロイされ実行されます。
2018-06-27追記
確認したところ、明示的にreleaseのためのコマンドを実行する必要になった模様です。
Container Registry & Runtime (Docker Deploys) | Heroku Dev Center
heroku container:release web -a <app-name>
実行することでpushされたイメージからアプリが起動し、下記のように接続できるようになりました。
$ curl -X POST -d "test" https://<app-name>.herokuapp.com/ test $ curl -X POST -d "ping" https://<app-name>.herokuapp.com/ pong $ curl -X POST -d "🍣🍺" https://<app-name>.herokuapp.com/ 🍣🍺
無事にScalaアプリのDockerイメージをHerokuで実行することができました。
途中の嵌りポイントは、Heroku Container Registryを利用する上で共通と思われますのでご注意ください。
Herokuをつかって、チャットワークのWebhookやOAuthアプリケーションをぜひ作ってみてください!
@hayasshi_です。Scalaのプロダクトを開発しています。
ついにチャットワークのWebhookがリリースされました!🎉
OAuthとあわせて、様々なサービスがチャットワークと連携しやすくなります。
今日はチャットワークのWebhookの署名検証について少しお話します。
チャットワークのWebhookは、イベントに応じてチャットワークからHTTPリクエストが行われ、それを連携先のサービスで受け付けていただく必要があります。
万一リクエスト先のURLが漏れてしまった場合、チャットワークになりすましてHTTPリクエストを送ることができてしまいます。
そこでチャットワークのWebhookでは、Webhookの設定毎にトークン(署名検証用の秘密鍵)を発行し、それをつかってHTTPリクエストがチャットワークからであるということを検証していただくことができます。(詳細は公式ドキュメントを参照してください。)
この記事では、各言語での署名検証のサンプルコードをご紹介したいと思います。
X-ChatWorkWebhookSignature
の値
今回は各言語での署名検証のコードのみにフォーカスするため、トークン等は下記にて準備されたものを利用します。 (実際のチャットワークのWebhookで作成された値です)
# 署名検証用のトークン A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU= # WebhookのHTTPリクエストボディ {"webhook_setting_id":"246","webhook_event_type":"message_created","webhook_event_time":1511238729,"webhook_event":{"message_id":"984676321621704704","room_id":36818150,"account_id":1484814,"body":"test","send_time":1511238729,"update_time":0}} # WebhookのHTTPリクエストヘッダ`X-ChatWorkWebhookSignature`の値 G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk=
チャットワークのWebhookは、主に下記の2つのことができるAPIが標準ライブラリにあればごく簡単に扱うことができます。
それではどんどんいってみましょう。
弊社ではまだまだPHPのプロダクトが多く、一緒に改善してくれるエンジニアを募集しています。(突然の告知)
そんなPHPでの検証コードです。
環境は以下のバージョンの対話ツールで確認しました。
$ php -v PHP 7.1.11 (cli) (built: Oct 27 2017 11:00:43) ( NTS ) Copyright (c) 1997-2017 The PHP Group Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
<?php $token = 'A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU='; $requestSignature = 'G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk='; $requestBody = '{"webhook_setting_id":"246","webhook_event_type":"message_created","webhook_event_time":1511238729,"webhook_event":{"message_id":"984676321621704704","room_id":36818150,"account_id":1484814,"body":"test","send_time":1511238729,"update_time":0}}'; $key = base64_decode($token); $digest = hash_hmac('sha256', $requestBody, $key, TRUE); $expectedSignature = base64_encode($digest); echo $requestSignature == $expectedSignature; // 1
とくに嵌まるところはありませんでしたが、hash_hmac
関数の第四引数にTRUE
を指定して生のバイナリデータを取得した上で、Base64エンコードしないと結果が変わってしまうことに注意が必要です。
弊社ではScalaエンジニアも(ry
Javaの標準ライブラリを利用しているので、JVM系の言語であれば同じように記載が可能と思われます。
Javaのバージョンは、オラクルのjdk1.8.0
の環境で確認しました。
import java.util.Base64; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; public class Main { public static void main(String[] args) throws Exception { String token = "A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU="; String requestSignature = "G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk="; String requestBody = "{\"webhook_setting_id\":\"246\",\"webhook_event_type\":\"message_created\",\"webhook_event_time\":1511238729,\"webhook_event\":{\"message_id\":\"984676321621704704\",\"room_id\":36818150,\"account_id\":1484814,\"body\":\"test\",\"send_time\":1511238729,\"update_time\":0}}"; String algo = "HmacSHA256"; byte[] key = Base64.getDecoder().decode(token); SecretKeySpec keySpec = new SecretKeySpec(key, algo); Mac mac = Mac.getInstance(algo); mac.init(keySpec); byte[] digest = mac.doFinal(requestBody.getBytes(StandardCharsets.UTF_8)); String expectedSignature = Base64.getEncoder(). encodeToString(digest); System.out.println(requestSignature.equals(expectedSignature)); // true } }
java.util.Base64
がJDK1.8から導入されたので楽に実装できました。
弊社(ry
最近はNode.jsを用いたWebサービスも増えてきました。弊社でもツール等で利用することがあります。
環境は以下のバージョンの対話ツールで確認しました。
$ node -v v8.9.1
const token = 'A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU='; const requestSignature = 'G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk='; const requestBody = '{"webhook_setting_id":"246","webhook_event_type":"message_created","webhook_event_time":1511238729,"webhook_event":{"message_id":"984676321621704704","room_id":36818150,"account_id":1484814,"body":"test","send_time":1511238729,"update_time":0}}'; const crypto = require('crypto'); const hmac = crypto.createHmac('sha256', new Buffer(token, 'base64')); const expectedSignature = hmac.update(requestBody).digest('base64'); process.stdout.write((requestSignature == expectedSignature).toString()); // true
普段JavaScript(Node.js)を書くことが少ないので、Base64のデコード方法にまよったりしましたが、なんとかかけました。 メッセージダイジェストの作成は下記のドキュメントが分かりやすかったです。
Crypto | Node.js v9.3.0 Documentation
Google Apps ScriptではNode.jsと環境が異なるため、以下の記事を参照ください。
Google Apps ScriptでChatWorkのWebhook署名を検証する方法 - ChatWork Creator's Note
環境は以下のバージョンの対話ツール(irb)で確認しました。
$ ruby -v ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
require 'base64' require 'openssl' token = 'A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU=' requestSignature = 'G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk=' requestBody = '{"webhook_setting_id":"246","webhook_event_type":"message_created","webhook_event_time":1511238729,"webhook_event":{"message_id":"984676321621704704","room_id":36818150,"account_id":1484814,"body":"test","send_time":1511238729,"update_time":0}}' key = Base64.strict_decode64(token) digest = OpenSSL::HMAC.digest('sha256', key, requestBody) expectedSignature = Base64.strict_encode64(digest) puts requestSignature == expectedSignature # true
最初、Base64.encode64
, Base64.decode64
を利用していたため、最後の比較で失敗していました。
Base64.strict_encode64
, Base64.strict_decode64
を利用しないと、改行が挿入されてしまうようです。
私事ですが、この署名検証のコードが私のGo入門でした。
2017-11-21 時点のThe Go Playgroundで確認しました。(なのでバージョンは1.9.2
?)
package main import ( "encoding/base64" "crypto/sha256" "crypto/hmac" "fmt" ) func main(){ token := "A9ne+ygvdV0IZBaPFV2zC1e5Bk+IsI14BPwieRoBQNU=" requestSignature := "G7Gtrh5Ee6d8erOVXhWPtUrkNJqqIT5vwLU50KhyLQk=" requestBody := "{\"webhook_setting_id\":\"246\",\"webhook_event_type\":\"message_created\",\"webhook_event_time\":1511238729,\"webhook_event\":{\"message_id\":\"984676321621704704\",\"room_id\":36818150,\"account_id\":1484814,\"body\":\"test\",\"send_time\":1511238729,\"update_time\":0}}" key, err := base64.StdEncoding.DecodeString(token) if err != nil { fmt.Println("token decode error:", err) } mac := hmac.New(sha256.New, key) mac.Write([]byte(requestBody)) expectedSignature := base64.StdEncoding.EncodeToString(mac.Sum(nil)) fmt.Println(requestSignature == expectedSignature) // true }
こちらも特に問題なく、公式のAPIリファレンスを参考に書くことができました。
どの言語も標準ライブラリをつかってごく簡単に検証することが可能でした。
みなさんも好きな言語をつかってチャットワークのWebhookをつかったサービスを作ってみてください!
こんにちは。プロダクト開発部の火村(id:eiel)です。
プロダクト開発部では、定期的に定例ミーティングが行われていて、技術的な内容のライトニングトークする時間があります。 発表者はプロダクト開発部のメンバーがローテーションしていて、一度のミーティングで発表するのは一人といった感じです。
先日、私の当番がやってきてしまいました。 いろいろ話したいことはあるのですが、時間がなかったため、今回は入院中にどうやって勉強していたという軽い話をしました。 内容が特に公開して問題のあるものではないので、ブログに書いてみることにしたのが今回の記事です。
こんにちは。田中といいます。 PHPカンファレンス2017 - #phpcon2017に参加し登壇をしてきました。
「ChatWorkとPHPと私」というタイトルで登壇しました。 ふざけたタイトルですが、ChatWork株式会社の失敗や成功の歴史を紹介しました。
発表の詳細は主に
みたいな話が含まれていますので、興味ありましたらスライドをご参考ください。
発表にあたり、本当にここまで話してよかったものかと思い悩んでいた時期もありましたが下記記事にも感化され公開するにいたりました。
また、ここまでのぶっちゃけトークを許可していただいたChatWorkもなかなか懐深いところがあるんだなとあらためて感じました。
実はPHPカンファレンスは初めての参加だったのですが、その規模に圧倒されました。 7並列で発表が行われていたり、参加枠が2000人だったりと、今まで私が参加してきたカンファレンスで一番大きなものでした。 このような場で1時間も枠をいただいて登壇できたことは、私にとってとても貴重な体験でした。
来年も12月頃に開催するらしいので、来年もまた参加したいと考えています。(登壇するかどうかは不明)
ということで、ChatWorkではPHPエンジニアもScalaエンジニアも募集中です。 私達と一緒に、日本発のビジネスチャットを世界に広めていきましょう!