LoginSignup
34
21

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-11-30

アドベントカレンダー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ファイルから環境変数にセットするようにしました。

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の認可確認画面が現れるので認可するとスクリーンショットのように名前とアイコンが表示されます。

スクリーンショット 2016-11-30 20.03.24.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})
isssubには署名するサービスアカウントのメールアドレスを指定します。
audhttps://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 さんです!

34
21
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
34
21