※ 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の全体の流れを説明します。
- アプリケーションサーバはECDSAの鍵ペアを生成する。
- ブラウザ(UA; User Agent)はアプリケーションサーバからECDSA公開鍵を受け取る。
- ブラウザがプッシュサーバにプッシュ通知の購読を登録する際、アプリケーションサーバのECDSA公開鍵を渡す。
- アプリケーションサーバがプッシュサーバにプッシュ通知を送信する際、ECDSA秘密鍵を使ってSHA-256 with ECDSAによってJSON Web Token (JWT)形式の署名トークンを生成し、ECDSA公開鍵と合わせてプッシュサーバに渡す。
- プッシュサーバは、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)
-
{"typ":"JWT","alg":"ES256"}
{"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)
を記述
POST /endpoint HTTP/1.1
Host: pushserver.example.com
Content-Encoding: aes128gcm
TTL: 86400
Authorization: vapid t=... k=...
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)
を記述
POST /endpoint HTTP/1.1
Host: pushserver.example.com
Content-Encoding: aes128gcm
Crypto-Key: p256ecdsa=...
TTL: 86400
Authorization: WebPush ...
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の署名と検証を試してみるを参照して下さい。