Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

※ 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の署名と検証を試してみるを参照して下さい。

tomoyukilabs
Qiitaでは今のところ、主にWeb標準関連の記事を書いております。
https://github.com/tomoyukilabs
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away