From 63671ff9629efabcc38a7ad51c46c99cc466c6a3 Mon Sep 17 00:00:00 2001 From: Rotzbua Date: Thu, 1 Jan 2026 05:56:02 +0100 Subject: [PATCH] feat: improve VAPID config doc and type hints Clarified VAPID requirements. --- README.md | 27 ++++++++++++++++++++------- phpstan.neon | 1 + src/VAPID.php | 31 ++++++++++++++++++++++++------- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6fa969eb..31312f54 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,12 @@ $report = $webPush->sendOneNotification( ### Authentication (VAPID) -Browsers need to verify your identity. A standard called VAPID can authenticate you for all browsers. You'll need to create and provide a public and private key for your server. These keys must be safely stored and should not change. +Browsers need to verify your identity. A standard called VAPID can authenticate you for all browsers based on [RFC8292](https://www.rfc-editor.org/rfc/rfc8292). +You'll need to create and provide a public and private key for your server. These keys must be safely stored and should not change. + +According to the standard it is optional to provide contact details by the `subject` property. +In practice all browsers require a valid `subject` which can contain an email address or an available https website. It should not change. +Please note that browser manufacturers may use additional verification methods to prevent abuse of the push service. You can specify your authentication details when instantiating WebPush. The keys can be passed directly (recommended), or you can load a PEM file or its content: @@ -144,12 +149,18 @@ use Minishlink\WebPush\WebPush; $endpoint = 'https://fcm.googleapis.com/fcm/send/abcdef...'; // Chrome $auth = [ - 'VAPID' => [ - 'subject' => 'mailto:me@website.com', // can be a mailto: or your website address - 'publicKey' => '~88 chars', // (recommended) uncompressed public key P-256 encoded in Base64-URL - 'privateKey' => '~44 chars', // (recommended) in fact the secret multiplier of the private key encoded in Base64-URL - 'pemFile' => 'path/to/pem', // if you have a PEM file and can link to it on your filesystem - 'pem' => 'pemFileContent', // if you have a PEM file and want to hardcode its content + 'VAPID' => [ // Recommended. + 'subject' => 'mailto:me@website.com', // Must be an email beginning with mailto: or available https website address. + 'publicKey' => '~88 chars', // Uncompressed public key P-256 encoded in Base64-URL. + 'privateKey' => '~44 chars', // In fact the secret multiplier of the private key encoded in Base64-URL. + ], + 'VAPID' => [ // Alternative 1. + 'subject' => 'mailto:me@website.com', // Must be an email beginning with mailto: or available https website address. + 'pemFile' => 'path/to/pem', // If you have a PEM file and can link to it on your filesystem. + ], + 'VAPID' => [ // Alternative 2. + 'subject' => 'mailto:me@website.com', // Must be an email beginning with mailto: or available https website address. + 'pem' => 'pemFileContent', // If you have a PEM file and want to hardcode its content. ], ]; @@ -157,6 +168,8 @@ $webPush = new WebPush($auth); $webPush->queueNotification(...); ``` +#### Create VAPID keys + In order to generate the uncompressed public and secret key, encoded in Base64, enter the following in your Linux bash: ```bash diff --git a/phpstan.neon b/phpstan.neon index 2e9b252c..29ba628c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ parameters: paths: - src reportUnmatchedIgnoredErrors: false + treatPhpDocTypesAsCertain: false ignoreErrors: - identifier: missingType.iterableValue strictRules: diff --git a/src/VAPID.php b/src/VAPID.php index bbc527c7..f8409a40 100644 --- a/src/VAPID.php +++ b/src/VAPID.php @@ -18,12 +18,30 @@ use Jose\Component\Signature\JWSBuilder; use Jose\Component\Signature\Serializer\CompactSerializer; +/** + * @phpstan-type VapidConfig array{ + * subject: string, + * publicKey: string, // ~88 chars Base64URL + * privateKey: string // ~44 chars Base64URL + * } + * @phpstan-type VapidPemFileConfig array{ + * subject: string, + * pemFile: string // path/to/pem + * } + * @phpstan-type VapidPemConfig array{ + * subject: string, + * pem: string // PEM file content + * } + * @phpstan-type InputVapidConfig VapidConfig|VapidPemFileConfig|VapidPemConfig + */ class VAPID { private const PUBLIC_KEY_LENGTH = 65; private const PRIVATE_KEY_LENGTH = 32; /** + * @param InputVapidConfig $vapid + * @return VapidConfig * @throws \ErrorException */ public static function validate(array $vapid): array @@ -36,19 +54,19 @@ public static function validate(array $vapid): array $vapid['pem'] = file_get_contents($vapid['pemFile']); if (!$vapid['pem']) { - throw new \ErrorException('Error loading PEM file.'); + throw new \ErrorException('[VAPID] Error loading PEM file.'); } } if (isset($vapid['pem'])) { $jwk = JWKFactory::createFromKey($vapid['pem']); if ($jwk->get('kty') !== 'EC' || !$jwk->has('d') || !$jwk->has('x') || !$jwk->has('y')) { - throw new \ErrorException('Invalid PEM data.'); + throw new \ErrorException('[VAPID] Invalid PEM data.'); } $binaryPublicKey = hex2bin(Utils::serializePublicKeyFromJWK($jwk)); if (!$binaryPublicKey) { - throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); + throw new \ErrorException('[VAPID] Failed to convert VAPID public key from hexadecimal to binary.'); } $vapid['publicKey'] = base64_encode($binaryPublicKey); $vapid['privateKey'] = base64_encode(str_pad(Base64Url::decode($jwk->get('d')), self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); @@ -113,7 +131,6 @@ public static function getVapidHeaders( 'alg' => 'ES256', ]; - try { $jwtPayload = json_encode( [ @@ -161,7 +178,7 @@ public static function getVapidHeaders( } // @phpstan-ignore deadCode.unreachable - throw new \ErrorException('This content encoding is not supported'); + throw new \ErrorException('This content encoding is not supported.'); } /** @@ -176,12 +193,12 @@ public static function createVapidKeys(): array $binaryPublicKey = hex2bin(Utils::serializePublicKeyFromJWK($jwk)); if (!$binaryPublicKey) { - throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); + throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary.'); } $binaryPrivateKey = hex2bin(str_pad(bin2hex(Base64Url::decode($jwk->get('d'))), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); if (!$binaryPrivateKey) { - throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary'); + throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary.'); } return [