Melhorias na interoperabilidade por push na Web

Joe Medley
Joe Medley

Quando o Chrome passou a oferecer suporte à API Web Push, ele dependia do serviço de push do Firebase Cloud Messaging (FCM), anteriormente conhecido como Google Cloud Messaging (GCM). Isso é necessário usando uma API reservada. Isso permitiu que o Chrome disponibilizasse a API Web Push para desenvolvedores em um momento em que a especificação do protocolo de push da Web ainda estava sendo escrita e mais tarde fornecia autenticação (ou seja, o remetente da mensagem é quem diz ser) em um momento em que o protocolo de push da Web não a tinha. Boa notícia: nenhuma das alternativas é verdadeira.

O FCM / GCM e o Chrome agora oferecem suporte ao Web Push Protocol padrão. A autenticação do remetente pode ser feita implementando o VAPID, o que significa que seu app da Web não precisa mais de um "gcm_sender_id".

Neste artigo, descreverei primeiro como converter seu código de servidor atual para usar o Web Push Protocol com o FCM. Agora vou mostrar como implementar o VAPID no código do cliente e do servidor.

O FCM oferece suporte ao protocolo de push da Web

Vamos começar com um pouco de contexto. Quando seu aplicativo da Web se registra para uma assinatura de push, ele recebe o URL de um serviço de push. Seu servidor usará esse endpoint para enviar dados ao usuário por meio do seu app da Web. No Chrome, você receberá um endpoint do FCM se inscrever um usuário sem VAPID. Abordaremos o VAPID mais tarde. Antes de o FCM oferecer suporte ao protocolo de push da Web, era preciso extrair o ID de registro do FCM do final do URL e colocá-lo no cabeçalho antes de fazer uma solicitação da API FCM. Por exemplo, um endpoint do FCM de https://android.googleapis.com/gcm/send/ABCD1234 teria o ID de registro "ABCD1234".

Agora que o FCM oferece suporte ao protocolo de push da Web, deixe o endpoint intacto e use o URL como um endpoint desse protocolo. Isso alinha o navegador com o Firefox e, esperamos, com todos os outros navegadores futuros.

Antes de nos aprofundarmos no VAPID, precisamos garantir que nosso código de servidor processe corretamente o endpoint do FCM. Abaixo está um exemplo de como fazer uma solicitação para um serviço de push no Node. No FCM, estamos adicionando a chave de API aos cabeçalhos das solicitações. Para outros endpoints de serviços de push, isso não será necessário. Para o Chrome anterior à versão 52, o Opera Android e o navegador Samsung, também é necessário incluir um "gcm_sender_id" no manifest.json do seu app da Web. A chave de API e o ID do remetente são usados para verificar se o servidor que está fazendo as solicitações tem permissão para enviar mensagens ao usuário de destino.

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');
    }
});

Lembre-se de que essa é uma mudança na API do FCM / GCM. Portanto, você não precisa atualizar suas assinaturas, basta alterar o código do servidor para definir os cabeçalhos, conforme mostrado acima.

Introdução ao VAPID para identificação do servidor

VAPID é o novo nome curto para "Voluntary Application Server Identification" (Identificação voluntária de servidores de aplicativos). Essa nova especificação define um handshake entre o servidor do app e o serviço de push e permite que o serviço de push confirme qual site está enviando mensagens. Com o VAPID, é possível evitar as etapas específicas do FCM para enviar uma mensagem push. Não é necessário mais um projeto do Firebase, um cabeçalho gcm_sender_id ou Authorization.

O processo é bastante simples:

  1. O servidor do aplicativo cria um par de chaves pública/privada. A chave pública é fornecida ao seu app da Web.
  2. Quando o usuário optar por receber push, adicione a chave pública ao objeto de opções da chamada subscribe().
  3. Quando o servidor do app enviar uma mensagem de push, inclua um JSON Web Token assinado junto com a chave pública.

Vamos analisar essas etapas em detalhes.

Criar um par de chaves públicas/privadas

Sou péssimo em criptografia, então aqui está a seção relevante da especificação sobre o formato das chaves públicas/privadas VAPID:

Os servidores de aplicativos DEVEM gerar e manter um par de chaves de assinatura utilizável com assinatura digital de curva elíptica (ECDSA, na sigla em inglês) na curva P-256.

Veja como fazer isso na biblioteca de nós de push da web:

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

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

Como assinar com a chave pública

Para inscrever um usuário do Chrome para push com a chave pública VAPID, você precisa transmitir a chave pública como um Uint8Array usando o parâmetro applicationServerKey do métodosubscribe().

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

Para saber se ele funcionou, verifique o endpoint no objeto de assinatura resultante. Se a origem for fcm.googleapis.com, está funcionando.

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

Como enviar uma mensagem push

Para enviar uma mensagem usando o VAPID, você precisa fazer uma solicitação normal do Web Push Protocol com dois cabeçalhos HTTP extras: um de autorização e outro de chave de criptografia.

Cabeçalho de autorização

O cabeçalho Authorization é um JSON Web Token (JWT) assinado com "WebPush" na frente.

Um JWT é uma maneira de compartilhar um objeto JSON com uma segunda parte de modo que a parte remetente possa assiná-lo e o destinatário possa verificar se a assinatura é do remetente esperado. A estrutura de um JWT tem três strings criptografadas, juntas com um único ponto entre elas.

<JWTHeader>.<Payload>.<Signature>

Cabeçalho JWT

O cabeçalho do JWT contém o nome do algoritmo usado para assinatura e o tipo de token. Para VAPID, isso precisa ser:

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

Em seguida, ele é codificado em base64 e forma a primeira parte do JWT.

Payload

Payload é outro objeto JSON que contém o seguinte:

  • Público-alvo ("aud")
    • Esta é a origem do serviço de push (NÃO a origem do seu site). Em JavaScript, faça o seguinte para conseguir o público-alvo: const audience = new URL(subscription.endpoint).origin
  • Data de validade ("exp")
    • Esse é o número de segundos até que a solicitação seja considerada como expirada. PRECISA estar dentro de 24 horas da solicitação, em UTC.
  • Assunto ("sub")
    • O assunto precisa ser um URL ou um URL do mailto:. Isso fornece um ponto de contato caso o serviço de push precise entrar em contato com o remetente da mensagem.

Um payload de exemplo pode ser semelhante ao seguinte:

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

Esse objeto JSON tem URL codificado em base64 e forma a segunda parte do JWT.

Assinatura

A assinatura é o resultado da junção do cabeçalho e do payload codificados com um ponto e, em seguida, criptografa o resultado usando a chave privada VAPID criada anteriormente. O resultado em si deve ser anexado ao cabeçalho com um ponto.

Não vou mostrar um exemplo de código para isso porque há várias bibliotecas que usam os objetos JSON de cabeçalho e payload e geram essa assinatura para você.

O JWT assinado é usado como o cabeçalho de autorização com "WebPush" no início e será semelhante ao seguinte:

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

Observe alguns detalhes sobre isso. Primeiro, o cabeçalho de autorização contém literalmente a palavra "WebPush" e deve ser seguida por um espaço e depois pelo JWT. Observe também os pontos separando o cabeçalho do JWT, o payload e a assinatura.

Cabeçalho Crypto-Key

Além do cabeçalho de autorização, você precisa adicionar sua chave pública VAPID ao cabeçalho Crypto-Key como uma string codificada de URL base64 com p256ecdsa= anexado a ela.

p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo

Quando estiver enviando uma notificação com dados criptografados, você já estará usando o cabeçalho Crypto-Key. Portanto, para adicionar a chave do servidor do aplicativo, basta adicionar um ponto e vírgula antes de adicionar o conteúdo acima, o que resulta em:

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S_k7zi7A1coR_sVjHmGrlvzYpAT1n4NPbioFlQkIrTNL8EH4V3ZZ4vJE;
p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaN

Verdade dessas mudanças

Com o VAPID, você não precisa mais se inscrever em uma conta com o GCM para usar push no Chrome e pode usar o mesmo caminho de código para inscrever um usuário e enviar uma mensagem a ele no Chrome e no Firefox. Ambos estão seguindo os padrões.

Tenha em mente que, no Chrome 51 e versões anteriores, no navegador Opera para Android e Samsung, você ainda precisará definir o gcm_sender_id no manifesto do seu app da Web e adicionar o cabeçalho de autorização ao endpoint do FCM que será retornado.

Com o VAPID, é possível se desviar desses requisitos próprios. Se você implementar o VAPID, ele funcionará em todos os navegadores compatíveis com push da Web. Como mais navegadores oferecem suporte a VAPID, você pode decidir quando descartar o gcm_sender_id do seu manifesto.