Firebase Authenticationで自前のバックエンドサーバーを使う
Firebase Authenticationでの認証 + 自前のバックエンドサーバー、という構成にしたい場合、クライアント側から「自分はFirebaseではこういうIDトークンの者です」という情報を送ってあげる必要がありますが、その際に本当に該当ユーザであり、悪意のあるユーザによるリクエストではないことを判別する必要があります。
認証結果として得られるIDトークンはJWTなので、Google側の公開鍵を利用して検証することで、改竄されておらず、Firebase側で認証されたユーザであるということが確認できます。
参考: https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja
SDKの対応状況
さて、上記のIDトークンの検証処理は、一部の言語ではSDKが提供されているため簡単に実装できます。
参考までに、2018/5時点で対応しているのは以下言語です。
- node
- Java
- Python
- Go
しかし、それ以外の言語では、独自で同等の検証処理を実装する必要があります。
Rubyでやりたい
RubyはSDKが無いため、上記の通り独自で検証する必要があります。
基本的な検証ロジック自体はfirebaseのドキュメントを参考にすれば良いですが、
Google公開鍵の取得頻度についてどうするかは自分で考える必要があります。
Google公開鍵を毎回取得する
これも場合によっては1つの解ではあるかもしれません。
しかし、例えば認証を前提とするSPAの構築などを想定した場合、
APIを呼ぶたびに都度公開鍵を取得しに行くことになり、オーバヘッドになるかもしれません。
Google公開鍵をキャッシュする
ドキュメントには以下のような記述があります。
最後に、トークンの kid 要件に対応する秘密鍵によって ID トークンが署名されたことを確認します。https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com から公開鍵を取得し、JWT ライブラリを使用して署名を確認します。該当エンドポイントからのレスポンスの Cache-Control ヘッダーに含まれる max-age の値を使用して、公開鍵を更新する時期を確認します。
つまり、max-ageを見たうえでキャッシュすることは想定されているようです。
これであれば必要最低限の取得で済むので良さそうですね。
あとは、どのようにキャッシュするかです。
faraday-http-cache
さて、Rubyにはfaraday-http-cacheというGemが存在します。
これを利用すると、faradayを利用した特定のリクエストのみをキャッシュすることができ、
ActiveSupport::Cacheにも対応しているので、Railsと組み合わせるのも簡単です。
しかも、内部でmax-age
をもとにしたTTLも行ってくれます。
これで完璧!と思いきや...
キャッシュされない
試しに以下のようなコードで公開鍵を取得してみます。
client = Faraday.new do |builder|
builder.use :http_cache, store: Rails.cache, logger: Rails.logger
builder.adapter Faraday.default_adapter
end
response = client.get(CERTIFICATION_URL)
しかし、動かしてみると以下のようなログが出力され、一切キャッシュされません。
HTTP Cache: [GET /robot/v1/metadata/x509/securetoken@system.gserviceaccount.com] miss, uncacheable
原因
公開鍵を取得した際のレスポンスヘッダを見てみると、must-revalidate
が含まれていることがわかります。
(参考: https://developer.mozilla.org/ja/docs/Web/HTTP/Caching#Cache_validation)
これには、「キャッシュにヒットしても、期限が有効かどうかを確認せずに利用してはならない」という意味合いがあります。
また、must-revalidate
がある場合には、ETag
ヘッダーやLast-Modified
ヘッダと組み合わせた陳腐化の検証を行うことが可能ですが、これらは公開鍵のレスポンスヘッダには含まれていません。
faraday-http-cacheでは、must-revalidate
が含まれる場合にはETag
,Last-Modified
を利用した検証を行うような実装となっているのですが、含まれていない場合には「キャッシュすることが出来ない」という判断を行います。
このため、一切キャッシュされないという動きになっていたようです。
キャッシュされるようにする
firebaseのドキュメント上には
該当エンドポイントからのレスポンスの Cache-Control ヘッダーに含まれる max-age の値を使用して、公開鍵を更新する時期を確認します。
とあるので、ETag
,Last-Modified
ではなく、max-age
を元にしてキャッシュすることは特に問題なさそうです。
というわけで、やや強引ですが、レスポンスから強制的にmust-revalidate
を削り落とすFaradayのMiddlewareを挟むことでキャッシュ可能となるようにしてあげます。
class IgnoreMustRevalidate < Faraday::Middleware
def initialize(app, *args)
super(app)
end
def call(env)
dup.call!(env)
end
def call!(env)
response = @app.call(env)
# faraday-http-cacheでキャッシュさせるためにmust-revalidateをヘッダから落とす
response.headers["cache-control"] = response.headers["cache-control"].gsub(/must-revalidate(, )?/, "")
response
end
end
client = Faraday.new do |builder|
builder.use :http_cache, store: Rails.cache, logger: Rails.logger
# これを足す
builder.use IgnoreMustRevalidate
builder.adapter Faraday.default_adapter
end
response = client.get(CERTIFICATION_URL)
これで再実行してみます。
HTTP Cache: [GET /robot/v1/metadata/x509/securetoken@system.gserviceaccount.com] miss, store
ちゃんとキャッシュされているようです!
再度リクエストを投げてみると..
HTTP Cache: [GET /robot/v1/metadata/x509/securetoken@system.gserviceaccount.com] fresh
2回目以降のリクエストではキャッシュが利用されていますね!
まとめ
というわけで、自分でキャッシュを頑張りたくないために、
結果として余計なところで頑張った結果をまとめてみました。
SPAで認証だけ楽にしたい〜〜!
というときにFirebaseAuthenticationだけ使うと便利ぽいので、積極的に試していきたい。