GCMの登録が不要になったChromeのWeb Pushを試してみる

  • 117
    いいね
  • 3
    コメント

※ 2016-10-03: Authorizationヘッダの先頭に記述するスキームがBearerからWebPushに変更されたようですので、更新しました。

W3CとIETFで標準化が進むWeb Pushですが、Firefoxでは事前の設定を特に必要としないのに対し、ChromeではAndroidアプリと同様にGoogle Cloud Messaging (GCM)の登録とキーやIDの取得が必要となります。従って、Webアプリ側では、Chrome専用のマニフェスト記述が、アプリケーションサーバ側ではGCMサーバの認証用に専用のHTTPヘッダの記述が必要となっています。

これに対し、ようやくプッシュサーバのアプリサーバに対する認証などの仕様について、IETFでの策定が進み、ChromeやFirefoxで実装が進んできています。今回はこの新しいサーバ認証の仕組みを使って、GCM特有の実装が不要になったWeb Pushを試してみます。これで、ChromeでもFirefoxでも完全に同じ手順でプッシュ通知に対応できるようになります

対応ブラウザは、Chrome 52以降、Chrome for Android 52以降、Firefox 48以降となります。(Chrome 51では、chrome://flagsでExperimental Web Platform FeaturesをEnabledにすると使えるようになります。)

全体の流れ

Web Pushでアプリケーションサーバのプッシュサーバへの登録と認証を行う仕組みとして、IETFではVoluntary Application Server Identification for Web Push (VAPID)の標準化作業が進んでいます。まず、このVAPIDの全体の流れを説明します。

78faeb04-41e3-a37a-0884-217cd8a5c931.png

  1. アプリケーションサーバはECDSAの鍵ペアを生成する。
  2. ブラウザ(UA; User Agent)はアプリケーションサーバからECDSA公開鍵を受け取る。
  3. ブラウザがプッシュサーバにプッシュ通知の購読を登録する際、アプリケーションサーバのECDSA公開鍵を渡す。
  4. アプリケーションサーバがプッシュサーバにプッシュ通知を送信する際、ECDSA秘密鍵を使ってSHA-256 with ECDSAによってJSON Web Token (JWT)形式の署名トークンを生成し、ECDSA公開鍵と合わせてプッシュサーバに渡す。
  5. プッシュサーバは、ECDSA公開鍵を使ってアプリケーションサーバから受け取ったJWTを検証し、内容に問題がなければプッシュ通知をブラウザに転送する。

以上の手順のうち、3.-5.は暗号化付きWeb Pushの手順の中で合わせて行う形となります。

なお、VAPIDはVoluntary(自発的)というだけあって、この仕様自体はJWTによる認証を必須にしているわけではありません。また、3.の手順を踏むことで、そのブラウザ(Webアプリ)に対するプッシュ通知に対してJWTによる認証が必須になりますが、Firefoxでは今のところ3.の手順は必須ではありません。Chromeの場合、今のところ、従来のGCM専用拡張かVAPIDによる認証のいずれかを使うことが必須になっています。

実際の処理

実際の具体的な処理や実装の仕方については、基本的にはこれまでのPush APIの使い方に則ったものですのでまずはChromeでW3C Push APIを使ってみたWeb Pushでブラウザにプッシュ通知を送ってみるを参照して下さい。本記事では、サーバの登録や認証に関連する部分にフォーカスして説明します。

なお、今回のVAPIDに対応する以前のバージョンのChrome (≦51)に対応する必要がなければ、マニフェストに記載していたgcm_sender_id等が不要になります。

詳細はサンプルコードおよびデモをご覧下さい。また、ブラウザからプッシュサーバへの鍵登録以外の部分については、次のNode.js向けコードが参考になります。

なお、VAPIDでは、SHA-256 with ECDSA (P-256楕円曲線)による署名からJWTを生成する処理を伴います。ECDSAを用いた署名の作り方については、WebCrypto APIでECDSAの署名と検証を試してみるを参照して下さい。

ブラウザでのプッシュ通知の購読とサーバ公開鍵の登録

まず、ブラウザのWebアプリでは、アプリケーションサーバからP-256楕円曲線のECDSA公開鍵を予め受け取るようにします。このECDSA公開鍵をArrayBufferもしくはUint8Arrayに格納した上で、プッシュ通知の購読処理を次のようにします。

プッシュ通知の購読要求
let serverPublicKey = /* アプリケーションサーバから入手したものを格納 */;

navigator.serviceWorker.ready.then(reg => {
  return reg.pushManager.subscribe({
    userVisibleOnly: true,  /* Firefoxでは必須ではない */
    applicationServerKey: serverPublicKey
  });
}).then(subscription => {
  /* ... */
  /* 購読成功時の処理 */
  /* ... */
});

すなわち、PushManager.subscribe()のオプションとして、applicationServerKeyメンバにアプリケーションサーバの公開鍵を渡すようにします。この時、公開鍵が65バイトのバイナリデータではない場合に、購読処理がエラーとなります。なお、Chrome 50以前、Firefox 47以前では、このオプションは無視されます。

プッシュ通知時のサーバ認証

applicationServerKeyオプション付きでプッシュ通知の購読処理を行った場合、アプリケーションサーバからプッシュサーバにプッシュ通知をHTTP POSTでリクエストする際、サーバ認証の必要なヘッダを追加する必要があります。具体的には次の内容をHTTPリクエストヘッダに追加します。

  • Crypto-Keyヘッダに、p256ecdsa=(アプリケーションサーバのECDSA公開鍵をURLセーフBase64エンコードしたもの)を追加
  • Authorizationヘッダに、WebPush (アプリケーションサーバが署名したJWT)を記述
VAPIDを用いたWebPush暗号化時のHTTPリクエストの大まかな構造
POST /endpoint HTTP/1.1
Host: pushserver.example.com
Content-Encoding: aesgcm
Crypto-Key: keyid=p256dh;dh=...;p256ecdsa=...
Encryption: keyid=p256dh;salt=...
TTL: 86400
Authorization: WebPush ...

Crypto-Keyヘッダについてですが、Web Push暗号化では既に同名のヘッダを使っていますので、その末尾等に;(セミコロン)区切りでp256ecdsaフィールドを追加することになります。
Authorizationヘッダについては、WebPush(+空白)の後にJWTを記述します。ここで、JWTのヘッダとクレームの内容は次のようにします。

  • JWTヘッダ:
    • typの値: JWT
    • algの値: ES256 (SHA-256 with ECDSA (P-256)を表す)
  • JWTクレーム:
    • audの値: Webアプリ側で取得したendpointのURL(PushSubscription.endpoint)のorigin
    • expの値: 適当なJWTの有効期限(秒)、但しリクエスト送信時から24時間以内とすること
    • subの値(オプション): Webアプリの連絡先(mailto:(メールアドレス)もしくはWebアプリのURL)
JWTヘッダの例
{"typ":"JWT","alg":"ES256"}
JWTクレームの例
{"aud":"https://android.googleapis.com","exp":1464269795,"sub":"https://labs.othersight.jp/webpushtest/"}

これらのヘッダとクレームをURLセーフBase64でエンコードしたものを.で連結して、アプリケーションサーバのECDSA秘密鍵を使ってSHA-256 with ECDSAで署名を作成し、最初の連結文字列にさらに、.と、署名をURLセーフBase64でエンコードしたものを連結すると、JWTが生成できますので、これをAuthorizationヘッダ(WebPushの後)に記述することとなります。

Chromeのendpointに関して

Chrome 52、Chrome Beta 52 for Androidでは、VAPIDを適用、すなわち、PushManager.subscribe()のオプションにapplicationServerKeyを指定した時に得られるendpointのURLは、

https://fcm.googleapis.com/fcm/send/[registration_id]

のような形式に変更されており、Firebase Cloud Messagingを経由したプッシュ通知に移行されています(参考)。

なお、Chrome 51でフラグを有効化してVAPIDを試してみたところ、これまでのWeb Push対応と同様に、endpointのURLを

https://android.googleapis.com/gcm/send/[registration_id]
    ↓
https://gcm-http.googleapis.com/gcm/[registration_id]

のように書き換えないと正常に動作しないようです。

ECDSAで署名してJWTを生成する際の注意事項

アプリケーションサーバでJWTを生成する際、ECDSAによる電子署名のフォーマットに注意が必要です。Java 8のjava.security.Signature等で扱われるECDSA署名は、JWTのシグネチャ部(JWS)とは異なるフォーマットとなっているため、変換が必要になります。

詳細は、WebCrypto APIでECDSAの署名と検証を試してみるを参照して下さい。