アドベントカレンダー1番手です。
2016年は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 コンソールからプロジェクトを作成しておきます。
次にサービスアカウントを作成します。
プロジェクトの設定 -> サービスアカウント -> すべてのサービス アカウントを管理 -> サービスアカウントを作成 をクリックします。
鍵の名前はdev-app
で新しい秘密鍵の提供にチェックを入れてp12形式でダウンロードします。
以下のコマンドでダウンロードしたp12形式の鍵をpem形式にします。
$ cat /path/to/key.p12 | openssl pkcs12 -nodes -nocerts -passin pass:notasecret | openssl rsa > backend/service_account.pem
Slack
次にSlackのApp Directoryでアプリケーションの作成をします。
作成をするとClientIDとClientSecretが取得できるので控えておきます。
dev.env
作成したプロジェクトIDとSlackアプリのClientIDとClientSecretをGAEにセットします。
今回はgodotenvを使って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の認可確認画面が現れるので認可するとスクリーンショットのように名前とアイコンが表示されます。
認証の流れ
基本的な流れとしては
- 通常のSlack認証でSlackのアクセストークンを発行する。
- Slackのアクセストークンを使ってプロフィール情報を取得する。
- SlackのユーザーIDをuidとしてFirebaseのカスタムトークンを作成する。(デモではSlackのトークンはデータベースに保存している)
- ユーザーにカスタムトークンを渡して、
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
は有効期限ですがカスタムトークンで指定できる有効期限は最大1時間までとなっています。
署名用の鍵はGAE/GoのAPIのIdentity APIを使います。
これは署名や検証をしてくれるAPIなので、利用することでプロダクションに関してはサービスアカウントの管理をしなくても済むようになります。
(ローカルでもFirebase Authの検証をするにはサービスアカウントが必要ですが...)
GoでJWTの生成は jwt-go を使っています。
jwt-goはSigningMethodを定義することでIdentity APIで署名と検証をすることができるようになります。
コードは以下の様になります。
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を渡してあげると署名ができます。
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
に相当するものを実装しなければいけないのでクライアントの実装が面倒になると思います。
ただサーバ側でトークンに関する構造体を2つ用意しなくちゃいけなくなるのでサーバ側に手間を寄せることになってしまいます。
でもクライアントがiOSだった場合のアップデートの手間などを考えるとサーバ側に寄せてしまったほうが色々都合がいいのではないでしょうか。
まとめ
- 色んな認証をする場合はカスタム認証システムは抑えておきましょう!
- Firebaseはモバイル以外にもWebでも使えます!(Firebase Analyticsは使えないけどね)
- デモではセッション情報をCookieに入れてるけどRealtimeDatabaseに入れてみてもいいかもしれないですね。
そういえばFirebase日本ユーザーグループってあるんですかね?
GCPの仲間になってるからGCPUGの一部なのかなーと思ってるけど分野が若干違うみたいなので別にあってもいいのかもしれないです。
(既にあるならこっそり教えてください)
来年はモバイルアプリでFirebaseを100%で使い倒したいです。
明日は @fullkawa さんです!