alastaircoote / webpush-webcrypto

15 stars 5 forks source link

Safari WebPush support doesn't accept JWTs from this library #4

Open Bluebie opened 1 year ago

Bluebie commented 1 year ago

I have your library working well with Chrome and Firefox on macOS Ventura, but Safari's web push endpoints are responding with HTTP 403 Forbidden: with body {"reason":"BadJwtToken"}

Apple's doc's indicate this error message could be caused by:

The JSON web token (JWT) has one of following issues:
- The JWT is missing.
- The JWT is signed with the wrong private key.
- The JWT subject claim isn’t a URL or mailto:.
- The JWT audience claim isn’t the origin of the push service where you sent the request.
- The JWT expiration parameter is more than one day into the future.

What I've looked at so far:

Bluebie commented 1 year ago

Debug logs:

{
  headers: {
    Encryption: 'salt=isPj-BpgoefoCo0zD6CZkQ',
    'Crypto-Key': 'dh=BAXyOq64oDKng00hII2X2xnMTVe1d_FVD26oT1his6YaZ6QhSox6jb2ID5CZeX1-YWhO1jQEkTWbvcHcCy_3JJ8; p256ecdsa=BEZCatmRpI460vTmtq3s3Ak9_A9EAFId7IeFlNz3szsgANV1Vlixa7MX4hjU9USzQeUkuUPKicjldA9AjlSZb3I',
    'Content-Length': '231',
    'Content-Type': 'application/octet-stream',
    'Content-Encoding': 'aesgcm',
    Authorization: 'WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwOi8vd2ViLnB1c2guYXBwbGUuY29tIiwiZXhwIjoxNjcyNDYxNjg0LCJzdWIiOiJtYWlsdG86Zm94QHBob2VuaXgubW9uc3RlciJ9.Tpm1kl9IQfnrTd9szQit3UqfHP8NGDwv2Zqsld-HzowK5-ZY5dprGIloV2xSKhMiOXx91mqnlnN-KTYzZB5-pg',
    TTL: '60',
    Urgency: 'low'
  },
  body: ArrayBuffer {
    [Uint8Contents]: <a8 43 0f f3 db 3a d8 80 62 af 74 bc 8a a3 90 ae 06 f0 65 35 f4 ec a4 7b 4a c5 54 94 69 35 5a f1 d6 1d a4 c6 f8 a5 ec f2 59 85 3c f1 f9 91 8c 18 25 00 eb 57 bf dc aa 56 22 46 ed f2 ea a0 a6 df 4d c8 fe 1e 98 b8 0a d3 27 c4 ed 7c c5 e3 a1 4c 69 90 bb e8 b1 0c c2 92 46 5f ad 3e 7d 40 03 65 6c f7 bc 96 ... 131 more bytes>,
    byteLength: 231
  },
  endpoint: 'https://web.push.apple.com/QKRk2ENkEtyy-bV6ful3vZQGES24AT-qZke1Mfm6ani6A79nU3JKiTtVxffhgelph7gewQ0Qa9SE03oSDFZUzC1CHlIc--5QCX4JEwOTtbCWwKB2GutvZenkA1xBConxbuxuZB_WE4M0VGS1wbc3NeiD9lprMAjNAZAQ7F77MIw'
}
Push Error to endpoint "https://web.push.apple.com/QKRk2ENkEtyy-bV6ful3vZQGES24AT-qZke1Mfm6ani6A79nU3JKiTtVxffhgelph7gewQ0Qa9SE03oSDFZUzC1CHlIc--5QCX4JEwOTtbCWwKB2GutvZenkA1xBConxbuxuZB_WE4M0VGS1wbc3NeiD9lprMAjNAZAQ7F77MIw": HTTP 403 Forbidden: {"reason":"BadJwtToken"}

from this implementation on sveltekit

///// -- $lib/io/webpush/server.ts
import type { WebPushSubscription } from "$lib/data/push"
import { readSiteConfig } from "$lib/data/site-config"
// @ts-expect-error
import { generatePushHTTPRequest } from "webpush-webcrypto"
import { applicationServerKeys } from "./key"

export async function sendWebPush (subscription: WebPushSubscription, title: string, options: NotificationOptions) {
  const siteConfig = await readSiteConfig()
  const { headers, body, endpoint } = await generatePushHTTPRequest({
    applicationServerKeys,
    payload: JSON.stringify({ title, options }),
    target: {
      endpoint: subscription.config.client.endpoint,
      keys: {
        p256dh: subscription.config.client.key,
        auth: subscription.config.client.auth,
      },
    },
    adminContact: siteConfig.adminContact,
    ttl: 60, // * 60 * 24 * 7,
    urgency: "low",
  })

  console.log({ headers, body, endpoint })

  const response = await fetch(endpoint, { method: 'POST', headers, body })
  if (!response.ok) {
    throw new Error(`Push Error to endpoint "${endpoint}": HTTP ${response.status} ${response.statusText}: ${await response.text()}`)
  }
}

///// -- endpoint server file:
import { readWebPushSubscription } from "$lib/data/push"
import { sendWebPush } from "$lib/io/webpush/server"
import { json } from "@sveltejs/kit"
import type { RequestHandler } from "./$types"

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json()
  const subscription = await readWebPushSubscription(body.endpoint)
  const result = await sendWebPush(subscription, body.title, body.config)
  return json({ ok: true, result })
}
alastaircoote commented 1 year ago

Unfortunately I don’t have access to a machine running Ventura right now so don’t have easy means to test Safari web push. One quick thought: does your siteConfig.adminContact have mailto: at the start of it? My examples don’t, I didn’t realise it was required (I guess Safari is stricter than others), fingers crossed that might be all it is.

Bluebie commented 1 year ago

Yes, I've tested with and without mailto: prefix. Maybe I can set up a mac and provide remote access if you want to try it out yourself over VNC or something like that?

alastaircoote commented 1 year ago

I just tried validating the JWT token in your example and the validation failed so I think for now I need to work out why that’s failing when others are validating. Will update with anything I find.

Bluebie commented 1 year ago

In case it's useful, this is how i'm setting up applicationServerKey

// file: ./key.ts
// @ts-expect-error
import { ApplicationServerKeys, setWebCrypto } from "webpush-webcrypto"
setWebCrypto(crypto)

// import.meta.env.VITE_WEB_PUSH_SERVER_KEY='{"publicKey":"BEZCatmRpI460vTmtq3s3Ak9_A9EAFId7IeFlNz3szsgANV1Vlixa7MX4hjU9USzQeUkuUPKicjldA9AjlSZb3I","privateKey":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQguBA_G1swtN7ZKsntySPnw8n5Y9L3TS0JtPxb384x2q2hRANCAARGQmrZkaSOOtL05rat7NwJPfwPRABSHeyHhZTc97M7IADVdVZYsWuzF-IY1PVEs0HlJLlDyonI5XQPQI5UmW9y"}'
const cfg = JSON.parse(import.meta.env.VITE_WEB_PUSH_SERVER_KEY)
export const applicationServerKeys = await ApplicationServerKeys.fromJSON(cfg)

and this is the script I used to generate that env value:

import crypto from 'node:crypto'
import { ApplicationServerKeys, setWebCrypto } from "webpush-webcrypto"
setWebCrypto(crypto)

ApplicationServerKeys.generate().then(x => {
  x.toJSON().then(x => console.log(JSON.stringify(x)))
})

I have rotated keys just before posting this, but those were the keys used to generate the logs posted earlier

Bluebie commented 1 year ago

Spent all day trying to track down what's going wrong here, and I'm still feeling lost. Here's what i've learnt along the way:

alastaircoote commented 1 year ago

I don't want to say "just use web_push for now"... but maybe just use web_push for now if it works for you. I will be able to take a look at Safari web pushes in January but not immediately, sorry.

CetinSert commented 9 months ago

@alastaircoote – this just works as of 2023-12-01 (confirmed with Safari 17.1 on iOS; with mailto: in adminContact)