Chatwork Creator's Note

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

読者になる

チャットワークのWebhookの署名検証を各言語で実装してみた その2

こんにちは。@eielhです。 id:cw-hayashiが投稿した記事である 「チャットワークのWebhookの署名検証を各言語で実装してみた」があります。 しかし、自分が利用しているプログラミング言語が網羅されていなかったので、続きがないのか確認してみたところ「自分でやってください」*1と言われてしまいました。 というわけで、早速挑戦してみました。

  • 準備するもの
  • Elixir
    • 参考
  • Rust
    • 参考
  • Haskell
    • 参考
  • まとめ

*1:正確には遠回しにしか聞いていません

続きを読む

ScalaアプリをDockerイメージ化してHerokuにデプロイした話

@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の手順で進めました。

devcenter.heroku.com

アプリケーションを用意する

まずはデプロイ対象のアプリケーションが必要です。
今回はサンプルとして、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)
  })
}

Dockerイメージを作成する

ChatWorkのScalaプロダクトではDockerイメージの作成は、sbt-native-packagerを利用しています。

今回も同様にsbt-native-packagerを利用しました。

plugins.sbt

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.1")

build.sbt

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)

いくつかポイントがあります。

  • Heroku Container Registry に Push するイメージのDockerfileではCMDを利用する
    • sbt-native-packagerではENTRYPOINTを使う形でDockerfileが出力されるので、変換するコードを入れています
  • DockerfileでEXPOSEは使えない
    • 記述があっても無視されます
  • 利用するコンテナポートはHerokuが環境変数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アプリを作成する

続いて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

HerokuにイメージをPushする

すでに存在する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アプリケーションをぜひ作ってみてください!

チャットワークのWebhookの署名検証を各言語で実装してみた

@hayasshi_です。Scalaのプロダクトを開発しています。

ついにチャットワークのWebhookがリリースされました!🎉
OAuthとあわせて、様々なサービスがチャットワークと連携しやすくなります。

今日はチャットワークのWebhookの署名検証について少しお話します。

Webhookの署名検証

チャットワークのWebhookは、イベントに応じてチャットワークからHTTPリクエストが行われ、それを連携先のサービスで受け付けていただく必要があります。

万一リクエスト先のURLが漏れてしまった場合、チャットワークになりすましてHTTPリクエストを送ることができてしまいます。

そこでチャットワークのWebhookでは、Webhookの設定毎にトークン(署名検証用の秘密鍵)を発行し、それをつかってHTTPリクエストがチャットワークからであるということを検証していただくことができます。(詳細は公式ドキュメントを参照してください。)

この記事では、各言語での署名検証のサンプルコードをご紹介したいと思います。

準備するもの

  • Webhookの設定で発行された署名検証用トークン
    • Webhookの設定編集画面で確認できます
    • f:id:cw-hayashi:20171121170324p:plain
  • WebhookのHTTPリクエストボディ
    • お使いのWebアプリケーションフレームワークなどのAPIをつかって、リクエスト毎に取得してください
    • 注意点として、リクエストされてきたそのままの文字列を取得してください
      • 例えば一度Jsonデシリアライズしたものをもう一度シリアライズするなどすると、フィールドの順番がいれかわったり、スペースがトリムされたりなど、元のリクエストボディの値と差異が出てしまう可能性があります
  • WebhookのHTTPリクエストヘッダX-ChatWorkWebhookSignatureの値
    • お使いのWebアプリケーションフレームワークなどのAPIをつかって、リクエスト毎に取得してください

今回は各言語での署名検証のコードのみにフォーカスするため、トークン等は下記にて準備されたものを利用します。 (実際のチャットワークの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が標準ライブラリにあればごく簡単に扱うことができます。

  • Base64エンコード・デコード
  • HMAC-SHA256でのメッセージダイジェストの作成

それではどんどんいってみましょう。

PHP

弊社ではまだまだ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エンコードしないと結果が変わってしまうことに注意が必要です。

Java

弊社では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から導入されたので楽に実装できました。

JavaScript (Node.js)

弊社(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

JavaScript (Google Apps Script)

Google Apps ScriptではNode.jsと環境が異なるため、以下の記事を参照ください。

Google Apps ScriptでChatWorkのWebhook署名を検証する方法 - ChatWork Creator's Note

Ruby

環境は以下のバージョンの対話ツール(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

私事ですが、この署名検証のコードが私の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)です。

プロダクト開発部では、定期的に定例ミーティングが行われていて、技術的な内容のライトニングトークする時間があります。 発表者はプロダクト開発部のメンバーがローテーションしていて、一度のミーティングで発表するのは一人といった感じです。

先日、私の当番がやってきてしまいました。 いろいろ話したいことはあるのですが、時間がなかったため、今回は入院中にどうやって勉強していたという軽い話をしました。 内容が特に公開して問題のあるものではないので、ブログに書いてみることにしたのが今回の記事です。

  • スライド
  • 入院中の勉強の仕方
    • 入院中の端末の利用可否
    • 長期入院時に重要なこと
  • 具体的な勉強方法や情報収集
    • 書籍
    • Reddit
    • GitHub Trending
    • GitBookで生成されたページ
    • ちょっと微妙だったもの
  • 長い入院の経験と「インクルーシブデザイン」
  • まとめ
続きを読む

「ChatWorkとPHPと私」というタイトルで登壇しました @PHPカンファレンス2017

こんにちは。田中といいます。 PHPカンファレンス2017 - #phpcon2017に参加し登壇をしてきました。

発表内容

「ChatWorkとPHPと私」というタイトルで登壇しました。 ふざけたタイトルですが、ChatWork株式会社の失敗や成功の歴史を紹介しました。

speakerdeck.com

発表の詳細は主に

  • ChatWorkが生まれた背景
  • レガシーとの戦い
  • Scala化Projectの失敗と成功
  • PHP7 ? HHVM ?

みたいな話が含まれていますので、興味ありましたらスライドをご参考ください。

発表にあたり、本当にここまで話してよかったものかと思い悩んでいた時期もありましたが下記記事にも感化され公開するにいたりました。

www.yasuhisa.com

また、ここまでのぶっちゃけトークを許可していただいたChatWorkもなかなか懐深いところがあるんだなとあらためて感じました。

PHPカンファレンス2017について

実はPHPカンファレンスは初めての参加だったのですが、その規模に圧倒されました。 7並列で発表が行われていたり、参加枠が2000人だったりと、今まで私が参加してきたカンファレンスで一番大きなものでした。 このような場で1時間も枠をいただいて登壇できたことは、私にとってとても貴重な体験でした。

来年も12月頃に開催するらしいので、来年もまた参加したいと考えています。(登壇するかどうかは不明)

ChatWorkではエンジニアを募集中です

ということで、ChatWorkではPHPエンジニアもScalaエンジニアも募集中です。 私達と一緒に、日本発のビジネスチャットを世界に広めていきましょう!