Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web Pushnotification 'UnauthorizedRegistration' or 'Gone' or 'Unauthorized'- subscription expires

I have developed a push notification service for my web site. the service worker is:

    'use strict';
    self.addEventListener('push', function (event) {    
    var msg = {};
    if (event.data) {
        msg = event.data.json();
    }    
    let notificationTitle = msg.title;
    const notificationOptions = {
        body: msg.body,//body
        dir:'rtl',//direction
        icon: msg.icon,//image
        data: {
            url: msg.url,//click
        },
    };
    event.waitUntil(
      Promise.all([
        self.registration.showNotification(
          notificationTitle, notificationOptions),      
      ])
    );
    });
    self.addEventListener('notificationclick', function (event) {    
        event.notification.close();

    let clickResponsePromise = Promise.resolve();
    if (event.notification.data && event.notification.data.url) {
        clickResponsePromise = clients.openWindow(event.notification.data.url);
    }
    const fetchOptions = 
        { method: 'post'}; 
    fetch('http://localhost:5333/usrh.ashx?click=true', fetchOptions).
    then(function (response) 
    {
        if (response.status >= 400 && response.status < 500) 
        {         
            throw new Error('Failed to send push message via web push protocol');
        } 
    }).catch((err) => 
    { 
        this.showErrorMessage('Ooops Unable to Send a Click', err); 
    });
});

self.addEventListener('notificationclose', function (event) {
    const fetchOptions = 
        { method: 'post'}; 
    fetch('http://localhost:5333/usrh.ashx?close=true', fetchOptions).
    then(function (response) 
    {
        if (response.status >= 400 && response.status < 500) 
        {         
            throw new Error('Failed to send push message via web push protocol');
        } 
    }).catch((err) => 
    { 
        this.showErrorMessage('Ooops Unable to Send a Click', err); 
    }); 
});
self.addEventListener('pushsubscriptionchange', function () {
    const fetchOptions = {
        method: 'post'
        ,
    };

    fetch('http://localhost:5333/usru.ashx', fetchOptions)
        .then(function (response) {
            if (response.status >= 400 && response.status < 500) {
                console.log('Failed web push response: ', response, response.status);
                throw new Error('Failed to update users.');
            }
        })
        .catch((err) => {
            this.showErrorMessage('Ooops Unable to Send a user', err);
        });
});

I have subscribed the users successfully using the following code:

registerServiceWorker() {
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('http://localhost:5333/service-worker.js')
                .catch((err) => {
                    this.showErrorMessage('Unable to Register SW', 'Sorry this demo requires a service worker to work and it ' + 'failed to install - sorry :(');
                    console.error(err);
                });
        } else {
            this.showErrorMessage('Service Worker Not Supported', 'Sorry this demo requires service worker support in your browser. ' +
                'Please try this demo in Chrome or Firefox Nightly.');
        }
    }

and

class PushClient {
    constructor(subscriptionUpdate, appkeys) {
        this._subscriptionUpdate = subscriptionUpdate;
        this._publicApplicationKey = appkeys;
        if (!('serviceWorker' in navigator)) {
            return;
        }
        if (!('PushManager' in window)) {
            return;
        }
        if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
            return;
        }
        navigator.serviceWorker.ready.then(() => {
            this.setUpPushPermission();
        });
    }
    setUpPushPermission() {
        return navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
            return serviceWorkerRegistration.pushManager.getSubscription();
        })
            .then((subscription) => {
                if (!subscription) {
                    return;
                }
                this._subscriptionUpdate(subscription);
            })
            .catch((err) => {
                console.log('setUpPushPermission() ', err);
            });
    }
    subscribeDevice() {
        return new Promise((resolve, reject) => {
            if (Notification.permission === 'denied') {
                sc(3);
                return reject(new Error('Push messages are blocked.'));
            }
            if (Notification.permission === 'granted') {
                sc(3);
                return resolve();
            }
            if (Notification.permission === 'default') {
                Notification.requestPermission((result) => {
                    if (result === 'denied') {
                        sc(0);
                    } else if (result === 'granted') {
                        sc(1);
                    } else {
                        sc(2);
                    }
                    if (result !== 'granted') {
                        reject(new Error('Bad permission result'));
                    }
                    resolve();
                });
            }
        })
            .then(() => {
                return navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
                    return serviceWorkerRegistration.pushManager.subscribe({
                        userVisibleOnly: true
                        , applicationServerKey: this._publicApplicationKey.publicKey
                    ,
                    });
                })
                    .then((subscription) => {
                        this._subscriptionUpdate(subscription);
                        if (subscription) {
                            this.sendPushMessage(subscription);
                        }
                    })
                    .catch((subscriptionErr) => { });
            })
            .catch(() => { });
    }
    toBase64(arrayBuffer, start, end) {
        start = start || 0;
        end = end || arrayBuffer.byteLength;
        const partialBuffer = new Uint8Array(arrayBuffer.slice(start, end));
        return btoa(String.fromCharCode.apply(null, partialBuffer));
    }
    unsubscribeDevice() {
        navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
            return serviceWorkerRegistration.pushManager.getSubscription();
        })
            .then((pushSubscription) => {
                if (!pushSubscription) {
                    this._subscriptionUpdate(null);
                    return;
                }
                return pushSubscription.unsubscribe()
                    .then(function (successful) {
                        if (!successful) {
                            console.error('We were unable to unregister from push');
                        }
                    });
            })
            .then(() => {
                this._subscriptionUpdate(null);
            })
            .catch((err) => {
                console.error('Error thrown while revoking push notifications. ' + 'Most likely because push was never registered', err);
            });
    }
    sendPushMessage(subscription) {
        let payloadPromise = Promise.resolve(null);
        payloadPromise = JSON.parse(JSON.stringify(subscription));
        const vapidPromise = EncryptionHelperFactory.createVapidAuthHeader(this._publicApplicationKey, subscription.endpoint, 'http://localhost:5333/');
        return Promise.all([payloadPromise, vapidPromise, ])
            .then((results) => {
                const payload = results[0];
                const vapidHeaders = results[1];
                let infoFunction = this.getWebPushInfo;
                infoFunction = () => {
                    return this.getWebPushInfo(subscription, payload, vapidHeaders);
                };
                const requestInfo = infoFunction();
                this.sendRequestToProxyServer(requestInfo);
            });
    }
    getWebPushInfo(subscription, payload, vapidHeaders) {
        let body = null;
        const headers = {};
        headers.TTL = 60;
        if (payload) {
            headers.Encryption = `auth=${payload.keys.auth}`;
            headers['Crypto-Key'] = `p256dh=${payload.keys.p256dh}`;
            headers['Content-Encoding'] = 'aesgcm';
        } else {
            headers['Content-Length'] = 0;
        }
        if (vapidHeaders) {
            headers.Authorization = `WebPush ${vapidHeaders.authorization}`;
            if (headers['Crypto-Key']) {
                headers['Crypto-Key'] = `${headers['Crypto-Key']}; ` + `p256ecdsa=${vapidHeaders.p256ecdsa}`;
            } else {
                headers['Crypto-Key'] = `p256ecdsa=${vapidHeaders.p256ecdsa}`;
            }
        }
        const response = {
            headers: headers
            , endpoint: subscription.endpoint
        ,
        };
        if (body) {
            response.body = body;
        }
        return response;
    }
    sendRequestToProxyServer(requestInfo) {
        const fetchOptions = {
            method: 'post'
        ,
        };
        if (requestInfo.body && requestInfo.body instanceof ArrayBuffer) {
            requestInfo.body = this.toBase64(requestInfo.body);
            fetchOptions.body = requestInfo;
        }
        fetchOptions.body = JSON.stringify(requestInfo);
        fetch('http://localhost:5333/usrh.ashx', fetchOptions)
            .then(function (response) {
                if (response.status >= 400 && response.status < 500) {
                    console.log('Failed web push response: ', response, response.status);
                    throw new Error('Failed to send push message via web push protocol');
                }
            })
            .catch((err) => {
                this.showErrorMessage('Ooops Unable to Send a Push', err);
            });
    }
}

All these codes are in javascript. I can successfully recieve user subscription infromarion on my server like:

Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwcxxxxx  
Crypto-Key: p256dh=BBp90dwDWxxxxc1TfdBjFPqxxxxxwjO9fCip-K_Eebmg=; p256ecdsa=BDd3_hVL9fZi9Yboxxxxxxo
endpoint: https://fcm.googleapis.com/fcm/send/cxxxxxxxxxxxxxxJRorOMHKLQ3gtT7
Encryption: auth=9PzQZ1mut99qxxxxxxxxxxyw== 
Content-Encoding: aesgcm

Also I can successfully send a push to this user using the code bellow in C#:

public static async Task<bool> SendNotificationByte(string endpoint, string[] Keys, byte[] userSecret, byte[] data = null,
                                        int ttl = 0, ushort padding = 0, bool randomisePadding = false, string auth="")
        {
            #region send
            HttpRequestMessage Request = new HttpRequestMessage(HttpMethod.Post, endpoint);                
                Request.Headers.TryAddWithoutValidation("Authorization", auth);
            Request.Headers.Add("TTL", ttl.ToString());
            if (data != null && Keys[1] != null && userSecret != null)
            {
                EncryptionResult Package = EncryptMessage(Decode(Keys[1]), userSecret, data, padding, randomisePadding);
                Request.Content = new ByteArrayContent(Package.Payload);
                Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                Request.Content.Headers.ContentLength = Package.Payload.Length;
                Request.Content.Headers.ContentEncoding.Add("aesgcm");
                Request.Headers.Add("Crypto-Key", "dh=" + Encode(Package.PublicKey)+" ;"+Keys[2]+"="+Keys[3]);
                Request.Headers.Add("Encryption", "salt=" + Encode(Package.Salt));
            }
            using (HttpClient HC = new HttpClient())
            {
                HttpResponseMessage res = await HC.SendAsync(Request).ConfigureAwait(false);
                if (res.StatusCode == HttpStatusCode.Created)
                    return true;
                else return false;
            }
            #endregion
        }

The problem is that after a period of time (about 20 hours or even less), when I want to send a push to this user I got the following errors:

firefox subscription:

{StatusCode: 410, ReasonPhrase: 'Gone', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Access-Control-Allow-Headers: content-encoding,encryption,crypto-key,ttl,encryption-key,content-type,authorization
  Access-Control-Allow-Methods: POST
  Access-Control-Allow-Origin: *
  Access-Control-Expose-Headers: location,www-authenticate
  Connection: keep-alive
  Cache-Control: max-age=86400
  Date: Tue, 21 Feb 2017 08:19:03 GMT
  Server: nginx
  Content-Length: 179
  Content-Type: application/json
}}

chrome subscription:

{StatusCode: 400, ReasonPhrase: 'UnauthorizedRegistration', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  X-Content-Type-Options: nosniff
  X-Frame-Options: SAMEORIGIN
  X-XSS-Protection: 1; mode=block
  Alt-Svc: quic=":443"; ma=2592000; v="35,34"
  Vary: Accept-Encoding
  Transfer-Encoding: chunked
  Accept-Ranges: none
  Cache-Control: max-age=0, private
  Date: Tue, 21 Feb 2017 08:18:35 GMT
  Server: GSE
  Content-Type: text/html; charset=UTF-8
  Expires: Tue, 21 Feb 2017 08:18:35 GMT
}}

I think I missed something, that makes the subscription expires, or have to make the users to resubscribe when their subscription information is changed or expired, but I do not know how?!!

like image 566
Alireza Mahmoudi Avatar asked Feb 21 '17 08:02

Alireza Mahmoudi


2 Answers

The Problem is solved by sending a push echo notification to the subscribed users to resubscribe them. I have wrote a job in which I send a push echo periodically and resubscribe the users and update their information.

To do so I send an special message called "push echo" using the code bellow:

self.addEventListener('push', function (event) {
lastEventName = 'push';
var msg = {};
if (event.data) {
    msg = event.data.json();
    if (!!msg.isEcho) {
        self.registration.pushManager.getSubscription()
            .then(function (subscription) {
                if (!subscription) {
                } else {
                    subscription.unsubscribe().then(function () {
                        self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array('xxxxxxxxxxxxxxxx') })
                            .then(function (subscription) {
                                resubscription(subscription);
                            });
                    });
                    }
            });
        return;
    }
}
if (!!msg.isEcho)
    return;
let notificationTitle = msg.title;
const notificationOptions = {
    body: msg.body,
    dir: 'rtl',
    icon: msg.icon,
    data: {
        url: msg.url,
        id: msg.id,
        key: msg.key
    },
};
event.waitUntil(
  Promise.all([
    self.registration.showNotification(
      notificationTitle, notificationOptions),
  ])
);

const fetchOptions =
    { method: 'post', mode: 'no-cors' };
fetch('http://example.com', fetchOptions).
    then(function (response) {
        if (response.status >= 400 && response.status < 500) {
            throw new Error('Failed to send push message via web push protocol');
        }
        lastEventName = 'view';
    }).catch((err) => {
        this.showErrorMessage('Ooops Unable to Send a Click', err);
    });
});

In the resubscription method you can unsubscribe and then subscribe the user and update server data.

like image 78
amirpaia Avatar answered Nov 10 '22 05:11

amirpaia


I think the issue is about how you send your applicationServerKey. I just have done an example of what you want to do and I had to send that key encoded with this function:

  function urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/\-/g, '+')
      .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }

so you have to create your subscription object in this way :

  registration.pushManager
    .subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(this._publicApplicationKey.publicKey),
    })

Mainly what I did was follow this tutorial. I have that working example in this github repo. The README file is in Spanish, but I think it can help you.

like image 2
gabrielperales Avatar answered Nov 10 '22 04:11

gabrielperales