web-push-libs / pywebpush

Python Webpush Data encryption library
Mozilla Public License 2.0
303 stars 52 forks source link

JWT token being refreshed too often #147

Open kukosk opened 2 years ago

kukosk commented 2 years ago

Apple has recently announced they will support web push for Safari on OSX this year, and also iOS and iPadOS the next year, so I'm checking my implementation if everything looks ok. I stumbled upon one sentence in the official documentation found here which states: Don’t refresh your JWT more frequently than once per hour.

I'm not sure yet if it will be a problem, not storing and reusing it for at least an hour as I don't have a beta version of OSX installed on my computer, but maybe this is something the library could do just in case? I suppose it would also be nice for other push endpoints, not just Apple.

I was also thinking how I would implement this on my side, but it seems to be quite complicated and maybe pywebpush is the right place for such a functionality?

jrconlin commented 2 years ago

You really shouldn't be updating your VAPID credentials unless you absolutely have to.

Your endpoints can be registered to your VAPID credentials (you do this by passing the VAPID Public key as the applicationServerKey). Registering your endpoints like this ensures that only your servers can provide updates. This also means that changing your VAPID keys immediately invalidates any existing endpoints.

Once you generate your VAPID key pair (You can use py-vapid to do this, which should be included in pywebpush), treat those keys as sensitive data. It's absolutely fine if you use the same key for decades and across multiple subscription types at that point, and we have some parties who do.

kukosk commented 2 years ago

Thank you very much @jrconlin for such a quick reply. I should've probably worded my original message a bit better.

I'm not actually updating my VAPID credentials, those are still the same, but what I was referring to was the JWT, which according to what I found is being generated and included in the Authorization header as the t parameter. When I was inspecting the value of the Authorization header, I found out that it's different for each request even though I'm sending a notification to the same client. Which, if I understand it correctly should not be happening in an ideal world? I suppose the token should be consistent between requests to the same host for at least 1 hour?

It's also possible that I'm missing something, though πŸ˜….

jrconlin commented 2 years ago

I think this gets into the weeds about VAPID's ECDH methodology (and I'll admit that it's been a couple of years since I had to seriously dig into this), but as I remember this is how it goes:

Vapid uses Elliptical Curve Diffie Hellman encoding. Elliptical Curve (EC) means that a coordinate pair is picked off some known Elliptical Curve (usually this is at some insane distance from 0,0, with all sorts of "dead coordinates" you really don't want to use.) The public and private keys are derivative values of that coordinate such that if you've got one set of points and I've got the other set of points, we can both agree that this third value is the coordinate. Tack on top of that the Diffie Hellman bits where you're doing fun stuff with theoretical math and how digits work when broken into chunks, and yeah, it gets weird and why cryptography is a fun way to break my mind.

On the server side, we take the value of t and use it to validate the signature portion of the JWT. That's using even more fun theoretical math to again, derive points off of the common coordinate, and then use those digits as primes for more encryption functions. Since those are basically signature values (and we're not actually encrypting or decrypting anything) the values can, and really should be different. A byproduct of that is that the derived public key we get from doing the VAPID check, should pair up with the public key you provided when you registered the subscription, and we can then prove that this update did come from you.

The key thing is that the principle coordinate (what's stuffed into the private key, if you're every curious enough to decode the DER structure it's stored in) stays the same, else, everything kinda goes to hell.

TL:DR; Don't take drugs, kids. Study Cryptography instead. It will absolutely screw with your head just as bad.

kukosk commented 2 years ago

Wow, that's some serious knowledge, and was very interesting to read for me! Thank you very much, I really learned a lot from that! 😊

So does this mean we're not actually 'refreshing' the JWT and follow the Apple requirement?

I initially thought that an approach similar to what I do in my pyapns_client package would be necessary - code sample to reuse the exact same token for a while.

And maybe one more quick question not related to the original one. Is it safe to reuse a requests.Session() for all the requests? Aren't there some edge cases with keepalive or servers going down for maintenance for example?

jrconlin commented 2 years ago

So does this mean we're not actually 'refreshing' the JWT and follow the Apple requirement?

pretty much. So long as you're not altering the base VAPID key, you're good.

I initially thought that an approach similar to what I do in my pyapns_client package would be necessary

APNS is it's own pile of fun. (Sorry, had to deal with that for the Push server stuff) IIRC, that's using OAuth2 (oh god, I get to point at all of my scars, don't I?) which has it's own authorization method that requires a chain of auth continuation. VAPID is WAY EASIER and is basically a glorified browser token.

The final note is that the pywebpush mini-app is also about as dumb and n00b friendly as I can make it. I really wouldn't use it as part of any serious platform, since I cut corners like nobody's business. Feel free to look at how I do the send() function to use far, far more robust implementations. The encode() does most of the work for you, with the rest being helpful stuff.

kukosk commented 2 years ago

Yeah, APNS was really fun πŸ˜“. I remember how much time I spent implementing it and making sure it works, just to hear Apple will be supporting webpush now 🀦. Even their JS API needed a ton of workarounds to make it functional. I don't know if the situation got better, but it was terrible. I'm glad they're making the move to webpush, but after working with their APIs for the last couple of years I'm a bit afraid if it'll work without "additional tweaks" πŸ˜….

Thank you so much btw, all of this has been very helpful! Now I just need to play around with the requests.Session() and hope for the best when the new version of OSX drops.

jrconlin commented 2 years ago

Well, Apple is Apple, which is always going to be interesting, but I have reasonably good hope that they really did implement their version of Push to the spec (which would be refreshing to say the least).

I know that one of the reasons that firefox for ios doesn't do webpush was because the original licensing terms for APNS prohibited third party messages. I heard that's no longer an issue, but it's been a while since I dug through all of that as well. (Sorry, I spend my time on back end servers, not front end stuff. How y'all stay ahead of that tsunami of information and changes never fails to impress the hell out of me.)

kukosk commented 2 years ago

Yeah, I'm actually really happy that Apple is making the move to webpush. The need to have an extra implementation just to make Safari work on OSX, and not being able to send push to iOS without an app wasn't ideal at all. I didn't expect Apple to implement webpush at all, they always tend to have their proprietary solutions for everything. So I guess we'll have to wait now and see their implementation, but I also hope they did it right 😊

I'm actually also struggling a bit to keep up with the frontend web stuff. It's true that I spend half of my time on frontend, but it's mainly iOS, and the other half is backend, so not that much time spent on web frontend. Yeah, on OSX, web browsers implemented custom solutions to make push notifications work even without APNS (with a limitation - they have to be running for push to work, which won't be the case for Safari), but on iOS that's just not possible, so I totally get your point why iOS browsers don't support push, if APNS prohibits 3rd party notifications.

Took me a lot more time to implement than I initially expected, but I think I have it done now 😊. I made a wrapper to handle stuff like error handling, payload trimming, retry logic and session reusing... All on top of the webpush function.

Thank you very much for all the valuable info about JWT btw, and for a great lib. It made my work a lot easier! πŸ‘