ウェブプッシュの相互運用性の成功事例

Matt Gaunt 氏
ジョー・メドレー
Joe Medley

Chrome が初めて Web Push API をサポートした当初は、Firebase Cloud Messaging(FCM)(旧称 Google Cloud Messaging(GCM))の push サービスを利用していました。これには独自の API を使用する必要がありました。これにより Chrome では、ウェブプッシュ プロトコル仕様の作成中であった時点で、ウェブプッシュ プロトコルに未実装の時点では認証(つまりメッセージの送信者本人が本人であることを証明する)を提供したデベロッパーも Web Push API を利用できるようになりました。幸いなことに、このどちらも当てはまりません。

FCM / GCM と Chrome では、標準のウェブプッシュ プロトコルがサポートされるようになりました。また、VAPID を実装することで送信者認証を実現できるため、ウェブアプリで「gcm_sender_id」が不要になります。

この記事では、まず既存のサーバーコードを、FCM でウェブプッシュ プロトコルを使用するように変換する方法について説明します。次にクライアントコードとサーバーコードで VAPID を実装する方法を説明します

FCM はウェブプッシュ プロトコルをサポートしています

少し背景情報から始めましょう。ウェブ アプリケーションが push サブスクリプションに登録されると、push サービスの URL が与えられます。サーバーはこのエンドポイントを使用して、ウェブアプリ経由でユーザーにデータを送信します。Chrome では、VAPID なしでユーザーを登録すると、FCM エンドポイントが提供されます。(VAPID については後ほど説明します)。FCM がウェブプッシュ プロトコルをサポートする前は、FCM API リクエストを実行する前に、URL の最後から FCM 登録 ID を抽出し、それをヘッダーに入れる必要がありました。たとえば、FCM エンドポイントが https://android.googleapis.com/gcm/send/ABCD1234 の場合、登録 ID は「ABCD1234」になります。

FCM でウェブプッシュ プロトコルがサポートされるようになったので、エンドポイントはそのままにして、URL をウェブプッシュ プロトコル エンドポイントとして使用できます。(Firefox や今後のブラウザのバージョンにも対応)

VAPID の説明に入る前に、サーバーコードが FCM エンドポイントを正しく処理していることを確認する必要があります。以下に、Node で push サービスにリクエストを送信する例を示します。FCM では、リクエスト ヘッダーに API キーを追加します。他の push サービス エンドポイントでは、これは必要ありません。また、バージョン 52 より前の Chrome、Opera Android、Samsung ブラウザの場合は、ウェブアプリの manifest.json に「gcm_sender_id」を含める必要もあります。API キーと送信者 ID は、リクエストを行うサーバーが受信側ユーザーへのメッセージの送信を実際に許可されているかどうかを確認するために使用されます。

const headers = new Headers();
// 12-hour notification time to live.
headers.append('TTL', 12 * 60 * 60);
// Assuming no data is going to be sent
headers.append('Content-Length', 0);

// Assuming you're not using VAPID (read on), this
// proprietary header is needed
if(subscription.endpoint
    .indexOf('https://android.googleapis.com/gcm/send/') === 0) {
    headers.append('Authorization', 'GCM_API_KEY');
}

fetch(subscription.endpoint, {
    method: 'POST',
    headers: headers
})
.then(response => {
    if (response.status !== 201) {
    throw new Error('Unable to send push message');
    }
});

これは FCM / GCM の API の変更であるため、サブスクリプションを更新する必要はありません。上記のようにサーバーコードを変更してヘッダーを定義するだけです。

サーバー識別のための VAPID のご紹介

VAPID は、「Voluntary Application Server Identification」の新しい略称です。この新しい仕様は基本的に、アプリサーバーと push サービス間の handshake を定義し、push サービスがメッセージを送信しているサイトを確認できるようにします。VAPID を使用すると、プッシュ メッセージを送信するための FCM 固有の手順が不要になります。Firebase プロジェクト、gcm_sender_idAuthorization ヘッダーは不要になりました。

手順はとても簡単です。

  1. アプリケーション サーバーが公開鍵/秘密鍵のペアを作成します。公開鍵はウェブアプリに渡されます。
  2. ユーザーがプッシュを受け取ることを選択したら、subscribe() 呼び出しのオプション オブジェクトに公開鍵を追加します。
  3. アプリサーバーがプッシュ メッセージを送信する場合は、署名付き JSON ウェブトークンと公開鍵を組み込みます。

これらの手順を詳しく見てみましょう。

公開鍵/秘密鍵のペアを作成する

暗号化は苦手なため、VAPID の公開鍵/秘密鍵の形式に関する仕様の関連セクションは次のとおりです。

アプリサーバーは、P-256 曲線で楕円曲線デジタル署名(ECDSA)で使用できる署名鍵ペアを生成し、維持すべきです。

その方法は web-push ノード ライブラリで確認できます。

function generateVAPIDKeys() {
    var curve = crypto.createECDH('prime256v1');
    curve.generateKeys();

    return {
    publicKey: curve.getPublicKey(),
    privateKey: curve.getPrivateKey(),
    };
}

公開鍵による登録

VAPID 公開鍵で Chrome ユーザーをプッシュ サブスクライブするには、subscribe() メソッドの applicationServerKey パラメータを使用して、公開鍵を Uint8Array として渡す必要があります。

const publicKey = new Uint8Array([0x4, 0x37, 0x77, 0xfe, …. ]);
serviceWorkerRegistration.pushManager.subscribe(
    {
    userVisibleOnly: true,
    applicationServerKey: publicKey
    }
);

正しく機能しているかどうかは、結果の購読オブジェクトのエンドポイントを調べることでわかります。オリジンが fcm.googleapis.com であれば、機能しています。

https://fcm.googleapis.com/fcm/send/ABCD1234

プッシュ メッセージの送信

VAPID を使用してメッセージを送信するには、Authorization ヘッダーと Crypto-Key ヘッダーの 2 つの追加 HTTP ヘッダーを使用して、通常のウェブプッシュ プロトコル リクエストを行う必要があります。

認証ヘッダー

Authorization ヘッダーは、先頭に「WebPush」が付いた署名付き JSON Web Token(JWT)です。

JWT は JSON オブジェクトをセカンド パーティと共有する方法です。これにより、送信側はオブジェクトに署名でき、受信側は想定される送信者からの署名であることを確認できます。JWT の構造は、暗号化された 3 つの文字列で、間に 1 つのドットで結合されています。

<JWTHeader>.<Payload>.<Signature>

JWT ヘッダー

JWT ヘッダーには、署名に使用されるアルゴリズム名とトークンの種類が含まれます。VAPID の場合、次のようにする必要があります。

{
    "typ": "JWT",
    "alg": "ES256"
}

これは base64 URL エンコードされ、JWT の最初の部分を形成します。

ペイロード

ペイロードは、以下を含むもう一つの JSON オブジェクトです。

  • オーディエンス(「aud」)
    • これは push サービスのオリジンです(サイトのオリジンではありません)。JavaScript でオーディエンスを取得するには、次のようにします。const audience = new URL(subscription.endpoint).origin
  • 有効期限(「exp」)
    • これは、リクエストを期限切れとみなすまでの秒数です。これは、リクエストが行われてから 24 時間以内(UTC)でなければなりません
  • 件名(サブ)
    • 件名は URL または mailto: の URL にする必要があります。これは、push サービスがメッセージ送信者に連絡する必要がある場合の連絡先になります。

ペイロードは次のようになります。

{
    "aud": "http://push-service.example.com",
    "exp": Math.floor((Date.now() / 1000) + (12 * 60 * 60)),
    "sub": "mailto: my-email@some-url.com"
}

この JSON オブジェクトは base64 URL エンコードされ、JWT の 2 番目の部分を形成します。

署名

署名は、エンコードされたヘッダーとペイロードをドットで結合し、前に作成した VAPID 秘密鍵を使用して結果を暗号化した結果です。結果自体は、ヘッダーにドットを付加する必要があります。

ヘッダーとペイロードの JSON オブジェクトを取得してこの署名を生成するライブラリは複数あるため、コードサンプルは示しません。

署名付き JWT は Authorization ヘッダーとして使用され、次のように「WebPush」が先頭に付加されます。

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

次の点に注目してください。まず、Authorization ヘッダーには文字どおり「WebPush」という単語が含まれ、その後にスペース、JWT が続く必要があります。また、JWT ヘッダー、ペイロード、署名を区切るドットもあります。

暗号鍵ヘッダー

Authorization ヘッダーに加えて、VAPID 公開鍵は、p256ecdsa= が先頭に付加された base64 URL エンコード文字列として Crypto-Key ヘッダーに追加する必要があります。

p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo

暗号化されたデータを含む通知を送信する場合、すでに Crypto-Key ヘッダーを使用しているため、アプリケーション サーバーキーを追加するには、上記の内容を追加する前にセミコロンを追加するだけで済みます。

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S_k7zi7A1coR_sVjHmGrlvzYpAT1n4NPbioFlQkIrTNL8EH4V3ZZ4vJE;
p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaN

これらの変更の現実

VAPID を使用すると、Chrome で push を使用するために GCM のアカウントに登録する必要がなくなります。また、Chrome と Firefox の両方で、ユーザーのサブスクライブとユーザーへのメッセージの送信に同じコードパスを使用できます。どちらも基準に従っています。

留意すべき点は、Chrome 51 以前の Android および Samsung ブラウザ向けの Opera では、ウェブアプリ マニフェストで gcm_sender_id を定義し、返される FCM エンドポイントに Authorization ヘッダーを追加する必要があります。

VAPID は、こうした独自の要件からの解放を実現します。VAPID を実装すると、ウェブプッシュをサポートするすべてのブラウザで機能します。VAPID をサポートするブラウザが増えているため、マニフェストから gcm_sender_id を削除するタイミングを決定できます。