Firebase Authを Nuxt + Railsの自前サービスに導入する

この記事は [Firebase Advent Calendar 2019](https://qiita.com/advent-calendar/2019/firebase) 19日目の記事です🎉

## はじめに

今年、個人開発で [ゆるすけ](https://yurusuke.com/) というサービスをリリースしました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/49b60a9e-f720-9bd6-cd79-7017fb7688e9.png)

自分がお出かけするときに「誘うほどじゃないけど、誰か行かない？」と可愛い画像をTwitterに投稿して「**ゆるくスケジュールを共有する**」サービスです。

勉強会の参加予定なんかにも相性が良いと思うので、ぜひ使ってください。（ちゃっかり宣伝）

さて、このサービスは Nuxt.js と Ruby on Rails で作られてますが、認証に Firebase Auth を使ってます。
少しハマりどころもあったので、将来同じような構成をトライする人のために知見を共有したいと思います！

## 認証周りの構成

こんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/12a5cf12-c031-7fde-1a6a-3b563f8cb846.png)

（ちなみに認証に Firebase Auth を使ったのはSPA で Twitter ログインのコールバックを管理するのが面倒だったからなのですが、これはこれで結構ハマったので今となってはどっちでもよかったかもと思ってます笑）

## Nuxt.js に Firebase Auth を導入する

たくさん記事があるのでざっくり **割愛** します！
「Firebase Auth Nuxt」
「Firebase Auth Vue」
などで検索してみてください。

## 自前のバックエンド（Rails）と連携する

### Knock を導入する
通常の Rails アプリでは Sorcery や Devise を使いますが、
今回は Firebase Auth & API モードなので、バックエンド側の認証を独自で実装する必要があります。
とはいえ１から全て実装するのは大変・・・
と何かいい Gem がないか探していたところ、 `Knock` という Gem を発見しました。

`authenticate_user` や `current_user` などのおなじみのメソッド使えます🙌

導入方法は README をご確認ください。
[nsarno/knock](https://github.com/nsarno/knock)

### Firebase Auth の JWT 検証の仕組みを追加する

公式で用意されている 「Firebase Admin SDK」 を使うと Firebase Auth のトークンを検証できますが、  
残念ながら Ruby 版は用意されていません😭
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/5a89d2fb-c390-608d-c647-7ef8d1f32eb3.png)

対応言語でモジュール作って部分的に呼び出す、とかも考えたのですがちょっと面倒臭い・・・

というわけで、自前で（ `OpenSSL` で）検証部分を実装することに。

Knock 自身にも JWT を検証する機能もついて今すが、検証用の公開鍵に変化がない前提になっています。
Firebase の公開鍵は定期的に変更されるので、都度取得する必要があります＞＜

そのため Knoｃｋ の機能は使わず、以下のように **Knock のメソッドをオーバーライドして検証**することにしました！

```ruby:application_controller.rb
module Knock::Authenticable
  def define_current_entity_getter(entity_class, getter_name)
           # 中略
           response = client.get("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")
           jwks_raw = response.body
           JSON.parse(jwks_raw).each do |_key, key_string|
             jwks_string = key_string.gsub("-----BEGIN CERTIFICATE-----", "").gsub("-----END CERTIFICATE-----", "").delete("\n")
             Knock.token_signature_algorithm = "RS256"
             Knock.token_public_key = OpenSSL::X509::Certificate.new(Base64.decode64(jwks_string)).public_key
             begin
               @payload = Knock::AuthToken.new(token: token).entity_for(entity_class)
               break if @payload.present?
             rescue
               next
             end
             # 後略
```

ただ毎回鍵を取りに行ってしまいレスポンスが遅くなるので、
実際には Faraday を使って鍵をキャッシュしています。

## ログイン・新規登録の仕組みを作る

User モデル等に以下のメソッドを作ると、
新規ユーザーなら新規登録、登録済ならログインされます。

```ruby:models/user.rb
def self.from_token_payload(payload)
  // Userが入れば取得
  user = find_by(sub: payload["sub"])

  // いなければ新規作成
  user || create!(sub: payload["sub"],
                 user_name: payload["name"][0..29],
                 remote_image_url: payload["picture"].sub(/_normal\./, "_bigger."))
end
```

ここまで実装できれば、あとはいつも通り `authenticate_user` や `crrent_user` を呼び出すだけ！

## まとめ
- Rails バックエンドで Firebase Auth 使うのはちょっと大変だった
- とはいえ、上記の仕組みさえ作ってしまえばあとは `Sorcery` や `Devise` と変わらず使える
- せっかく Auth と連携できたので、いつか Auth 以外の Firebase 機能と連携したい