Skip to content

Instantly share code, notes, and snippets.

@stampycode
Last active May 28, 2021 00:18
Show Gist options
  • Save stampycode/3c4734b7fab382a40f7e05edc2edd001 to your computer and use it in GitHub Desktop.
Save stampycode/3c4734b7fab382a40f7e05edc2edd001 to your computer and use it in GitHub Desktop.
Create a ServiceWorker Push notification in PHP
<?php
/*
* If you get any part of this process wrong, Google gives the really helpful error message "invalid JWT provided".
*
* Mozilla (Firefox) gives a slightly just-as-useful error:
* {
* "code": 401, "errno": 109, "error": "Unauthorized",
* "more_info": "http://autopush.readthedocs.io/en/latest/http.html#error-codes",
* "message": "Request did not validate Invalid Authorization Header"
* }
*/
// Generate the keys like this, although you can probably do it in PHP.
// `openssl ecparam -genkey -name prime256v1 -noout -out server-push-ecdh-p256.pem &>/dev/null`;
// `openssl ec -in server-push-ecdh-p256.pem -pubout -out server-push-ecdh-p256.pub &>/dev/null`;
$privk = file_get_contents('server-push-ecdh-p256.pem');
$pubk = file_get_contents('server-push-ecdh-p256.pub');
$endpoint = "https://fcm.googleapis.com/fcm/send/some-really-long-unique-secret-string-that-your-web-client-gets-on-push-subscribe";
$contact = "mailto:your-admin-email-address@example.com";
function base64web_encode($a) {
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($a));
}
function base64web_decode($a) {
return base64_decode(str_replace(['-', '_', ''], ['+', '/', '='], $a));
}
$asn = new phpseclib\File\ASN1();
$header = [
"typ" => "JWT",
"alg" => "ES256"
];
$claims = [
// just the https://hostname part
"aud" => substr($endpoint, 0, strpos($endpoint, '/', 10)),
// this push message will be discarded after 24 hours of non-delivery
"exp" => time() + 86400,
// who the server can talk to if our push script is causing problems
"sub" => $contact
];
/*
* Note these need to be base64 url-safe encoded, not standard base64.
* @see https://tools.ietf.org/html/rfc4648#section-5
*/
$strHeader = base64web_encode(json_encode($header));
$strPayload = base64web_encode(json_encode($claims));
$toSign = $strHeader . '.' . $strPayload;
$signature = '';
if (!openssl_sign($toSign, $signature, $privk, OPENSSL_ALGO_SHA256)) {
trigger_error('sign failed: '. openssl_error_string());
}
/*
* openssl_sign produces a signature which is the hash wrapped in an
* ASN.1 structure, so we need to extract the 256-bit raw hash manually.
* There's no PHP function to do this, so we use a library.
*/
$xx = $asn->decodeBER($signature);
/** @var \phpseclib\Math\BigInteger $a */
/** @var \phpseclib\Math\BigInteger $b */
$a = $xx[0]['content'][0]['content']; // 128-bits
$b = $xx[0]['content'][1]['content']; // 128-bits
$signature = $a->toBytes() . $b->toBytes();
$strSignature = base64web_encode($signature);
/*
* This is now a complete JWT object.
*/
$jwt = $strHeader . '.' . $strPayload . '.' . $strSignature;
/*
* Our PEM formatted public key is wrapped in an ASN.1 structure, so just
* like our signature above, lets extract
* the raw public key part, which is the bit we need.
*/
$xx = $pubk;
$xx = str_replace(['-----BEGIN PUBLIC KEY-----','-----END PUBLIC KEY-----',"\n"], '', $xx);
$xx = base64_decode($xx);
$xx = $asn->decodeBER($xx);
$xx = $xx[0]['content'][1]['content'];
$xx = substr($xx, 1); // need to strip the first char, which is not part of the key
$xx = base64web_encode($xx);
$pubkey = $xx;
/*
* We need to append the public key used for signing this JWT object, so
* the server can validate the JWT and compare the public key against the
* push-registration by the client, where we said which public key we would
* accept pushes from.
*/
$headers = [
"Authorization: vapid t=$jwt,k=$pubkey",
"Content-length: 0",
"Ttl: 86400",
];
/**
* Push!
*/
$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_POST, 1);
curl_exec($ch);
$ct = curl_multi_getcontent($ch);
echo curl_error($ch);
curl_close($ch);
echo $ct;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment