Firebase カスタム認証システムとGAE/GoでSlack認証

アドベントカレンダー１番手です。

２０１６年はFirebaseにとって大きなアップデートがあった年でした。
Firebase Analyticsが発表されてモバイルの解析ツールとしてすごいのが無料で使えるというすごいニュースでした。
このFirebase Analyticsがどれくらいすごいかというと月●●●万円するGoogle Analyticsの有料版レベルで使えるくらいすごいです。（EC向けの解析とか現在はまだまだGoogle Analyticsが優位な点は多々ありますが）

この記事ではFirebase Analyticsの話はありませんがFirebase AnalyticsやFirebaseを使う上でも基本となるFirebase認証についてです。
Firebase認証は一般的に使われているパスワード認証からGoogleやTwitter、Facebookなどのプロバイダ認証などサーバを用意せずに実装できる機能です。
サポートされている認証を使う分には何も難しいことはないのですがサポートされていない認証を使う場合はカスタム認証システムと自前でサーバを用意する必要があります。
その時Herokuなどで立てるより同じGCPの仲間であるGoogle App Engineを使うことによりサービスアカウントをスムーズに使えたり殆どの場合無料で運用することができます。

今回のデモではSlack認証を試してみました。

# Firebaseカスタム認証システムについて

Firebaseカスタム認証システムは管理者ユーザーやプレミアムユーザーなど特殊な権限をもたせたい時や自分のサービスやサポートされていないプロバイダ認証を使う時に使います。
サーバ用のSDKは今のところNode.jsとJavaしか提供されていないのですがカスタム認証をするためのトークンはJWTなので各言語のJWTライブラリが使えます。
JWTの署名にはGoogle Cloudのサービスアカウントを利用しているのでApp Engineを使うとAPIを通じて署名することもできます。

# 環境

Macです
コードはGitHubにあります。
https://github.com/k2wanko/firauth-example
デモはGAE/Go SDKとNode.jsを必要としています。

# デモのセットアップ

## Firebase

今回はローカルでも認証を通せるようにサービスアカウントを作成します。

まずは[Firebase コンソール](https://console.firebase.google.com/)からプロジェクトを作成しておきます。

次にサービスアカウントを作成します。
プロジェクトの設定 -> サービスアカウント -> すべてのサービス アカウントを管理 -> サービスアカウントを作成 をクリックします。

鍵の名前は`dev-app`で新しい秘密鍵の提供にチェックを入れてp12形式でダウンロードします。

以下のコマンドでダウンロードしたp12形式の鍵をpem形式にします。

```bash
$ cat /path/to/key.p12 | openssl pkcs12 -nodes -nocerts -passin pass:notasecret | openssl rsa > backend/service_account.pem
```

## Slack

次にSlackの[App Directory](https://api.slack.com/apps/new)でアプリケーションの作成をします。

作成をするとClientIDとClientSecretが取得できるので控えておきます。

## dev.env

作成したプロジェクトIDとSlackアプリのClientIDとClientSecretをGAEにセットします。

今回は[godotenv](https://github.com/joho/godotenv)を使ってenvファイルから環境変数にセットするようにしました。

```bash:dev.env
SESSION_SECRET=<random string>
SLACK_CLIENT_ID=<Slack Client ID>
SLACK_CLIENT_SECRET=<Slack Client Secret>
```

SESSION_SECRETにはランダムな文字列を指定します。
コード内のセッションに使うための文字列です。
`pwgen`コマンドなので生成するといいです。

ちなみに`goapp serve`で起動する場合はローカルの環境変数は取得できないのでファイルからセットするようにしています。

ここまでセットアップできれば

`npm run serve`で起動できます。

`http://localhost:8080`にアクセスして Slack loginボタンを押すとポップアップが開いてSlackの認可確認画面が現れるので認可するとスクリーンショットのように名前とアイコンが表示されます。

<img width="424" alt="スクリーンショット 2016-11-30 20.03.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/15818/cead7f4e-8695-617d-d5db-714a8d585f21.png">

# 認証の流れ

基本的な流れとしては

1. 通常のSlack認証でSlackのアクセストークンを発行する。
2. Slackのアクセストークンを使ってプロフィール情報を取得する。
3. SlackのユーザーIDをuidとしてFirebaseのカスタムトークンを作成する。（デモではSlackのトークンはデータベースに保存している）
4. ユーザーにカスタムトークンを渡して、`signInWithCustomToken(token)`を呼び出す。

# Firebase カスタムトークンの作成

Firebase カスタムトークンはFirebaseのカスタム認証システムで使用するJWTです。
カスタムトークンを作る時はJWTのClaimsの部分が決まったフォーマットになっているので自前で実装する場合はそこに注意する必要があります。
署名のアルゴリズムはRS256で固定です。
`uid`にはユーザーを識別するための任意の文字列、
`claims`にはセキュリティルールで使う権限を指定します。(例: `{is_premium: true}`)
`iss`と`sub`には署名するサービスアカウントのメールアドレスを指定します。
`aud`は`https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit`で固定です。
`iat`には発行した時間のUNIXタイム、`exp`は有効期限ですがカスタムトークンで指定できる**有効期限は最大１時間まで**となっています。

署名用の鍵はGAE/GoのAPIのIdentity APIを使います。
これは署名や検証をしてくれるAPIなので、利用することでプロダクションに関してはサービスアカウントの管理をしなくても済むようになります。
（ローカルでもFirebase Authの検証をするにはサービスアカウントが必要ですが...）

GoでJWTの生成は [jwt-go](github.com/dgrijalva/jwt-go) を使っています。
jwt-goはSigningMethodを定義することでIdentity APIで署名と検証をすることができるようになります。

コードは以下の様になります。

```go
type AppEngineSigningMethod struct{}

// Sign is Implment SigningMethod#Sign
func (s *AppEngineSigningMethod) Sign(signingString string, key interface{}) (string, error) {
	c, ok := key.(context.Context)
	if !ok {
		return "", jwt.ErrInvalidKey
	}

	_, sig, err := appengine.SignBytes(c, []byte(signingString))
	if err != nil {
		return "", err
	}
	return jwt.EncodeSegment(sig), nil
}

// Verify is Implment SigningMethod#Verify
func (s *AppEngineSigningMethod) Verify(signingString, signature string, key interface{}) error {
	c, ok := key.(context.Context)
	if !ok {
		return jwt.ErrInvalidKey
	}
	sig, err := jwt.DecodeSegment(signature)
	if err != nil {
		return err
	}
	certs, err := appengine.PublicCertificates(c)
	if err != nil {
		return err
	}

	haser := sha256.New()
	haser.Write([]byte(signingString))

	var certErr error
	for _, cert := range certs {
		key, err := jwt.ParseRSAPublicKeyFromPEM(cert.Data)
		if err != nil {
			return err
		}
		if certErr = rsa.VerifyPKCS1v15(key, crypto.SHA256, haser.Sum(nil), sig); certErr == nil {
			return nil
		}
	}

	return certErr
}
```

`appengine.SignBytes`が署名するためのAPIで`appengine.PublicCertificates`が公開鍵一覧を取得するAPIです。
鍵に相当する部分を`context.Context`にすることでApp EngineのAPI呼び出しに利用しています。


使う時は以下のコードのように`SignedString`の引数に`appengine.NewContext`で作ったcontextを渡してあげると署名ができます。

```go

t := &jwt.Token{
	Header: map[string]interface{}{
		"typ": "JWT",
		"alg": "RS256",
		"kid": "appengine",
	},
	Claims: claims, // 上記のFirebaseカスタムトークン用のクレーム
	Method: new(AppEngineSigningMethod),
}

t.SignedString(c)
```

# Firebase IDトークンとカスタムトークン

`getToken`で取得できるトークンはIDトークンです。
カスタムトークンとの違いは署名がFirebaseかサービスアカウントでやったかの違いです。
トークン構造もちょっと違います。
バックエンドロジックを実装したい時、認証のためにトークンをサーバに送ると思いますがその時は常にIDトークンを送るようにしたほうがいいと思います。
何故なら常に`getToken`で取得するようにするとSDKがクライアント側が持っているトークンの有効期限をチェックして切れていれば勝手にリフレッシュしてくれるためです。
カスタムトークンだと自前で`getToken`に相当するものを実装しなければいけないのでクライアントの実装が面倒になると思います。
ただサーバ側でトークンに関する構造体を２つ用意しなくちゃいけなくなるのでサーバ側に手間を寄せることになってしまいます。
でもクライアントがiOSだった場合のアップデートの手間などを考えるとサーバ側に寄せてしまったほうが色々都合がいいのではないでしょうか。

# まとめ

- 色んな認証をする場合はカスタム認証システムは抑えておきましょう！
- Firebaseはモバイル以外にもWebでも使えます！（Firebase Analyticsは使えないけどね）
- デモではセッション情報をCookieに入れてるけどRealtimeDatabaseに入れてみてもいいかもしれないですね。

そういえばFirebase日本ユーザーグループってあるんですかね？
GCPの仲間になってるからGCPUGの一部なのかなーと思ってるけど分野が若干違うみたいなので別にあってもいいのかもしれないです。
（既にあるならこっそり教えてください）

来年はモバイルアプリでFirebaseを１００％で使い倒したいです。

明日は @fullkawa さんです！