Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A javascript api for sending push notifications to others #303

Open
jimmywarting opened this issue Nov 30, 2018 · 25 comments
Open

A javascript api for sending push notifications to others #303

jimmywarting opened this issue Nov 30, 2018 · 25 comments

Comments

@jimmywarting
Copy link

jimmywarting commented Nov 30, 2018

When you subscribe for push you receive a endpoint and keys for generating push and payloads.
You can forge a payload fully in the browser but not all endpoints don't respond with CORS headers.
(Got a 401 when i was sending a OPTION preflight to google)

That creates a problem: A browser is not able to send a push notification to another person

If two persons have exchanged subscription shouldn't they be able to ping eachother to initiate a conversation without having to go throught a backend signaling server?

I'm building a PWA and i want to host it on a static site like Github-Pages and do as little to no server stuff as possible. And a push subscription exchange only has to happen once - then you can store the subscription in localstorage or something and reconnect a friend later again and again without having to rely on a server.

function gotFriendsSubscription (friendsSubscription) {
  localStorage.myFriend = JSON.stringify(friendsSubscription)
})

async function initiateConversation (friendsSubscription) {
  const { pushManager } = serviceWorkerRegistration
  const pushSubscription = await pushManager.getSubscription()

  const sdp = await (
    // - create RTCPeerConnection
    // - create DataChannel
    // - wait for all iceCandidate to complete
    // - generate offer
    // - disable trickle
  )
  
  const payload = JSON.stringify({
    msg: "Hi lets start chatting",
    sdpOffer: sdp,
    // share my subscription so he can respond back with a sdpAnswer
    subscription: pushSubscription 
  })

  // My proposal:
  pushManager.send(friendsSubscription, payload)
  // maybe send some TTL ( time to live ) option
  // and maybe vapid is necessary also? don't know what should be required
  // its more about the concept then my api proposal
}

// sometime later
initiateConversation(JSON.parse(localStorage.myFriend))

with something like pushManager.send(subscription, payload) you can send message to yourself to debug and call friends without involving a server - the browser will happily send that push message without worry about any CORS problem

This would help to reestablish a lost WebRTC connection since offer/answers only works one time...

@martinthomson
Copy link
Member

Is the request for the specification to mandate the use of appropriate CORS headers so that push services could accept requests from arbitrary JS origins? If so, how does that interact with the expected VAPID keys?

@jimmywarting
Copy link
Author

jimmywarting commented Apr 2, 2019

My request is for browser to be able to make a push request on my behalf given that i supply all the necessary parameters for generating a payload and sending it to a receiver.

But "mandate the use of appropriate CORS" could work too, it's just means more complicated work on my end to generate sign and what not, I don't remember what VAPID dose for you. was a long time ago since i did some development with WebPush and have forgotten all about it and how it works so i can't comment on VAPID. it was also with the older "aesgcm" encoding

All i want is for it to be possible to send a WebPush to a Friend without a backend server

@martinthomson
Copy link
Member

I think that this is a common enough request. I don't think that it is appropriate to have the browser generate the message, as that can be trivially provided by a JSL. Recommending that servers allow POST requests via CORS is reasonable. @beverloo, @jrconlin, does this make sense to you? Do you think we need some way for the UA to request that the push service disable this allowance?

@jrconlin
Copy link

jrconlin commented Apr 3, 2019

I think it's fairly reasonable for Push Subscription Servers to allow POST requests for subscription updates.

A few things:

  1. I've built a few javascript WebPush test pages (although they use the older "aesgcm" encoding). The problem I tend to hit on are more around importing and deriving public keys inside of DOM level javascript. It works on Firefox, not so much on Chrome, but that's probably just my misunderstanding of Blink internals.

  2. VAPID is how you identify yourself when you're pushing a subscription message. With firefox, it's optional. With Google, it's less optional. You can also use VAPID to create "restricted subscriptions" that are only valid if the same key is used to sign the VAPID header is used to lock the subscription. It's the application server key element of the .subscribe() call.

  3. Push servers get a lot of trash like messages for expired push endpoints, badly formed messages, etc. One of the other goals of VAPID was to provide a way for you to say, "Hey, if you need to contact me, here's how you can." This can give our Ops folks a way to let you know there's a problem and let us help you fix it. So if you use some contact info like "bob@example.com" they're just going to black-hole all your data.

Does that help?

@beverloo
Copy link
Member

beverloo commented Apr 3, 2019

This sounds like a reasonable SHOULD requirement to me too. I believe that we enable CORS on our endpoints.

@jimmywarting
Copy link
Author

jimmywarting commented Apr 3, 2019

So what you are proposing is "A request should always be possible to make regardless if CORS are enable or not"? - essentially disable CORS (preflight) requirement for the endpoint?

@beverloo
Copy link
Member

beverloo commented Apr 3, 2019

No, the proposal is for the specification to state that push services should send the appropriate headers so that you can send messages from JavaScript.

You'll still need code (perhaps a library) on your website to exchange the subscription and keying material between clients, and to encrypt the message prior to sending it.

@jimmywarting
Copy link
Author

Gotcha

@evan-brass
Copy link

@alastaircoote
Copy link

Just bumping this, curious to know if there is any further consensus. I've recently discovered that I can successfully encrypt a Web Push payload via the Web Crypto API but can't send in Chrome because of CORS issues. A quick examination:

  • Mozilla (updates.push.services.mozilla.com): 👍
  • Chrome and Opera (fcm.googleapis.com): 👎
  • Edge (wns2-by3p.notify.windows.com) 👎

It would be great to get more consistency here, IMO true P2P sending is an interesting possibility.

@collimarco
Copy link

@alastaircoote What about the VAPID keys? You can't send them to the client for security reasons... So in my opinion Web Push is not made for P2P but it always requires a server.

@jrconlin
Copy link

I'm going to push back a bit on the VAPID requirement, mostly because VAPID is (supposed to be) Voluntary. It's also quite possible to generate VAPID headers on-the-fly, since they're bound to a given subscription, and that subscription may be as ephemeral as a test page. Heck, the body content of a push message is also optional, which means you don't have to have encryption if the body is empty, and the recipient would just get push event.

It's absolutely true that you don't want to expose the full subscription info block (the endpoint + encryption credentials) to anyone, since those could be used to send messages as the subscription provider. VAPID provides a bit of extra protection there since the Key should be generated externally. Technically, that can still be true if the VAPID key is generated outside of the web app, and the user trusts that the private key is never communicated outside of the app.

It's absolutely possible to generate the VAPID key in javascript using WebCrypto libraries.

@alastaircoote
Copy link

@collimarco in a true P2P scenario there would be no central VAPID keys. Any given client would generate keys then share them with the clients it wishes to exchange messages with, creating a kind of private group. How you'd transfer those keys securely is an open question but I can see WebRTC or in-person QR code scanning being two possibilities.

(that said, even if you did make the VAPID private key public you'd still need the endpoint, p256 and auth keys for any client so it's not like you'd be giving over a skeleton key.)

In any case I understand that P2P isn't a core aim of Web Push but it is possible with this relatively small tweak on the part of push providers. FWIW after I left my previous comment I put together a module that lets you use WebPush entirely via WebCrypto APIs:

https://github.com/alastaircoote/webpush-webcrypto

@jrconlin
Copy link

Probably getting a bit off-track from the original request here. Looping back, the original requirements were
A PWA that creates a push subscription between two parties that can be used later. The PWA would store data locally. The road block was that some Push Services block CORS requests.

I think that's the crux of the problem we're being asked to resolve.

Do we feel that a subscription service should allow Javascript services to POST data? I'm OK with that, mostly because of all the reasons we're mentioning here. Plus, the levels of spam our servers get pretty much indicate that CORS isn't doing much to prevent that. (Our biggest issue are around Subscription Servers that happily ignore 404/410 responses.)

@jimmywarting
Copy link
Author

i have also created my own fully client side WebPush entirely via WebCrypto APIs:
https://github.com/jimmywarting/jimmy.warting.se/tree/master/packages/webpush

@collimarco
Copy link

I am really curious about this... However I can't think of any real word scenario where P2P web push can be used.

A user would share his endpoint directly with other users, but notifications are associated to a domain name! (and to its authority, permissions, etc.).

A random user that receives the endpoint of another user can:

  1. share the endpoint with the world
  2. someone can send phishing or any other dangerous content to that user

... And the domain displayed (that will be probably blocked by antivirus, etc) is the domain. I don't think that your reasoning can be applied to real world applications, but only to demo applications.

@jrconlin
Copy link

Yeah, Push would make a terrible messaging system for a lot of reasons.

Push's main concept was to provide a way for sites (which tend to have addresses that are fairly static) to send messages back to users (which tend to have addresses that vary), in a way that's controllable by the client. There are a LOT of better protocols for doing messages (e.g. WebRTC, Message Layer Security (MLS), etc.)

That said, I've seen some configurations do some fairly clever things with push. Including encoding a subscription info block into a QR code so that two devices could share info. Say, connecting up a mobile device with a desktop device. In that case, both "users" are the same person, just different user agents. Could that have been done another way? Absolutely. Still, I can understand the trade-offs that come with all of the solutions.

@evan-brass
Copy link

evan-brass commented Dec 21, 2021

With this slight change, two users (which tend to have addresses that vary) could signal / bootstrap a WebRTC connection between them.

What excites me is that a universal WebRTC signaling protocol could be created on top of WebPush. For browser <-> browser peers they would use whatever push service is provided by the browser. For native applications or browsers that don't support WebPush or browsers with push services that require CORS they could use subscriptions from Mozilla Autopush. Lastly, for server peers, the server could act as its own push service so it wouldn't be dependent on anything other servers. That way all peers in the network are equal (can be bootstrapped from) and signal their peer-connections in the same protocol.

A user would share his endpoint directly with other users, but notifications are associated to a domain name! (and to its authority, permissions, etc.).

True, any app that makes its push info public would need to be careful when parsing the resulting messages. It would also probably need to add signatures to the messages since you otherwise couldn't know who was sending you the message. Lastly, it would be a form of cross-origin communication because any website with your push info can send you a message.

  1. share the endpoint with the world

True, If abuse is noticed by either the client or the server they can invalidate / unsubscribe from the push subscription. VAPID is mandatory now, and each token is valid for no more than 48hr (if I remember correctly). Assuming you don't give away your application server key then you would need to provide the other person with a signed VAPID token limiting their abuse to ~48 hours (or however far out you pre-sign keys).

  1. someone can send phishing or any other dangerous content to that user

Right, a website would need to be careful parsing. I agree that it's totally an abuse of the "notification" system to do this and protecting users might necessitate not allowing it. Browser's have tried to limit this use case by requiring web apps to produce a notification for every push message they receive (a default notification is created if the app doesn't create one).

@FluorescentHallucinogen

@beverloo

I believe that we enable CORS on our endpoints.

Any news about enabling CORS for https://fcm.googleapis.com/fcm/send/ so it can be used directly from the browser without the need for the CORS proxy, like https://updates.push.services.mozilla.com/wpush/v2/ did?

@alastaircoote
Copy link

  • Mozilla (updates.push.services.mozilla.com): 👍

Just happened to try this again today and discovered that Mozilla's push service doesn't allow CORS requests now either. Curious to know if this is a deliberate change or a regression.

@jrconlin
Copy link

It's a deliberate decision. Mostly for the reasons I noted above.

@jimmywarting
Copy link
Author

jimmywarting commented Feb 6, 2023

@jrconlin There are a LOT of better protocols for doing messages (e.g. WebRTC, Message Layer Security (MLS), etc.)

I know push isn't the best p2p communication channel, but it very useful for setting up a WebRTC connection if both user know each others endpoints/keys
I want to use web push to establish a WebRTC connection as a signaling service - without the need of having my own server / WebSocket logic.

it's either shared/public web push subscriptions or allow us to somehow be able to connect to a more static deterministic/reusable SDP. (A.K.A: hole punch once and use one-to-many - so that multiple peers can connect to same static RTCPeerConnection, but it's not designed like that) We can't do that today. the only thing that comes even close to it is a push subscription that is re-usable, static and deterministic and dose not change.

I can not say: "Here is a RTCPeerConnection who is online, go and talk to him via his SDP (hole punched ip/port) if you want to talk ti each other"
instead i have to say: "here this person is online and have this push subscription, you can send a push message to him to start signaling and also send him your own subscription while you are at it so he can send back his SDP answer back to you"

instead i need to create a WebSocket and constantly be connected at least to some peers via my own server and never being able to disconnect from them. if you have a PHP server then this would be way harder b/c it isn't event driven or dose not share the same variables (as it could be connected to a completely different slave machine or thread)

Really wish that Mozilla did not pull the plug on supporting CORS. it was so useful.

Now i want a built in method pushManager.send(friendsSubscription, payload) more then ever to be able send a cross origin message to all other push service provider cuz nobody wants to enable CORS 😞

@jrconlin
Copy link

jrconlin commented Feb 6, 2023

Yeah, I can see your point.

I apologize that I'm not familiar with the CORS note you mention, but I can also see it being a bit of an abuse vector. It's also kind of funny to me that in some respects, you're substituting one Websocket service for another, since our Push service for desktop is Websocket based. I mean, it's absolutely fair, and I'm not slighting your idea at all, it's just one of those "paths of least resistance" things I sometimes talk about in design meetings.

FWIW, I know that one of the bigger challenges with websockets is routing, particularly around firewalls. The internet hates long lived things, and definitely doesn't like unexpected things. (I mean, the fact that STUN is a thing and actually works is hilarious, but also why public STUN servers are a rarity.)

And since you're gonna need a payload (the URL of the hosting service at least), you're going to have to also manage the encryption keys for all the connections. Push doesn't do broadcast. At all. For a reason. (The reason is privacy.) The way I used to tell folk to do broadcast was to say "Have the clients go check a well known, public URL." (Update the URL with whatever content (encrypted or not), use a data free push to alert the UAs to go update, each pulls when it can). That still requires a common server for your service, which I think you wanted to get away from.

It also sounds like you're having fun with stateless computing. For that, you're gonna need some commonly shared data store. (There are "clever" ways to do it with internode communication, but trust me, go with the far simpler and more reliable common data store approach. Future you will not invent a time machine to murder you that way. Ask me how I know.)

@gustavcorpas
Copy link

I just wanted to leave my two cents on this matter..
If there is a discussion forum that is more suited for this matter please let me know.

A case for Peer to peer web push

The effects of GDPR as well as ethical and economic costs of running centralized servers will ( and has already ) increased the demand for peer-to-peer decentralized applications.

From what I can tell from this thread all major web push services are using a CORS policy that makes it impossible for the browser to create and send a web push notification. This is IMO stifiling innovation in the peer-to-peer space.

It is already possible for a client to build and share all the necessary web push things entirely in the browser via the web crypto api.

The only purpose of this CORS policy is to disallow browsers to initiate the push message.
Sharing and key generation can already happen entirely in the browser.

If this issue is not solved we may in the future see an increase in third party services acting as CORS proxies, or services more explicitly accepting JWT tokens to pass along to the web push service provider.
This is bad practise using unnessesary actors in the exchange.

Security concerns:

I am not a professional, but consider the following.

The main security concern is that the service worker can be leveraged to display notifications containing malicious content; e.g. links to other fraudulent sites.

Assuming a dishonest website there are no security gains from disallowing the browser to initiate push notifications, as the server holds the private vapid keys to sign JTW tokens today.

Assuming an honest website owner there may be an increased implementation risk, as vapid keys need to be passed securely to other users. However, this is already possible today. Only the actual request to the push service endpoint is disallowed in browsers.

Also consider that notifications are tied to the application, and users can always revoke notifications form certain apps.

Forcing developers into insecure solutions based on unnecessary proxies is IMO much more damaging than embracing this trend and supporting developers with standardized libraries and good documentation.

@jrconlin
Copy link

Yeah, like I said, I'm not hugely opposed to DOM level webpush subscription creation and use, and I've got a test page from the very early days of Push that basically does it. (I've also learned that one of the reasons that we have CORS enabled isn't for security, but because we were getting flooded by folks looking for security bounties because "Hey! This endpoint doesn't have CORS enabled!". Technical problems having non-techincal solutions and all...)

All that aside: what is required to do Push is to have a ServiceWorker to handle the incoming push request (for very good security reasons) and a trusted publisher to handle the outbound requests. That trusted publisher could be a python script sharing space on your server that gets called via a simple FastCGI that limits to the local server, and gets fed the remote user's subscription info.

You still need to have a way for the remote party to agree to get subscription updates (again, could be a simple FastCGI callback on your server that offers a PushSubscription link) and run the corresponding service worker, which means all the fun you normally have with trying to get two devices talking to each other (DNS, Firewalls, etc.). After that, you'd have two peers essentially sending push notifications to each other.

All of this seems like, frankly, a lot of work, just so that you can still rely on an outside party (in this case, Mozilla / Google / Apple / Microsoft) to enable very limited data exchange between two devices.

I'd probably just cut out the literal middleman in this case and use something like WebRTC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants