スマートフォンアプリ向けのプッシュ通知配信に便利なFCMだがRuby実装のSDKが存在しない。
プッシュ通知送信でFCMを使う機会があり調査していたものの、トピックを使ったプッシュ通知の一斉配信をする際に、それまでのFCM API認証とは異なる認証を利用する必要が出てきた。
プッシュ通知を送るだけなのに2種類の認証を実装しないといけなくなってしまった。
gemも fcm と andpush の2つを読んだが、いずれも古いバージョンのAPIと認証方式(※後述)を利用していてあまり使いたくなかった。
- https://github.com/spacialdb/fcm/blob/d7ccf74a56d236b07848cfc37beebd89cbd48854/lib/fcm.rb#L183
- https://github.com/yuki24/andpush/blob/78e66d35a5f437d3f25c6a75a14cd25a0b771ed3/lib/andpush.rb#L40
FCMのドキュメントによるとLegacy APIからHTTP v1 APIに移行が推奨されているため、自分で実装してみることにした。
Migrate from legacy HTTP to HTTP v1 | Firebase
今回のユースケースではHTTP v1 APIを使った単発のプッシュ通知配信のほか、トピックを使った一斉配信も実現したかったためInstance ID APIも使うことになったが、ここの認証でつまづいた。
Instance ID API | Google Developers
HTTP v1 APIとInstance ID APIの認証方式の違い
実装を始めてから気付いたが、2種類のAPIで認証方式が違った。
HTTP v1 API
プッシュ通知を送信する際に利用するAPI
HTTP v1 APIの認証は先述のドキュメントでも触れられているように、OAuth2.0の認証トークンを利用することが推奨されている。
require 'googleauth' require 'faraday' authorizer = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open('fcm-your-secret-key.json'), scope: 'https://www.googleapis.com/auth/firebase.messaging', ) access_token = authorizer.fetch_access_token! conn = Faraday.new(url: 'https://fcm.googleapis.com/v1/projects/$YOUR_PROJECT_ID/messages:send') response = conn.post do |req| req.headers['Content-Type'] = 'application/json' req.headers['Authorization'] = "Bearer #{access_token['access_token']}" # Bearer Tokenとして取得したトークンを埋める req.body = ... end
Instance ID API
対してトピック操作などで利用する Instance ID API は Legacy API でサポートされているサーバーキーを使った認証を行う。
サーバーキーはFCMコンソールの設定画面から取得可能。
server_key = 'YOUR_SEVER_KEY' conn = Faraday.new(url: 'https://iid.googleapis.com/iid/v1:batchAdd') response = conn.post do |req| req.headers['Content-Type'] = 'application/json' req.headers['Authorization'] = "key=#{server_key}" req.body = { to: '/topics/your_topic', registration_tokens: ['$YOUR_FCM_TOKENS'] }.to_json end
公式SDKでの認証
公式SDKのドキュメントではHTTP v1 APIで用いる秘密鍵 + OAuth2.0のトークンを利用した認証方法しか記載していないにもかかわらず
Instance ID APIを用いるはずの SubscribeToTopic
メソッドが実装されている。
https://godoc.org/firebase.google.com/go/messaging#Client.SubscribeToTopic
HTTP v1 APIに https://iid.googleapis.com/iid/v1:batchAdd
に相当するエンドポイントが実は存在するのかと思ったが
SDKの実装を読むと Instance ID API を利用していることがわかった。
サーバーキーを渡している形跡がないにもかかわらず、なぜ Instance ID API が利用できるのかわからなかったので、さらに読み進めると見覚えのないHTTP Headerを設定している箇所を見つけた。
func newIIDClient(hc *http.Client) *iidClient { client := internal.WithDefaultRetryConfig(hc) client.CreateErrFn = handleIIDError client.Opts = []internal.HTTPOption{internal.WithHeader("access_token_auth", "true")} return &iidClient{ iidEndpoint: iidEndpoint, httpClient: client, } }
SDKではBearer Tokenに加えて access_token_auth: true
なHeaderを付与していた。
公式ドキュメントでは見たことがないヘッダーだったので site:developers.google.com "access_token_auth"
でググるなどしてそれらしき記述を探したが、見つけることはできなかった。
隠れ機能なんだろうと思うことにしている。
Rubyで公式SDK流の認証を試してみる
まずは access_token_auth: true
なしでBearer Tokenを使った認証を試す。
Instance ID APIではサポートされていないはずの認証なので 401 Unauthorized が返ってくる。
require 'googleauth' require 'faraday' authorizer = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open('fcm-your-secret-key.json'), scope: 'https://www.googleapis.com/auth/firebase.messaging', ) access_token = authorizer.fetch_access_token! conn = Faraday.new(url: 'https://iid.googleapis.com/iid/v1:batchAdd') response = conn.post do |req| req.headers['Content-Type'] = 'application/json' req.headers['Authorization'] = "Bearer #{access_token['access_token']}" # Bearer Tokenとして取得したトークンを埋める req.body = { to: '/topics/your_topic', registration_tokens: ['$YOUR_FCM_TOKENS'] }.to_json end p response.body => "<HTML>\n<HEAD>\n<TITLE>Unauthorized</TITLE>\n</HEAD>\n<BODY BGCOLOR=\"#FFFFFF\" TEXT=\"#000000\">\n<H1>Unauthorized</H1>\n<H2>Error 401</H2>\n</BODY>\n</HTML>\n"
今度は access_token_auth: true
を付与してリクエストしてみると...
require 'googleauth' require 'faraday' authorizer = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open('fcm-your-secret-key.json'), scope: 'https://www.googleapis.com/auth/firebase.messaging', ) access_token = authorizer.fetch_access_token! conn = Faraday.new(url: 'https://iid.googleapis.com/iid/v1:batchAdd') response = conn.post do |req| req.headers['Content-Type'] = 'application/json' req.headers['access_token_auth'] = 'true' req.headers['Authorization'] = "Bearer #{access_token['access_token']}" # Bearer Tokenとして取得したトークンを埋める req.body = { to: '/topics/your_topic', registration_tokens: ['$YOUR_FCM_TOKENS'] }.to_json end p response.body => {:results=>[{}]}
正常にリクエストが通った。
まとめ
ドキュメントには存在しない認証方式がInstance ID APIには存在することがわかった。
この記事で検証したとおりSDKで使われている方法で実装することも可能だが、公式に言及されているわけではないので気軽に使うとある日突然痛い目を見るかもしれない。
公式でRuby SDKを提供してくれれば全て解決する話なので、Googleさん色々忙しいとは思うんですがそこをなんとか頼んます。