Notes from attempting to setup web push notifications

Files included

  • service-worker.js - Service worker file, it listens for push events and displays notifications
  • example.html - Main file, it will immediately ask for permission to send notifications and log the subscription object to the console. You might have to refresh after giving permissions to see the subscription object.

Steps to setup

Browser setup

  1. run npx serve
  2. Open the browser and go to http://localhost:3000
  3. Open the console and you should see the subscription object

Server setup

(Or you can use web-push package)

Generate VAPID keys

  1. Run openssl ecparam -name prime256v1 -genkey -noout -out private.pem
    1. You might also want to generate a PKCS8 key for the private key
      • Run openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem
  2. Run openssl ec -in private.pem -pubout -out public.pem

Now you can your favourite http request library to send a POST request to the url returned by the pushManager.subscribe method.

When you do you should include the following header (and no body!):

TTL: 60
Authorization: `Bearer ${jwtToken}`
Crypto-Key: `p256ecdsa=${publicKey}`
Topic: `An optional topic`
Urgency: `An optional urgency`

The jwtToken is a JWT token that you can generate using the private key you generated earlier. The payload should include the aud field which should be the url returned by the pushManager.subscribe method. For example:

const {default: fs} = await import("fs");
const {default: crypto} = await import("crypto");
const {default: jwt} = await import("jsonwebtoken");

const privateKeyBuffer = fs.readFileSync('private.pem');
const privateKey = crypto.createPrivateKey(privateKeyBuffer);

const payload = {
    aud: 'https://fcm.googleapis.com',
    sub: 'mailto:example@example.com',
};

const jwtToken = jwt.sign(payload, privateKey, {algorithm: 'ES256', expiresIn: '1h'});
console.log(jwtToken);

It has to use the ES256 algorithm and the exp field should be set to a timestamp in the future.

The publicKey is the public key you generated earlier however it should be encoded as JWK and then base64 url encoded. You can use the following code to do that:

const {default: fs} = await import("fs");
const {default: crypto} = await import("crypto");

const publicKeyBuffer = fs.readFileSync('public.pem');
const publicKey = crypto.createPublicKey(publicKeyBuffer);

const jwk = {
    kty: 'EC',
    crv: 'P-256',
    x: publicKey.export({format: 'jwk'}).x,
    y: publicKey.export({format: 'jwk'}).y,
};

const base64UrlEncode = (str) => {
    return str.toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
};

const base64UrlEncodedPublicKey = base64UrlEncode(publicKey.export({format: 'jwk'}).x) + '.' + base64UrlEncode(publicKey.export({format: 'jwk'}).y);

console.log(base64UrlEncodedPublicKey);

This should be enough to get you started with web push notifications.

Notes

  • The pushManager.subscribe method may require depending on the browser a userVisibleOnly field or applicationServerKey field. The userVisibleOnly field should be set to true and the applicationServerKey field should be set to the public key you generated earlier.

  • For sending a payload additional changes need to happen, you can't just send a payload with the request.

    • For it to work you will have to update the headers:
      • Encoding the payload, it's complex and I haven't done it yet but you can look at the Mozilla Blog for more information.
      • The Crypto-Key header should be set to 3 values:
        • p256ecdsa=${publicKey} - The public key you generated earlier
        • dh=${publicHalfKey} - The first half of the shared secret when the payload is encoded
        • keyid=p256dh - The key id
      • The Encryption header should be set to 2 values:
        • salt=${salt} - The salt used to generate the shared secret
        • keyid=p256dh - The key id
      • The Content-Encoding header should be set to aesgcm

For more information you can check the useful links below.