HTML5
Chrome
firefox
Push通知
FirebaseCloudMessaging

Web Pushのサーバ認証VAPIDを試してみる (旧題: GCMの登録が不要になったChromeのWeb Pushを試してみる)

※ 2018-2-26: VAPIDのRFC 8292対応ですが、FirefoxとMicrosoft Edgeが対応済みで、Chromeでも少々手を加えれば使えるようです。

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以降、Microsoft Edge (EdgeHTML 17以降)となります。(Chrome 51では、chrome://flagsでExperimental Web Platform FeaturesをEnabledにすると使えるようになります。)

全体の流れ

Web Pushでアプリケーションサーバのプッシュサーバへの登録と認証を行う仕組みとして、IETFではVoluntary Application Server Identification for Web Push (VAPID) (RFC 8292)が標準化されています。まず、この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の場合、今のところ、従来のFCM/GCM専用拡張かVAPIDによる認証のいずれかを使うことが必須になっています。

実際の処理

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

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

詳細はサンプルコードおよびデモをご覧下さい。また、Node.js向けのWeb Pushライブラリweb-pushがVAPIDに対応しています。

なお、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,
    applicationServerKey: serverPublicKey
  });
}).then(subscription => {
  /* ... */
  /* 購読成功時の処理 */
  /* ... */
});

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

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

applicationServerKeyオプション付きでプッシュ通知の購読処理を行った場合、アプリケーションサーバからプッシュサーバにプッシュ通知をHTTP POSTでリクエストする際、サーバ認証の必要なヘッダを追加する必要があります。現時点では、Chrome (FCM)はdraft-ietf-webpush-vapid-01に、Firefox (Autopush)とMicrosoft Edge (Windows Push Notification Service (WNS))はRFC 8292およびdraft-ietf-webpush-vapid-01に対応しています。

以下、JWTに記述する内容と、各バージョンでのHTTPリクエストヘッダの内容を順に示します。

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が生成できます。

HTTPリクエストヘッダ: RFC 8292 (Chrome(要ハック)/Firefox/Edge)

RFC 8292に対応する場合、アプリケーションサーバがプッシュサーバに送信するリクエストには、HTTPリクエストヘッダとして以下を追加します。

  • Authorizationヘッダに、vapid t=(アプリケーションサーバのECDSA公開鍵をURLセーフBase64エンコードしたもの) k=(アプリケーションサーバが署名したJWT)を記述
VAPID(RFC8292)+aes128gcm
POST /endpoint HTTP/1.1
Host: pushserver.example.com
Content-Encoding: aes128gcm
TTL: 86400
Authorization: vapid t=... k=...
VAPID(RFC8292)+aesgcm
POST /endpoint HTTP/1.1
Host: pushserver.example.com
Content-Encoding: aesgcm
Crypto-Key: dh=...
Encryption: salt=...
TTL: 86400
Authorization: vapid t=... k=...

すなわち、Web Push暗号化でaes128gcm、VAPIDをRFC 8292準拠とすると、これまでのバージョンのWeb PushやVAPIDで使われていたCrypto-Keyヘッダが完全に不要になります。

ChromeでのRFC 8292対応

https://github.com/web-push-libs/web-push/issues/278#issuecomment-356783840 によると、RFC 8292のVAPIDをFCMで使うには、

FCMのエンドポイントURLにおいてfcm/sendの部分をwpに書き換える

という手順を加えるとよいとのことです。恐らく一時的なハックだと思われますので、不要になったのが確認出来次第、この説明を削除します。

HTTPリクエストヘッダ: draft-ietf-webpush-vapid-01 (Chrome/Firefox/Edge)

draft-ietf-webpush-vapid-01に対応する場合、アプリケーションサーバがプッシュサーバに送信するリクエストには、HTTPリクエストヘッダとして以下を追加します。

  • Crypto-Keyヘッダに、p256ecdsa=(アプリケーションサーバのECDSA公開鍵をURLセーフBase64エンコードしたもの)を追加
  • Authorizationヘッダに、WebPush (アプリケーションサーバが署名したJWT)を記述
VAPID(draft-ietf-webpush-vapid-01)+aes128gcm
POST /endpoint HTTP/1.1
Host: pushserver.example.com
Content-Encoding: aes128gcm
Crypto-Key: p256ecdsa=...
TTL: 86400
Authorization: WebPush ...
VAPID(draft-ietf-webpush-vapid-01)+aesgcm
POST /endpoint HTTP/1.1
Host: pushserver.example.com
Content-Encoding: aesgcm
Crypto-Key: dh=...;p256ecdsa=...
Encryption: salt=...
TTL: 86400
Authorization: WebPush ...

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

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

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