Nous avons vu comment une bibliothèque peut être utilisée pour déclencher des messages push, mais que font exactement ces bibliothèques ?
Eh bien, ils effectuent des requêtes réseau tout en s'assurant que ces requêtes sont au bon format. La spécification qui définit cette requête réseau est le protocole Web push.
Cette section explique comment le serveur peut s'identifier à l'aide des clés de serveur d'applications, et comment la charge utile chiffrée et les données associées sont envoyées.
Ce n'est pas un bon aspect du Web push et je ne suis pas expert en chiffrement, mais examinons chaque élément, car il est pratique de savoir ce que font ces bibliothèques en arrière-plan.
Clés du serveur d'application
Lorsque nous abonnons un utilisateur, nous transmettons un applicationServerKey
. Cette clé est transmise au service push et utilisée pour vérifier que l'application à laquelle l'utilisateur a souscrit est également celle qui déclenche les messages push.
Lorsque nous déclenchons un message push, nous envoyons un ensemble d'en-têtes qui permettent au service push d'authentifier l'application. (Ceci est défini par la spécification VAPID.)
Qu'est-ce que tout cela signifie réellement et que se passe-t-il exactement ? Voici les étapes suivies pour l'authentification du serveur d'applications:
- Le serveur d'application signe certaines informations JSON avec sa clé d'application privée.
- Ces informations signées sont envoyées au service push sous la forme d'un en-tête dans une requête POST.
- Le service push utilise la clé publique stockée qu'il a reçue de
pushManager.subscribe()
pour vérifier que les informations reçues sont signées par la clé privée en rapport avec la clé publique. Rappel: La clé publique est l'applicationServerKey
transmise dans l'appel d'abonnement. - Si les informations signées sont valides, le service push envoie le message à l'utilisateur.
Vous trouverez ci-dessous un exemple de ce flux d'informations. (Notez la légende en bas à gauche qui indique les clés publiques et privées.)
Les "informations signées" ajoutées à un en-tête de la requête sont un jeton Web JSON.
Jeton Web JSON
Un jeton Web JSON (ou JWT, en abrégé) est un moyen d'envoyer un message à un tiers afin que le destinataire puisse valider l'expéditeur.
Lorsqu'un tiers reçoit un message, il doit obtenir la clé publique des expéditeurs et l'utiliser pour valider la signature du jeton JWT. Si la signature est valide, le jeton JWT doit avoir été signé avec la clé privée correspondante. Il doit donc provenir de l'expéditeur attendu.
Il existe de nombreuses bibliothèques sur https://jwt.io/ qui peuvent effectuer la signature pour vous, et je vous recommande de le faire là où vous le pouvez. Pour plus d'exhaustivité, voyons comment créer manuellement un jeton JWT signé.
Jetons JWT signés et push Web
Un jeton JWT signé n'est qu'une chaîne, bien qu'il puisse être considéré comme trois chaînes reliées par des points.
La première et la deuxième chaîne (informations JWT et données JWT) sont des éléments JSON qui ont été encodés en base64, ce qui signifie qu'elles sont lisibles publiquement.
La première chaîne contient des informations sur le jeton JWT lui-même, indiquant l'algorithme utilisé pour créer la signature.
Les informations JWT pour le mode push Web doivent contenir les informations suivantes:
{
"typ": "JWT",
"alg": "ES256"
}
La deuxième chaîne correspond aux données JWT. Elle fournit des informations sur l'expéditeur du jeton JWT, à qui il est destiné et sa durée de validité.
Pour le Web push, les données se présentent comme suit:
{
"aud": "https://some-push-service.org",
"exp": "1469618703",
"sub": "mailto:example@web-push-book.org"
}
La valeur aud
correspond à l'"audience", c'est-à-dire à l'audience du jeton JWT. Pour le Web push, l'audience est le service push. Nous définissons donc cet élément sur l'origine du service push.
La valeur exp
correspond à l'expiration du jeton JWT. Cela empêche les pirates de pouvoir le réutiliser s'ils l'interceptent. Le délai d'expiration est un horodatage en secondes et ne doit plus être de 24 heures.
Dans Node.js, le délai d'expiration est défini à l'aide du code suivant:
Math.floor(Date.now() / 1000) + 12 * 60 * 60;
Pour éviter tout problème de différence d'horloge entre l'application émettrice et le service push, il faut 12 heures au lieu de 24 heures.
Enfin, la valeur sub
doit être une URL ou une adresse e-mail mailto
.
Ainsi, si un service push doit contacter l'expéditeur, il peut trouver les coordonnées à partir du JWT. (C'est pourquoi la bibliothèque web-push avait besoin d'une adresse e-mail.)
Tout comme les informations JWT, les données JWT sont encodées en tant que chaîne base64 sécurisée pour les URL.
La troisième chaîne, la signature, résulte de la signature des deux premières chaînes (informations JWT et données JWT) par un point, que nous appellerons le "jeton non signé".
Le processus de signature nécessite le chiffrement du "jeton non signé" à l'aide d'ES256. Selon la spécification JWT, ES256 est l'abréviation de "ECDSA utilisant la courbe P-256 et l'algorithme de hachage SHA-256". À l'aide de cryptos Web, vous pouvez créer la signature comme suit:
// Utility function for UTF-8 encoding a string to an ArrayBuffer.
const utf8Encoder = new TextEncoder('utf-8');
// The unsigned token is the concatenation of the URL-safe base64 encoded
// header and body.
const unsignedToken = .....;
// Sign the |unsignedToken| using ES256 (SHA-256 over ECDSA).
const key = {
kty: 'EC',
crv: 'P-256',
x: window.uint8ArrayToBase64Url(
applicationServerKeys.publicKey.subarray(1, 33)),
y: window.uint8ArrayToBase64Url(
applicationServerKeys.publicKey.subarray(33, 65)),
d: window.uint8ArrayToBase64Url(applicationServerKeys.privateKey),
};
// Sign the |unsignedToken| with the server's private key to generate
// the signature.
return crypto.subtle.importKey('jwk', key, {
name: 'ECDSA', namedCurve: 'P-256',
}, true, ['sign'])
.then((key) => {
return crypto.subtle.sign({
name: 'ECDSA',
hash: {
name: 'SHA-256',
},
}, key, utf8Encoder.encode(unsignedToken));
})
.then((signature) => {
console.log('Signature: ', signature);
});
Un service push peut valider un jeton JWT à l'aide de la clé publique du serveur d'applications pour déchiffrer la signature et s'assurer que la chaîne déchiffrée est identique au "jeton non signé" (c'est-à-dire les deux premières chaînes du jeton JWT).
Le jeton JWT signé (c'est-à-dire les trois chaînes reliées par des points) est envoyé au service Web push en tant qu'en-tête Authorization
avec le préfixe WebPush
, comme suit:
Authorization: 'WebPush [JWT Info].[JWT Data].[Signature]';
Le protocole Web Push indique également que la clé publique du serveur d'applications doit être envoyée dans l'en-tête Crypto-Key
en tant que chaîne encodée en base64 sécurisée pour les URL, avec le préfixe p256ecdsa=
.
Crypto-Key: p256ecdsa=[URL Safe Base64 Public Application Server Key]
Le chiffrement de la charge utile
Voyons maintenant comment envoyer une charge utile avec un message push afin que, lorsque notre application Web reçoit un message push, elle puisse accéder aux données qu'elle reçoit.
Tous les utilisateurs ayant utilisé d'autres services push se demandent souvent : pourquoi la charge utile push Web doit-elle être chiffrée ? Avec les applications natives, les messages push peuvent envoyer des données en texte brut.
L'un des avantages du service Web push est que, comme tous les services push utilisent la même API (le protocole Web push), les développeurs n'ont pas à se soucier de son identité. Nous pouvons envoyer une requête au bon format et espérer qu'un message push soit envoyé. L'inconvénient est que les développeurs peuvent éventuellement envoyer des messages à un service push non digne de confiance. En chiffrant la charge utile, un service push ne peut pas lire les données envoyées. Seul le navigateur peut déchiffrer les informations. Cela permet de protéger les données de l'utilisateur.
Le chiffrement de la charge utile est défini dans la spécification de Message Encryption.
Avant d'examiner les étapes spécifiques permettant de chiffrer la charge utile d'un message push, nous devons aborder certaines techniques qui seront utilisées lors du processus de chiffrement. (Astuce de Mat Scales pour son excellent article sur le chiffrement push.)
ECDH et HKDF
Les protocoles ECDH et HKDF sont utilisés tout au long du processus de chiffrement et offrent des avantages pour le chiffrement des informations.
ECDH: échange de clés Diffie-Hellman à courbe elliptique
Imaginez que deux personnes souhaitent partager des informations, Alice et Bob. Alice et Bob possèdent leurs propres clés publiques et privées. Alice et Bob partagent leurs clés publiques entre eux.
La propriété utile des clés générées avec ECDH est qu'Alice peut utiliser sa clé privée et la clé publique de Bob pour créer la valeur secrète "X". Bob peut faire de même, en utilisant sa clé privée et la clé publique d'Alice pour créer indépendamment la même valeur "X". « X » devient alors une clé secrète partagée et Alice et Bob n'avaient à partager que leur clé publique. Bob et Alice peuvent désormais utiliser "X" pour chiffrer et déchiffrer les messages entre eux.
À ma connaissance, ECDH définit les propriétés des courbes permettant à cette "caractéristique" de créer un secret partagé "X".
Il s'agit d'une explication générale du CPE. Je vous recommande de regarder cette vidéo si vous souhaitez en savoir plus.
En termes de code, la plupart des langages et plates-formes sont fournis avec des bibliothèques pour faciliter la génération de ces clés.
Dans le nœud, nous procéderons comme suit:
const keyCurve = crypto.createECDH('prime256v1');
keyCurve.generateKeys();
const publicKey = keyCurve.getPublicKey();
const privateKey = keyCurve.getPrivateKey();
HKDF: fonction de dérivation de clé basée sur HMAC
Wikipédia propose une description succincte du HKDF:
HKDF est une fonction de dérivation de clé basée sur HMAC qui transforme tout matériel de clé faible en matériel de clé renforcé de manière cryptographique. Elle permet, par exemple, de convertir les secrets partagés échangés par Diffie Hellman en un matériel de clé adapté au chiffrement, au contrôle de l'intégrité ou à l'authentification.
En gros, HKDF prend une entrée qui n'est pas particulièrement sécurisée et la rendra plus sûre.
La spécification définissant ce chiffrement nécessite l'utilisation de SHA-256 comme algorithme de hachage. Les clés obtenues pour HKDF en mode push Web ne doivent pas dépasser 256 bits (32 octets).
Dans le nœud, cela peut être implémenté comme suit:
// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
// Extract
const keyHmac = crypto.createHmac('sha256', salt);
keyHmac.update(ikm);
const key = keyHmac.digest();
// Expand
const infoHmac = crypto.createHmac('sha256', key);
infoHmac.update(info);
// A one byte long buffer containing only 0x01
const ONE_BUFFER = new Buffer(1).fill(1);
infoHmac.update(ONE_BUFFER);
return infoHmac.digest().slice(0, length);
}
Conseil sur l'article de Mat Scale pour cet exemple de code.
Nous allons aborder les termes ECDH et HKDF dans les grandes lignes.
ECDH est un moyen sécurisé de partager des clés publiques et de générer une clé secrète partagée. HKDF est un moyen de prendre des matériaux non sécurisés et de les sécuriser.
Il sera utilisé lors du chiffrement de notre charge utile. Voyons ensuite ce que nous prenons comme entrée et comment elle est chiffrée.
Entrées
Pour envoyer un message push à un utilisateur avec une charge utile, nous avons besoin de trois entrées:
- La charge utile elle-même.
- Le secret
auth
dePushSubscription
. - La clé
p256dh
dePushSubscription
.
Nous avons constaté que les valeurs auth
et p256dh
étaient récupérées à partir d'un PushSubscription
. Cependant, pour rappel, dans le cas d'un abonnement, nous avons besoin des valeurs suivantes:
subscription.toJSON().keys.auth;
subscription.toJSON().keys.p256dh;
subscription.getKey('auth');
subscription.getKey('p256dh');
La valeur auth
doit être traitée comme un secret et ne doit pas être partagée en dehors de votre application.
La clé p256dh
est une clé publique, parfois appelée clé publique du client. Ici, nous appellerons p256dh
la clé publique d'abonnement. La clé publique d'abonnement est générée par le navigateur. Le navigateur conserve la clé privée secrète et l'utilise pour déchiffrer la charge utile.
Ces trois valeurs, auth
, p256dh
et payload
sont nécessaires en tant qu'entrées. Le résultat du processus de chiffrement sera la charge utile chiffrée, une valeur de salage et une clé publique servant uniquement au chiffrement des données.
Sel
Le salage doit être de 16 octets de données aléatoires. En NodeJS, nous procéderons comme suit pour créer un salage:
const salt = crypto.randomBytes(16);
Clés publiques / privées
Les clés publiques et privées doivent être générées à l'aide d'une courbe elliptique P-256, ce que nous ferions dans Node comme suit:
const localKeysCurve = crypto.createECDH('prime256v1');
localKeysCurve.generateKeys();
const localPublicKey = localKeysCurve.getPublicKey();
const localPrivateKey = localKeysCurve.getPrivateKey();
Nous appellerons ces clés "clés locales". Elles ne sont utilisées que pour le chiffrement et n'ont rien à voir avec les clés du serveur d'application.
Avec la charge utile, le secret d'authentification et la clé publique d'abonnement en tant qu'entrées, et avec un nouveau salage et un ensemble de clés locales, nous sommes prêts à effectuer un chiffrement.
Clé secrète partagée
La première étape consiste à créer une clé secrète partagée à l'aide de la clé publique d'abonnement et de notre nouvelle clé privée (vous souvenez-vous de l'explication ECDH avec Alice et Bob ? en toute simplicité).
const sharedSecret = localKeysCurve.computeSecret(
subscription.keys.p256dh,
'base64',
);
Elle sera utilisée à l'étape suivante pour calculer la clé pseudo-aléatoire (PRK).
Clé pseudo-aléatoire
La clé pseudo-aléatoire (PRK) est la combinaison du secret d'authentification de l'abonnement push et du secret partagé que nous venons de créer.
const authEncBuff = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(subscription.keys.auth, sharedSecret, authEncBuff, 32);
Vous vous demandez peut-être à quoi sert la chaîne Content-Encoding: auth\0
.
En bref, son objectif n'est pas clair, bien que les navigateurs puissent déchiffrer un message entrant et rechercher l'encodage de contenu attendu.
\0
ajoute un octet avec une valeur de 0 à la fin du tampon. Ce comportement est attendu par les navigateurs qui déchiffrent le message, qui attendront un tel nombre d'octets pour l'encodage du contenu, suivi d'un octet ayant la valeur 0, puis des données chiffrées.
Notre clé pseudo-aléatoire exécute simplement l'authentification, la clé secrète partagée et une information d'encodage via HKDF (autrement dit, pour renforcer le chiffrement).
Contexte
Le "contexte" est un ensemble d'octets utilisé pour calculer deux valeurs ultérieurement dans le navigateur de chiffrement. Il s'agit essentiellement d'un tableau d'octets contenant la clé publique d'abonnement et la clé publique locale.
const keyLabel = new Buffer('P-256\0', 'utf8');
// Convert subscription public key into a buffer.
const subscriptionPubKey = new Buffer(subscription.keys.p256dh, 'base64');
const subscriptionPubKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = subscriptionPubKey.length;
const localPublicKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = localPublicKey.length;
const contextBuffer = Buffer.concat([
keyLabel,
subscriptionPubKeyLength.buffer,
subscriptionPubKey,
localPublicKeyLength.buffer,
localPublicKey,
]);
Le tampon de contexte final est constitué d'une étiquette, du nombre d'octets de la clé publique d'abonnement, suivi de la clé elle-même, puis du nombre d'octets de la clé publique locale, et enfin de la clé elle-même.
Avec cette valeur de contexte, nous pouvons l'utiliser dans la création d'un nonce et d'une clé de chiffrement de contenu (CEK).
Clé et nonce de chiffrement du contenu
Un nonce est une valeur qui empêche les attaques par rejeu, car il ne doit être utilisé qu'une seule fois.
La clé de chiffrement de contenu (CEK) est la clé qui sera à terme utilisée pour chiffrer notre charge utile.
Nous devons d'abord créer les octets de données du nonce et du CEK, qui sont simplement une chaîne d'encodage de contenu suivie du tampon de contexte que nous venons de calculer:
const nonceEncBuffer = new Buffer('Content-Encoding: nonce\0', 'utf8');
const nonceInfo = Buffer.concat([nonceEncBuffer, contextBuffer]);
const cekEncBuffer = new Buffer('Content-Encoding: aesgcm\0');
const cekInfo = Buffer.concat([cekEncBuffer, contextBuffer]);
Ces informations sont exécutées via HKDF en combinant le salage et la PRK avec nonceInfo et cekInfo:
// The nonce should be 12 bytes long
const nonce = hkdf(salt, prk, nonceInfo, 12);
// The CEK should be 16 bytes long
const contentEncryptionKey = hkdf(salt, prk, cekInfo, 16);
Cela nous donne notre nonce et notre clé de chiffrement de contenu.
Effectuer le chiffrement
Maintenant que nous avons notre clé de chiffrement de contenu, nous pouvons chiffrer la charge utile.
Nous créons un chiffrement AES128 en utilisant la clé de chiffrement de contenu comme clé et le nonce est un vecteur d'initialisation.
Dans Node, cela se fait comme ceci:
const cipher = crypto.createCipheriv(
'id-aes128-GCM',
contentEncryptionKey,
nonce,
);
Avant de chiffrer la charge utile, nous devons définir la marge intérieure à ajouter au début de la charge utile. L'ajout d'une marge intérieure empêche le risque que des personnes indiscrètes puissent déterminer des "types" de messages en fonction de la taille de la charge utile.
Vous devez ajouter deux octets de marge intérieure pour indiquer la longueur de toute marge intérieure supplémentaire.
Par exemple, si vous n'avez ajouté aucun remplissage, vous aurez deux octets avec la valeur 0, c'est-à-dire qu'aucun remplissage n'existe. Après ces deux octets, vous lirez la charge utile. Si vous avez ajouté 5 octets de marge intérieure, les deux premiers octets auront la valeur 5. Le consommateur lira donc cinq octets supplémentaires, puis commencera à lire la charge utile.
const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeros, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);
Nous passons ensuite notre remplissage et notre charge utile par ce chiffrement.
const result = cipher.update(Buffer.concat(padding, payload));
cipher.final();
// Append the auth tag to the result -
// https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
const encryptedPayload = Buffer.concat([result, cipher.getAuthTag()]);
Nous avons maintenant notre charge utile chiffrée. C'est super !
Il ne reste plus qu'à déterminer comment cette charge utile est envoyée au service push.
En-têtes et corps de la charge utile chiffrés
Pour envoyer cette charge utile chiffrée au service push, nous devons définir plusieurs en-têtes différents dans notre requête POST.
En-tête de chiffrement
L'en-tête "Chiffrement" doit contenir le sel utilisé pour chiffrer la charge utile.
La valeur salt de 16 octets doit être encodée en toute sécurité en base64 et ajoutée à l'en-tête de chiffrement, comme suit:
Encryption: salt=[URL Safe Base64 Encoded Salt]
En-tête Crypto-Key
Nous avons constaté que l'en-tête Crypto-Key
était utilisé dans la section "Clés de serveur d'applications" pour contenir la clé publique du serveur d'applications.
Cet en-tête est également utilisé pour partager la clé publique locale utilisée pour chiffrer la charge utile.
L'en-tête obtenu se présente comme suit:
Crypto-Key: dh=[URL Safe Base64 Encoded Local Public Key String]; p256ecdsa=[URL Safe Base64 Encoded Public Application Server Key]
Type de contenu, longueur et en-têtes de codage
L'en-tête Content-Length
correspond au nombre d'octets dans la charge utile chiffrée. Les en-têtes "Content-Type" et "Content-Encoding" sont des valeurs fixes.
Ce processus est illustré ci-dessous.
Content-Length: [Number of Bytes in Encrypted Payload]
Content-Type: 'application/octet-stream'
Content-Encoding: 'aesgcm'
Une fois ces en-têtes définis, nous devons envoyer la charge utile chiffrée dans le corps de la requête. Notez que Content-Type
est défini sur application/octet-stream
. En effet, la charge utile chiffrée doit être envoyée sous forme de flux d'octets.
En NodeJS, nous procéderons comme suit:
const pushRequest = https.request(httpsOptions, function(pushResponse) {
pushRequest.write(encryptedPayload);
pushRequest.end();
Plus d'en-têtes ?
Nous avons abordé les en-têtes utilisés pour les clés JWT et de serveur d'applications (c'est-à-dire, comment identifier l'application avec le service push), ainsi que les en-têtes utilisés pour envoyer une charge utile chiffrée.
Les services push utilisent des en-têtes supplémentaires pour modifier le comportement des messages envoyés. Certains de ces en-têtes sont obligatoires, d'autres sont facultatifs.
En-tête TTL
Obligatoire
TTL
(ou durée de vie) est un entier spécifiant le nombre de secondes pendant lequel vous souhaitez que votre message push soit diffusé sur le service push avant sa distribution. Lorsque le champ TTL
expire, le message est supprimé de la file d'attente du service d'envoi et n'est pas distribué.
TTL: [Time to live in seconds]
Si vous définissez la valeur TTL
sur zéro, le service push tente de distribuer le message immédiatement, mais si l'appareil n'est pas accessible, votre message est immédiatement retiré de la file d'attente du service push.
Techniquement, un service push peut réduire le TTL
d'un message push s'il le souhaite. Vous pouvez savoir si cela s'est produit en examinant l'en-tête TTL
dans la réponse d'un service push.
Thème
Optional
Les sujets sont des chaînes qui peuvent être utilisées pour remplacer un message en attente par un nouveau message s'ils ont des noms de sujets correspondants.
Cela est utile dans les cas où plusieurs messages sont envoyés alors qu'un appareil est hors connexion et que vous souhaitez qu'un utilisateur ne voie le dernier message que lorsque l'appareil est allumé.
Degré d'urgence
Optional
L'urgence indique au service push l'importance d'un message pour l'utilisateur. Le service push peut l'utiliser pour préserver l'autonomie de la batterie de l'appareil d'un utilisateur en ne réveillant les messages importants que lorsque la batterie est faible.
La valeur de l'en-tête est définie comme indiqué ci-dessous. La valeur par défaut est normal
.
Urgency: [very-low | low | normal | high]
Centralisez vos opérations
Si vous avez d'autres questions sur le fonctionnement de tout cela, vous pouvez toujours voir comment les bibliothèques déclenchent des messages push sur l'organisation web-push-libs.
Une fois que vous disposez d'une charge utile chiffrée et des en-têtes ci-dessus, il vous suffit d'envoyer une requête POST au endpoint
dans un PushSubscription
.
Que faire de la réponse à cette requête POST ?
Réponse du service push
Une fois que vous avez envoyé une requête à un service push, vous devez vérifier le code d'état de la réponse, car il vous indique si la requête a abouti ou non.
Status Code | Description |
---|---|
201 | Création terminée La demande d'envoi de message push a été reçue et acceptée. |
429 | Trop de requêtes. Cela signifie que votre serveur d'applications a atteint une limite de débit avec un service push. Le service push doit inclure un en-tête "Réessayer-After" pour indiquer le délai avant qu'une autre requête puisse être effectuée. |
400 | Demande incorrecte. Cela signifie généralement que l'un de vos en-têtes n'est pas valide ou est mal formaté. |
404 | Introuvable. Cela signifie que l'abonnement a expiré et qu'il ne peut pas être utilisé. Dans ce cas, vous devez supprimer "PushSubscription" et attendre que le client se réabonne à l'utilisateur. |
410 | Supprimé. L'abonnement n'est plus valide et doit être supprimé du serveur d'application. Cela peut être reproduit en appelant "unsubscribe()" sur un "PushSubscription". |
413 | Charge utile trop volumineuse. La charge utile de taille minimale prise en charge par un service push est de 4 096 octets (ou 4 Ko). |
Étapes suivantes
- Présentation des notifications push Web
- Fonctionnement du mode push
- S'abonner à un utilisateur
- Expérience utilisateur des autorisations
- Envoyer des messages avec des bibliothèques Web Push
- Protocole Web Push
- Gérer les événements push
- Afficher une notification
- Comportement des notifications
- Formats de notification courants
- Questions fréquentes sur les notifications push
- Problèmes courants et signalement de bugs