skip to Main Content

I cannot seem to get push notifications to work whatever I try…

This is the error code.
{"code":401,"errno":109,"error":"Unauthorized","message":"InvalidSignature","more_info":"http://autopush.readthedocs.io/en/latest/http.html#error-codes"}

It appears that the issue has something to do with ether a key mismatch or invalid signature.

Here are some of the resources I was using:
https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/

https://autopush.readthedocs.io/en/latest/http.html#error-codes

https://datatracker.ietf.org/doc/rfc8292/

I’m generating the public/private keys as so:

    function base64url_encode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    function generateVapidKeys(){
        if(file_exists('vapid.json')){
            $vapidKeys = json_decode(file_get_contents('vapid.json'));
            return base64url_encode(hex2bin('04'.$vapidKeys->x.$vapidKeys->y));

        }else{
            $keyPair = openssl_pkey_new([
                'digest_alg' => 'sha256',
                'private_key_type' => OPENSSL_KEYTYPE_EC,
                'curve_name' => 'prime256v1', // P-256 curve
            ]);
        }

        $privateKeyDetails = openssl_pkey_get_details($keyPair);

        $x = str_pad(bin2hex($privateKeyDetails['ec']['x']), 64, '0', STR_PAD_LEFT);
        $y = str_pad(bin2hex($privateKeyDetails['ec']['y']), 64, '0', STR_PAD_LEFT);
        $d = str_pad(bin2hex($privateKeyDetails['ec']['d']), 64, '0', STR_PAD_LEFT);

        file_put_contents('vapid.json', json_encode([
            'x' => $x,
            'y' => $y,
            'd' => $d,
        ], JSON_PRETTY_PRINT));

        return base64url_encode(hex2bin('04'.$x.$y));
    }

    $publicKey = generateVapidKeys();

And finally here is my Notification send:

<?php
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    error_reporting(E_ALL);

    header('Content-Type: application/json; charset=utf-8');

    function generate_jwt($headers, $payload, $privateKey){
        $headers_encoded = base64url_encode(json_encode($headers));
        $payload_encoded = base64url_encode(json_encode($payload));
        
        //$signature = hash_hmac('SHA256', "$headers_encoded.$payload_encoded", $secret, true);
        openssl_sign("$headers_encoded.$payload_encoded", $signature, $privateKey, OPENSSL_ALGO_SHA256);
        $signature_encoded = base64url_encode($signature);
        
        return "$headers_encoded.$payload_encoded.$signature_encoded";
    }

    function is_jwt_valid($jwt, $publicKey){
        $tokenParts = explode('.', $jwt);
    
        // check the expiration time - note this will cause an error if there is no 'exp' claim in the jwt
        $expires = json_decode(base64_decode($tokenParts[1]))->exp < time();//($expires - time()) < 0;

        $signature = openssl_verify($tokenParts[0].'.'.$tokenParts[1], base64_decode($tokenParts[2]), $publicKey, OPENSSL_ALGO_SHA256);
        
        if($expires || !$signature){
            return false;
        }
        return true;
    }


    function generateVapidToken($url, $privateKey) {
    
        $expiration = time() + (12 * 60 * 60);  // 12 hours
    
        $header = [
            'alg' => 'ES256',
            'typ' => 'JWT',
        ];

        $body = [
            'aud' => $url,
            'exp' => $expiration,
            'sub' => 'mailto:[email protected]',
        ];

        return generate_jwt($header, $body, $privateKey);
    }
    
    function base64url_encode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }


    // Assuming you have a database connection established

    // Function to send a push notification
    function sendPushNotification($subscription, $payload)
    {

        $parse = parse_url($subscription->endpoint);
        $url = $parse['scheme'].'://'.$parse['host'];//.pathinfo(parse_url($parse['path'], PHP_URL_PATH))['dirname'];
        echo $url.PHP_EOL.PHP_EOL;

        $vapidKeys = json_decode(file_get_contents('vapid.json'));

        //print_r(json_encode($vapidKeys, JSON_PRETTY_PRINT));


        $keyPair = openssl_pkey_new([
            'ec' => [
                'digest_alg' => 'sha256',
                'private_key_type' => OPENSSL_KEYTYPE_EC,
                'curve_name' => 'prime256v1', // P-256 curve
                'x' => hex2bin($vapidKeys->x),
                'y' => hex2bin($vapidKeys->y),
                'd' => hex2bin($vapidKeys->d)
            ]
        ]);

        $privateKeyDetails = openssl_pkey_get_details($keyPair);

        openssl_pkey_export($keyPair, $privateKey);
        $token = generateVapidToken($url, $privateKey);
        //openssl_sign('HELLO WORLD', $signature, $privateKey, OPENSSL_ALGO_SHA256);
        echo $token;

        echo PHP_EOL;
        echo PHP_EOL;

        $publicKey = openssl_pkey_get_public($privateKeyDetails['key']);
        $verified = is_jwt_valid($token, $publicKey);
        //$verified = openssl_verify('HELLO WORLD', $signature, $publicKey, OPENSSL_ALGO_SHA256);


        echo 'Token Valid: '.(($verified) ? "TRUE" : "FALSE");
        echo PHP_EOL;
        echo PHP_EOL;


        $publicKey = base64url_encode(hex2bin('04'.$vapidKeys->x.$vapidKeys->y));
        echo $publicKey;


        echo PHP_EOL;
        echo PHP_EOL;

        $headers = [
            //'Authorization: WebPush '.$token,
            'Authorization: vapid t='.$token.',k='.$publicKey,
            //'Authorization: key=' . $subscription->keys->auth,
            //'Crypto-Key: p256ecdsa='.$publicKey.';dh='.$subscription->keys->auth,//$subscription->keys->p256dh,
            'Content-Type: application/json',
        ];

        /*
        $notification = [
            'title' => 'Your Notification Title',
            'body' => 'Your Notification Body',
            'icon' => 'path/to/icon.png',
        ];
        */

        $data = [
            'notification' => $payload,
            //'applicationServerKey' => $vapidKeys->publicKey
        ];

        $options = [
            CURLOPT_URL => $subscription->endpoint,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_RETURNTRANSFER => true,
        ];

        $ch = curl_init();
        curl_setopt_array($ch, $options);
        $result = curl_exec($ch);
    
        if ($result === false) {
            echo 'Error: ' . curl_error($ch) . PHP_EOL;
        } else {
            echo 'Push notification sent successfully!' . PHP_EOL;
        }


        print_r($result);
    }

    // Example payload
    $notificationPayload = [
        'title' => 'New Notification',
        'body' => 'This is the body of the notification.',
        'icon' => 'icon.png'
    ];

    if(file_exists('endpoints.json')){
        $subscriptions = json_decode(file_get_contents('endpoints.json'));

        // Send push notifications to all stored subscriptions
        foreach ($subscriptions as $subscription) {
            sendPushNotification($subscription, $notificationPayload);
        }
    }

?>

2

Answers


  1. Chosen as BEST ANSWER

    The answer for this specific issue was the way I was generating the JWT.I needed to modify the signature differently.

        function generate($headers, $payload, $privateKey){
            $message = self::base64url_encode(json_encode($headers)).'.'.self::base64url_encode(json_encode($payload));
            openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256);
    
            $components = [];
            $pos = 0;
            $size = strlen($signature);
            while ($pos < $size) {
                $constructed = (ord($signature[$pos]) >> 5) & 0x01;
                $type = ord($signature[$pos++]) & 0x1f;
                $len = ord($signature[$pos++]);
                if ($len & 0x80) {
                    $n = $len & 0x1f;
                    $len = 0;
                    while ($n-- && $pos < $size) $len = ($len << 8) | ord($signature[$pos++]);
                }
        
                if ($type == 0x03) {
                    $pos++;
                    $components[] = substr($signature, $pos, $len - 1);
                    $pos += $len - 1;
                } else if (! $constructed) {
                    $components[] = substr($signature, $pos, $len);
                    $pos += $len;
                }
            }
            foreach ($components as &$c) $c = str_pad(ltrim($c, "x00"), 32, "x00", STR_PAD_LEFT);
        
            return $message . '.' . self::base64url_encode(implode('', $components));
            
            //return "$headers_encoded.$payload_encoded.$signature_encoded";
        }
    

  2. Your procedure is wrong.

    1. keypair (vapid) generate only once but you generate every time when send notification.
    2. I don’t see your code for get subscription so i guess you mock manually. we can’t do that because we can get subscription from browser only (by javascript).

    To make push API working we should.

    1. generate keypair save private key to safe place on server. pubic key is require for create subscription in browser you can save them in javascript code or any method.
    2. create subscription in browser/user agent (it’s done by end user) and send to server save them to DB or any storage.
    3. send notification create http request contain notification payload (payload might different for each browser/user agent) follow rfc standard and send them to endpoint in subscription.

    Here some resources

    Don’t confuse between Push API and Notification API.
    It’s different API. We could Push but don’t need to use pushed data in Notification API. So you could push any data from server and convert them to Notification on browser.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search