Éxitos de la interoperabilidad push web

Joe Medley
Jo Medley

Cuando Chrome admitía por primera vez la API de Web Push, dependía del servicio de envío de Firebase Cloud Messaging (FCM), antes conocido como Google Cloud Messaging (GCM). Esto requiere el uso de su API patentada. Esto permitió que Chrome pudiera poner la API de Web Push a disposición de los desarrolladores en un momento en que la especificación del Protocolo web push aún se estaba escribiendo y, más tarde, proporcionaba autenticación (lo que significa que el remitente del mensaje es quien dice ser) en un momento en que el protocolo de envío web no la tenía. Buenas noticias: ya no esto es cierto.

FCM / GCM y Chrome ahora admiten el Protocolo de envío web estándar, mientras que la autenticación del remitente se puede lograr mediante la implementación de VAPID, es decir, tu app web ya no necesita un “gcm_sender_id”.

En este artículo, primero describiré cómo convertir tu código de servidor existente para usar el protocolo de envío web con FCM. A continuación, te mostraré cómo implementar VAPID en el código del cliente y del servidor.

FCM es compatible con el protocolo push web

Comencemos con un poco de contexto. Cuando tu aplicación web se registra para una suscripción de envío, se le proporciona la URL de un servicio de envío. Tu servidor usará este extremo para enviar datos a tu usuario a través de tu app web. En Chrome, se proporcionará un extremo de FCM si suscribes a un usuario sin VAPID. (Hablaremos sobre VAPID más adelante). Antes de que el protocolo de envío web admitiera FCM tenía que extraer el ID de registro de FCM del final de la URL y colocarlo en el encabezado antes de realizar una solicitud a la API de FCM. Por ejemplo, un extremo de FCM de https://android.googleapis.com/gcm/send/ABCD1234 tendría el ID de registro “ABCD1234”.

Ahora que FCM admite el protocolo de envío web, puedes dejar el extremo intacto y usar la URL como extremo del protocolo de envío web. (De esta manera, se alinea con Firefox y, con suerte, en todos los demás navegadores del futuro).

Antes de profundizar en VAPID, debemos asegurarnos de que nuestro código de servidor maneje correctamente el extremo de FCM. A continuación, se muestra un ejemplo de cómo realizar una solicitud a un servicio de envíos en Node. Ten en cuenta que, para FCM, agregamos la clave de API a los encabezados de la solicitud. Para otros extremos del servicio de envío, esto no será necesario. En el caso de Chrome anterior a la versión 52, Opera Android y el navegador Samsung, también se te solicitará que incluyas un "gcm_sender_id" en el manifiesto.json de tu aplicación web. La clave de API y el ID de remitente se usan para verificar si el servidor que realiza las solicitudes puede enviar mensajes al usuario receptor.

const headers = new Headers();
// 12-hour notification time to live.
headers.append('TTL', 12 * 60 * 60);
// Assuming no data is going to be sent
headers.append('Content-Length', 0);

// Assuming you're not using VAPID (read on), this
// proprietary header is needed
if(subscription.endpoint
    .indexOf('https://android.googleapis.com/gcm/send/') === 0) {
    headers.append('Authorization', 'GCM_API_KEY');
}

fetch(subscription.endpoint, {
    method: 'POST',
    headers: headers
})
.then(response => {
    if (response.status !== 201) {
    throw new Error('Unable to send push message');
    }
});

Recuerda que este es un cambio en la API de FCM / GCM, por lo que no necesitas actualizar tus suscripciones. Solo cambia el código de tu servidor para definir los encabezados como se mostró anteriormente.

Presentamos VAPID para la identificación del servidor

VAPID es el nuevo nombre corto de "Identificación voluntaria del servidor de aplicaciones". En esencia, esta especificación nueva define un protocolo de enlace entre tu servidor de apps y el servicio de envío, y permite que este confirme qué sitio está enviando mensajes. Con VAPID, puedes evitar los pasos específicos de FCM para enviar un mensaje push. Ya no necesitas un proyecto de Firebase, un encabezado gcm_sender_id o Authorization.

El proceso es bastante simple:

  1. Tu servidor de aplicaciones crea un par de clave pública/privada. La clave pública se proporciona a tu aplicación web.
  2. Cuando el usuario opte por recibir notificaciones push, agrega la clave pública al objeto de opciones de la llamada de Subscribe().
  3. Cuando tu servidor de apps envíe un mensaje push, incluye un token web JSON firmado junto con la clave pública.

Veamos estos pasos en detalle.

Crea un par de claves pública/privada

No tengo problemas con la encriptación. Esta es la sección relevante de la especificación relacionada con el formato de las claves públicas/privadas de VAPID:

Los servidores de aplicaciones DEBEN generar y mantener un par de claves de firma que se pueda usar con firma digital de curva elíptica (ECDSA) sobre la curva P-256.

Puedes ver cómo hacerlo en la biblioteca de nodo web-push:

function generateVAPIDKeys() {
    var curve = crypto.createECDH('prime256v1');
    curve.generateKeys();

    return {
    publicKey: curve.getPublicKey(),
    privateKey: curve.getPrivateKey(),
    };
}

Suscríbete con la clave pública

Para suscribir a un usuario de Chrome para envío con la clave pública de VAPID, debes pasar la clave pública como un elemento Uint8Array con el parámetro applicationServerKey del método Subscribe().

const publicKey = new Uint8Array([0x4, 0x37, 0x77, 0xfe, …. ]);
serviceWorkerRegistration.pushManager.subscribe(
    {
    userVisibleOnly: true,
    applicationServerKey: publicKey
    }
);

Para saber si funcionó, examina el extremo en el objeto de suscripción resultante. Si el origen es fcm.googleapis.com, está funcionando.

https://fcm.googleapis.com/fcm/send/ABCD1234

Envía un mensaje push

Para enviar un mensaje mediante VAPID, debes realizar una solicitud normal del protocolo de envío web con dos encabezados HTTP adicionales: uno de autorización y uno de clave criptográfica.

Encabezado de autorización

El encabezado Authorization es un token web JSON (JWT) firmado con “WebPush” delante de él.

Un JWT es una forma de compartir un objeto JSON con una segunda parte de modo que la parte emisora pueda firmarlo y la parte receptora pueda verificar que la firma sea del remitente esperado. La estructura de un JWT consta de tres strings encriptadas unidas con un solo punto entre ellas.

<JWTHeader>.<Payload>.<Signature>

Encabezado JWT

El encabezado JWT contiene el nombre del algoritmo que se usó para la firma y el tipo de token. Para VAPID, debe ser lo siguiente:

{
    "typ": "JWT",
    "alg": "ES256"
}

Esta URL se codifica en base64 y forma la primera parte del JWT.

Carga útil

La carga útil es otro objeto JSON que contiene lo siguiente:

  • Público ("aud")
    • Este es el origen del servicio push (NO el origen de tu sitio). En JavaScript, puedes hacer lo siguiente para obtener el público: const audience = new URL(subscription.endpoint).origin
  • Hora de vencimiento ("exp")
    • Esta es la cantidad de segundos que faltan para que la solicitud se considere vencida. Esto DEBE dentro de las 24 horas posteriores a la solicitud, en UTC.
  • Asunto ("sub")
    • El asunto debe ser una URL o una URL de mailto:. Esto proporciona un punto de contacto en caso de que el servicio de envío necesite comunicarse con el remitente del mensaje.

Una carga útil de ejemplo podría verse de la siguiente manera:

{
    "aud": "http://push-service.example.com",
    "exp": Math.floor((Date.now() / 1000) + (12 * 60 * 60)),
    "sub": "mailto: my-email@some-url.com"
}

Este objeto JSON está codificado como URL base64 y forma la segunda parte del JWT.

Firma

La firma es el resultado de unir el encabezado codificado y la carga útil con un punto y, luego, encriptar el resultado con la clave privada VAPID que creaste antes. El resultado en sí debe agregarse al encabezado con un punto.

No voy a mostrar una muestra de código para esto, ya que hay una cantidad de bibliotecas que tomarán el encabezado y los objetos JSON de carga útil y generarán esta firma por ti.

El JWT firmado se usa como el encabezado de autorización con “WebPush” antepuesto y se verá de la siguiente manera:

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

Observa algunos aspectos al respecto. Primero, el encabezado de autorización contiene literalmente la palabra “WebPush” y debe estar seguida por un espacio y, luego, el JWT. También observa los puntos que separan el encabezado JWT, la carga útil y la firma.

Encabezado de la clave criptográfica

Además del encabezado de autorización, debes agregar tu clave pública de VAPID al encabezado Crypto-Key como una string codificada de URL base64 con p256ecdsa= antepuesto.

p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo

Cuando envíes una notificación con datos encriptados, ya usarás el encabezado Crypto-Key, por lo que, para agregar la clave de servidor de la aplicación, solo debes agregar un punto y coma antes de agregar el contenido anterior, lo que genera lo siguiente:

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S_k7zi7A1coR_sVjHmGrlvzYpAT1n4NPbioFlQkIrTNL8EH4V3ZZ4vJE;
p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaN

Realidad de estos cambios

Con VAPID, ya no es necesario registrarse en una cuenta con GCM para usar el envío en Chrome, y puedes usar la misma ruta de código para suscribir a un usuario y enviar un mensaje a un usuario en Chrome y Firefox. Ambos siguen los estándares.

Lo que debes tener en cuenta es que, en Chrome 51 y en versiones anteriores, en los navegadores Opera para Android y Samsung, deberás definir el gcm_sender_id en el manifiesto de la app web y deberás agregar el encabezado de autorización al extremo de FCM que se mostrará.

El VAPID proporciona un acceso de salida a partir de estos requisitos de propiedad. Si implementas VAPID, funcionará en todos los navegadores compatibles con el envío web. A medida que más navegadores admiten VAPID, puedes decidir cuándo descartar gcm_sender_id de tu manifiesto.