Sending Messages

There are actually two servers involved in sending a message: your server and a third party messaging server. You keep track of two things: recipients and recipient-specific endpoints on the messaging server. The messaging server handles the routing.

The broader context

We've seen how to subscribe to push within a web application. That process included passing in a public key, called the applicationServerKey, into the subscription API.

The following diagram shows the order of operations.

Sending a message

  1. A device downloads your web app containing an already created publicKey, referred to in scripts as the applicationServerKey. Your web app installs a service worker.
  2. During the subscription flow the browser contacts the messaging server to create a new subscription and returns it to the app.

  3. After the subscription flow, your app passes a subscription object back to your app server.

  4. At some later point, your app server sends a message to the messaging server, which forwards it to the recipient.

Generating the applicationServerKey

There are several things you need to know about the applicationServerKey:

  • It's the public key portion of a public/private key pair generated on your application server.
  • The key pair should be usable with an elliptic curve digital signature (ECDSA) over the P-256 curve.
  • Your app should pass the public key to the messaging server as an array of eight-bit unsigned integers.
  • It's defined in a spec called Voluntary Application Server Identification for Web Push (VAPID), which we'll discuss in the sending messages section.

You can find an example for generating this in the web-push node library. It looks something like this:

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

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

Anatomy of a subscription object

Earlier we said that a subscription object must be stringified and passed to the server, but we didn't tell you what was in the subscription object. That's because the client doesn't do anything with it. The server does.

The subscription object looks like this:

{  
  "endpoint": "https://example.com/push-service/send/dbDqU8xX10w:APA91b...",  
  "keys": {  
    "auth": "qLAYRzG9TnUwbprns6H2Ew==",  
    "p256dh": "BILXd-c1-zuEQYXH\\_tc3qmLq52cggfqqTr\\_ZclwqYl6A7-RX2J0NG3icsw..."  
  }  
}

What's in this?

endpoint—Contains two parts: the URL of the messaging service used by the subscribing browser followed by unique identifier for the user.

keys—Encryption keys used for encrypting data passed to the service worker messages. It contains the following:

  • auth-A 16 byte authentication secret generated by the browser.
  • p256dh-65 bytes containing a public key from the browser that developers need to use when encrypting messages they want to send to that push service.

Creating the message

This is where things start to get a little crazy. In this section we're no longer in your client app. We're on the app server where we are going to create and send a message to the client. There's much to keep track of.

Before continuing, let's review what we have and where it came from.

  • Subscription object - This came from the client. It contains the endpoint of the messaging server, a copy of the public key, and an authentication secret generated by the client. From here on, we're going to stop talking about the subscription object and only refer to the endpoint, public key, and authentication secret.
  • private key - The VAPID private key corresponding to the VAPID public key. This is a private key for your application server.

We're going to look at message creation in three parts. First we'll create some HTTP headers, then we'll create a payload for the message, and finally we'll combine them and send them to the messaging server.

A note about code samples

The code samples given in this section, which are taken from the web-push node library.

The product

Let's look at what we're going to end up with, then we'll talk about how to build it.

POST /push-service/send/dbDqU8xX10w:APA91b... HTTP/1.1  
Host: push.example.net  
Push-Receipt: https://push.example.net/r/3ZtI4YVNBnUUZhuoChl6omU  
TTL: 43200  
Content-Type: text/plain;charset=utf8  
Content-Length: 36  
Authorization: WebPush
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0.i3CYb7t4xfxCDquptFOepC9GAu\_HLGkMlMuCGSK2rpiUfnK9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA  
Crypto-Key:
p256ecdsa=BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU\_RCPCfA5aq9ojSwk5Y2EmClBPsiChYuI3jMzt3ir20P8r\_jgRR-dSuN182x7iB

Notice that this request is sent to the endpoint contained in the subscription object. The Authorization, Crypto-Key , and TTL headers take a little explanation. Let's start with the simplest.

HTTP headers

TTL

It may be some time before a message server can deliver a message sent by your app server. Message services aren't obligated to hold a message forever. Frankly, to be timely, an app server should never send a message that can live forever. That's why it's required to include a header called TTL, literally 'time to live'.

The TTL header is a a value in seconds given as a suggestion to the message server for how long the server should keep the message and attempt to deliver it. If it chooses, the message server may shorten the time it is willing to keep the message. If it does, it must return that shortened time in a TTL header in a response to the message request. If the TTL has a value of 0, the message server should deliver it immediately if the user agent is available. If the user agent isn't available the message expires immediately and is never delivered.

Crypto-Key header

To validate a message sent by your app server, the message server needs the public key. You send the public key in the Crypto-Key header. The Crypto-Key header has several parts.

dh=publicKey,p256ecdsa=applicationServerKey

For example:

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S\_k7zi7A1coR\_sVjHmGrlvzYpAT1n4NPbioFlQkIrT  
NL8EH4V3ZZ4vJE,p256ecdsa=BDd3\_hVL9fZi9Ybo2UUzA284WG5FZR30\_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6\_LFTeZaN

The first part (dh=publicKey) is the public key, the one we created under Requesting permission and subscribing users. The second part (p256ecdsa=applicationServerKey) is the public key created by your app server. Both must be base64 url encoded. Notice the comma separating the two parts of the Crypto-Key.

Authorization header

To send messages, you need an an Authorization header. It contains four parts:

WebPush <JWTHeader>.<Payload>.<Signature>

The word WebPush is a literal that must be followed by a space. The remaining portions, which are encrypted and concatenated using a period form a signed JSON web token (JWT). A JWT is a way of sharing a JSON object with a second party in such a way that the sending party can sign it and the receiving party can verify the signature is from the expected sender.

Let's look at each part of the token in detail.

JWT Header

The JWT Header contains two standard pieces of information: a typ property to indicate that the type of message, in this case, a JWT message, and an alg property to indicate the algorithm used for signing the the message. These details should then be base64 url encoded.

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

JWT Payload

The JWT calls this section the payload. This isn't where the message payload is stored. We'll get to that shortly. The payload is another JSON object with the following members:
aud
This contains the origin of the push service endpoint, which you should extract from the subscription object. This is not the origin of your site.
exp
Specifies the time the JWT request expires in milliseconds (not the expiration of the message itself). It must be within twenty-four hours. This can be calculated by converting the current date to milliseconds and adding the duration. For example, in Node.js you could do this:

Math.floor((Date.now() / 1000) + 12 * 60 * 60)

sub
Specifies a subject, which the VAPID spec defines as a way for the push service to contact a message sender. This can be a URL or a mailto address (see the example below).

A complete JWT payload looks like this:

{  
  "aud": "http://push.example.net",  
  "exp": "1469618703",  
  "sub": "mailto: my-email@some-url.com"  
}

Signature

The signature is the last section of the JWT.

WebPush <JWTHeader>.<Payload>.<Signature>

Create the signature by first concatenating the JWT header and the payload with a dot. For example:

<JWTHeader>.<Payload>

Encrypt this using the private key you created in Generating the applicationServerKey.

You now have all three pieces that form the JWT which you join to together with a dot.

<JWTHeader>.<Payload>.<Signature>

We're not going to show you how to do the signature encryption, but there are a number of libraries available. A good place to look is the libraries section of jwt.io.

Finally, prepend the word 'WebPush' followed by a space. The result will look something like the following:

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

Message payload

When implementing server code there are two methods of sending messages to consider.

  • Messages with a data payload.
  • Messages without a data payload, often called a tickle.

In the case of the tickle, the service worker uses the message as a signal to fetch data from an endpoint. The Handling Messages section contains sample code for showing how a service worker does this.

Why would you ever send a message without a payload? There are two reasons.

  • You need to send something larger than the 4k payload limit set by the spec
  • The clients needs fresher data than what would be in the push.

Technically, there's another reason which is that browser capabilities may vary for a while yet, but the two main reasons will likely always apply. If the browser doesn't support payloads, the subscription object won't contain keys.

The payload, regardless of how you get it to the client, must be encrypted. Encryption is enough of a specialty, even within software development, that we don't recommend writing your own encryption system. Fortunately, there are a range of push libraries available.

For payloads sent through the message server you must encrypt it using the publicKey and the authentication secret. It must also be salted with 16 random bytes that are unique to the message. Finally, it's added to the body of the request sent to the message server.

Sending it on its way

In the web-push node library this is accomplished with an instance of the request object from the built-in https library.

const https = require('https');

At some point the request is sent to the message server. The web-push node library wraps this code inside a promise (with appropriate calls to resolve and reject) so that it can happen asynchronously. The code example below, taken from the web-push node library illustrates this.

Note that the message server responds to the network request immediately, meaning it is asynchronous to sending the message to the client app.

const pushRequest = https.request(options, function(pushResponse) {  
  let body = '';    
  // Allow the payload to be sent out in chunks.  
  pushResponse.on('data', function(chunk) {  
    body += chunk;  
  });    
  // Check to see if the push is successful.  
  pushResponse.on('end', function() {  
    if (pushResponse.statusCode !== 201) {  
      reject(new WebPushError('Received unexpected response code',  
        pushResponse.statusCode, pushResponse.headers, body));  
    } else {  
      // Do something with the response body.  
    }  
 });  
});

if (requestPayload) {  
  pushRequest.write(requestPayload);  
}

pushRequest.end();

pushRequest.on('error', function(e) {  
  console.error(e);  
});