zaru / webpush

webpush, Encryption Utilities for Web Push protocol
MIT License
394 stars 73 forks source link

WebPush, 0.2.5, Webpush::InvalidSubscription (#<Net::HTTPBadRequest 400 UnauthorizedRegistration readbody=true>): #25

Closed tomgallagher closed 5 years ago

tomgallagher commented 8 years ago

Hi

I'm making a first venture in push notifications using gcm and your timely gem.

I'm suffering from the error above, even when I send without payload.

Double and triple checked, 1) the id in manifest.json 2) the endpoint, the keys 3) the apikey.

I'm getting everything back from the service worker as I expect but I'm failing on this bit:

Webpush.payload_send( message: JSON.generate(message), endpoint: endpoint, p256dh: p256dh, auth: auth, api_key: api_key)

Any ideas of further things I could try? I'm kind of stuck.

Thanks!

Tom

tomgallagher commented 8 years ago

I thought I'd add the relevant bits of JS and ruby

registration.pushManager.subscribe({ userVisibleOnly: true })
                  .then(function(subscription) {

                        var subData = new Object();
                        subData.endpoint = subscription.endpoint;
                        subData.p256dh = btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('p256dh')))).replace(/\+/g, '-').replace(/\//g, '_');
                        subData.auth = btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('auth')))).replace(/\+/g, '-').replace(/\//g, '_');
                        console.log(subData);
                        var subscriptionURL = "/push_subscribe";

                        $.ajax({
                            type: "POST",
                            contentType: 'application/json; charset=UTF-8',
                            dataType: 'json',
                            url: subscriptionURL,
                            data: JSON.stringify(subData),
                            cache: false,
                            success: function(response) {
                                        console.log("Successful Posting of Subscription Data");
                                        },
                            error: function (jqXHR, exception) {
                                        var msg = '';
                                        if (jqXHR.status === 0) {
                                            msg = 'Verify network connection.';
                                        } else if (jqXHR.status == 404) {
                                            msg = 'Requested page not found. [404]';
                                        } else if (jqXHR.status == 500) {
                                            msg = 'Internal Server Error [500].';
                                        } else if (exception === 'parsererror') {
                                            msg = 'Requested JSON parse failed.';
                                        } else if (exception === 'timeout') {
                                            msg = 'Time out error.';
                                        } else if (exception === 'abort') {
                                            msg = 'Ajax request aborted.';
                                        } else {
                                            msg = 'Uncaught Error:' + jqXHR.responseText;
                                        }
                                        console.log(msg);
                            }

                        });    

                });
def push_subscribe
      @user = current_user
      session[:subscription_endpoint] = params[:endpoint]
      session[:P256DH_key] = params[:p256dh]
      session[:auth_key] = params[:auth]
      head :ok
      welcome_message
    end

    def welcome_message

      @user = current_user

      endpoint = session[:subscription_endpoint]
      p256dh = session[:P256DH_key] 
      auth = session[:auth_key]

      puts endpoint
      puts p256dh
      puts auth

      if endpoint.include? "googleapis.com"
          api_key = ENV['Google_Messaging_Key']
      else
          api_key = ""
      end

      puts api_key

      message = {
        title: "Notification",
        body: "Hello #{@user.name}, the time is #{Time.zone.now}",
        icon: "https://s3-eu-west-1.amazonaws.com/images/THE_ICON.png"
      }

      Webpush.payload_send( message: JSON.generate(message), endpoint: endpoint, p256dh: p256dh, auth: auth, key: api_key)
      #Webpush.payload_send( endpoint: endpoint, api_key: api_key)

    end
rossta commented 8 years ago

@tomgallagher Some combination of endpoint, p256dh, and/or auth is invalid.

First, you could try resubscribing in your browser, which would create a new set of values for those variables.

If you still get the same error, you could try following my tutorial on the subject and make some changes where you see relevant differences between your code and my example. I'm particularly suspicious of these lines:

btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('p256dh')))).replace(/\+/g, '-').replace(/\//g, '_')
btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('auth')))).replace(/\+/g, '-').replace(/\//g, '_');

I've seen that code in older tutorials that I don't believe is necessary anymore, at least in Chrome or Firefox last time I checked. You should be able to simply call subscription.toJSON() to serialize it for transport to your backend; you can see how I did this in my blog post.

Hope that's helpful.

tomgallagher commented 8 years ago

Hi - thanks very much for getting back.

I've followed your tutorial already - in fact that was what made me realize that push notifications might now be possible from my Rails 4 app.

Ironically, I couldn't get your example to work, and it was the functionally equivalent lines in Ruby to the ones you don't like in the above Javascript.

JSON.parse(Base64.urlsafe_decode64(encoded_subscription)).with_indifferent_access

I'm trying to introduce this into existing code. I have wrap parameters for JSON in my controller, and I suspected that, with the wrap parameters, I did not need the JSON dump, but the fact that it was all encoded as well meant I lost my way.

I'll try your method again.

tomgallagher commented 8 years ago

So just to be get a better understanding of what's going on, I have three elements:

1) an endpoint, like this:

https://android.googleapis.com/gcm/send/cUzAjNfC37k:APA91bFhy7mgDaVJpohn4k-4_jJG52dX0xIVGog4Gcjt8-zLHYVb_yRyhSpm8UVaxMRxURnspURmAhgmjBjx6O017sAsHAFAZXppF6SGI9zB8v7pNiHR7RMrgfvMfMi62qdp52OJ-VVj

2) a 'p256dh', like this: BK7oMqjuY1utS6dBzPAbqQgvR12NdRu49IO-j9g6MdxcPZc1nKSW76fY-TSCE9ZO6x5KOkoyNYvWgyQ8KdQ1GJY=

3) and an 'auth' element, like this: msd8p7ydAkewsyu4ml0PyA==

Am I right in saying that all of these are encoded in some way when I get the subscription from the service worker? Or is it just the keys that are encoded? So how I transport them back, whether .toJSON() or JSON.stringify, doesn't matter? So I am looking to decode them before they get sent as part of the payload web push?

Could you talk a little bit more about the decoding part and this line in particular?

JSON.parse(Base64.urlsafe_decode64(encoded_subscription)).with_indifferent_access

tomgallagher commented 8 years ago

What I don't understand is why any decoding needs to be done at all. I get two values p256dh and auth that are encoded in a variant of Base64 that Chrome call URL-Safe Base64. Are you then saying that these strings need to be decoded before I can use them as inputs for the Webpush.payload_send?

rossta commented 8 years ago

I'm encoding/decoding the entire subscription JSON string to/from the session. This technique is irrelevant to your problem. You should feel free to continue using three separate session values for your app.

Then subscription.toJSON() will convert your subscription object into the following:

{
  endpoint: "https://android.googleapis.com/gcm/send/a-subscription-id",
  keys: {
    auth: "16ByteString",
    p256dh: "65ByteString"
  }
}

I suggested that as a convenience. You're right, how you transport them doesn't matter. Just be aware you don't have to do any sort of encoding/decoding on the client or server. The raw values you extract from the subscription object are the same values you pass to the web push API call.

Does that help?

rossta commented 8 years ago

Your note lead me to a "bug" in the tutorial with the session encoding/decoding step - thanks!.

To simplify things for session storage, I'm going to edit the tutorial as follows (I'll need to test it out first, but it "should work" fine):

# app/controllers/subscriptions_controller.rb
class SubscriptionsController < ApplicationController
  def create
    session[:subscription] = JSON.dump(params.fetch(:subscription, {}))

    head :ok
  end
end

# app/controllers/push_notifications_controller.rb
class PushNotificationsController < ApplicationController
  # ...
  def fetch_subscription
    subscription = session.fetch(:subscription) do
      raise "Cannot create notification: no :subscription in params or session"
    end

    JSON.parse(subscription).with_indifferent_access
  end
end

So, no more base64 encoding to avoid confusion.

tomgallagher commented 8 years ago

Hi - thanks for getting back.

I've just had a look at the code in the gem and webpush.rb has these lines in it.

GCM_URL = 'https://android.googleapis.com/gcm/send'
TEMP_GCM_URL = 'https://gcm-http.googleapis.com/gcm'

and then in the payload_send function:

def payload_send(endpoint:, message: "", p256dh: "", auth: "", **options)
      endpoint = endpoint.gsub(GCM_URL, TEMP_GCM_URL)

      payload = build_payload(message, p256dh, auth)

      Webpush::Request.new(endpoint, options.merge(payload: payload)).perform
end

So I'm not actually sending to the address in my endpoint. Doesn't this make a difference?

In the code it says:

It is temporary URL until supported by the GCM server.

Are you the author? Do you know why this is there?

Thanks a lot for taking the time to help me with all this.

rossta commented 8 years ago

The android url is the "old" version, still but I believe is still valid. The gcm-http url I believe is the proper one to use. @zaru may be able to speak to the gsub line, but it may simply be a future proofing step.

Related SO question: http://stackoverflow.com/questions/32418171/correct-url-for-message-to-gcm-device

tomgallagher commented 8 years ago

Rats. I was hoping that might be the cause of my problems. Sometimes with these API issues, it's simply a case of "turning them on", for example in the Google Developers Console. But I can't find anything related to the GCM API that looks like it needs to be enabled. I've got no filters on my allowed HTTP addresses, the API is current and appears in my list of APIs. It's really quite frustrating!

tomgallagher commented 8 years ago

A simple curl request to the endpoint retrieved from my subscription info returns "Unauthorized Registration" as well. So it's not the gem.

curl "https://android.googleapis.com/gcm/send/fcYabJTn9OU:APA91bGljSEtDZsSk9GZYNpLpFS6LgO5qeq4lPZfPLh8Bk-_r7Iq0dAq3xZ1yL1Ck6FeZ9SdkaUaGITSXb82X9Dwox-xbXqn2Lpv6MbFSAxc2CCzOeR9F1EnKkeyO_AnKfT5fkTv7BNb" --request POST --header "TTL: 60" --header "Content-Length: 0"

tomgallagher commented 8 years ago

Looks like Google have abandoned GCM, with the advent of the Web Push Protocol and VAPID. Maybe that's why I'm having problems with a new project and API

rossta commented 8 years ago

Possible though I'd be really surprised if GCM support was dropped that quickly. Wondering out loud if there may also be some bugs in handling the deprecated protocol as they transition to VAPID...

tomgallagher commented 8 years ago

OK, look thanks for everything. Could you let me know on here if you hear anything? I'd be most grateful! Also, I've checked out the commercial ones, PushCrew and OneSignal - they don't offer the flexibility in terms of responding to push events that I need. So I'm in for the long haul....

rossta commented 8 years ago

@tomgallagher There is an open issue for this library to support VAPID #13 so we may have that soon as a next step.

mpontus commented 8 years ago

What helped me solve 400 UnauthorizedRegistration error was migrating from GCM to FCM and updating the API key as explained here: http://stackoverflow.com/questions/37789264/api-key-for-gcm-is-suddenly-invalid-unauthorized-401-error

gazay commented 8 years ago

@rossta hello! I've tried your branch of webpush gem - https://github.com/rossta/webpush/tree/vapid-spike and I'm receiving #<Net::HTTPCreated 201 Created readbody=true> but event didn't appear in my browser's service worker as if I send it with same subscription object and keys through node web-push library. Maybe you know what the problem could be? How can I help you with fixing webpush gem?

gazay commented 8 years ago

Btw I've found this fresh gem and it works: https://github.com/miyucy/web_push

rossta commented 8 years ago

@gazay Thanks for offering to help! I cleaned up my vapid spike and opened a PR at https://github.com/zaru/webpush/pull/26. I can get webpush working in FFNightly without any issues, but consistently get the UnauthorizedRegistration 400 error in Chrome. I'm not sure yet if this is because of the library or my web app setup (like the manifest or FCM app settings).

If you have time to test it out yourself, let me know what you see. My PR is not well-documented yet, but it looks like you've figured out the usage already. I'll try to add some usage info to the README in the PR soon.

rossta commented 8 years ago

@tomgallagher VAPID support has landed for this gem as of version 0.3.0. The README provides some instructions for setting it up. Hope that helps with the issues you've been having in Chrome.

tomgallagher commented 8 years ago

That's great! Thanks very much I'll give it a try

jorgecuevas92 commented 7 years ago

@rossta I've been having this exact InvalidSubscription problem with your serviceworker-rails-sandbox in a weird way, everything works on my laptop through localhost but when I upload the web app to a server I get this error for every single push request on Chrome. On Firefox however everything works fine. Any ideas on how to solve this?

rossta commented 7 years ago

@jorgecuevas92 Hard to say without more info about your setup... do you have the most recent checkout from my project? Have you tried clearing the serviceworker(s) for the app?

rossta commented 7 years ago

@jorgecuevas92 Have you tried unsubscribing and resubscribing on the front-end? Here's a one way to do it from the sandbox.

dfabreguette commented 7 years ago

@rossta @jorgecuevas92 Looks like I have the exact same problem - It works fine through localhost but not in production. I'm wondering if using a Load Balancer (and marshalling headers) isn't part of our problem. And i've tried unsubscribe / resubscribe thing but none of the notifications are going through anyway. :( Don't really know where to debug know ! Any ideas ?

dfabreguette commented 7 years ago

I solved temporarily my problem by switching back to non VAPID method. Also, my chrome was not detecting my manifest file, even tough I placed the "<link rel='manifest'..." in my head section. I had to move it right after the "<title" declaration.

jorgecuevas92 commented 7 years ago

@rossta @dfabreguette-ap I tried unsubscribing and subscribing again with no luck, but it seems that is not related to that since I tried on different devices and the subscription was new on them.

I also noticed that on Opera 42 the result is the same when trying to send a notification.

After that I tried on Chromium and the result was exactly the same.

Since chrome and opera are chromium based browsers it seems safe to say that it is an issue that only happens on chromium based browsers.

I noticed this Sucker Punch error on the application logs, it happens on the three browsers:

Sucker Punch job error for class: 'ActiveJob::QueueAdapters::SuckerPunchAdapter::JobWrapper' args:[{"job_class"=>"WebpushJob", "job_id"=>"73ea0a87-43cd-4a11-bfeb-c901442459eb", "queue_name"=>"default", "priority"=>nil, "arguments"=>["Received a Notification ", {"endpoint"=>"https://fcm.googleapis.com/fcm/send/eCWMA_kR1cY:APA91bFlYt8TSdhBa4yupEcLAPAd62QVh0a8TJ1dO-Hlw-ROBqPFKG4DRgLaBk1N6JBV-Esy-B98VcuMuXB7Dan0jOzThg_1GgzrterZXx3Os4Vxtdnty_2oL4hEVTfZ-OHbuD370", "p256dh"=>"BILYUEKOIvXFI6_R2rL68lfu_A0df5J6RnCB_aiuf7E8tImghAJRSLjTKO0CT3wEYlhn20jojY9HpkilA0E BILYUEKOIvXFI6_R2rL68lfu_A8IgIeoW5J6RnCB_aiuf7E8tImghAJRSLjTKO0CT3wEYlhn20jojY9HpkilA0E=", "auth"=>"j1nRpCySPtryvUFOUrTy5P7YAw==", "_aj_symbol_keys"=>["endpoint", "p256dh", "auth"]}], "locale"=>"en"}]

As for the system setup:

Not sure what else would be useful, let me know so I can post it.

By the way sorry for the late response I got caught up on some other projects :D

redtachyons commented 7 years ago

Same issue here

quake commented 7 years ago

same issue here, found the root cause: our server time is not sync with NTP:

https://blog.mozilla.org/services/2016/04/04/using-vapid-with-webpush/

The “expiration” date is the UTC time in seconds when the claim should expire. This should not be longer than 24 hours from the time the request is made. For instance, in Javascript you can use: Math.floor(Date.now() * .001 + 86400).

https://github.com/zaru/webpush/blob/master/lib/webpush/request.rb#L109

rossta commented 5 years ago

Closing due to inactivity and several new releases in the interim.