web-push-libs / webpush-java

Web Push library for Java
MIT License
318 stars 112 forks source link

Authentication done seems to be broken #147

Closed kopax closed 4 years ago

kopax commented 4 years ago

I have copied the PushService and added somes logging into it to understand the headers send while notifying and I got:

2020-08-18 15:06:11,672 DEBUG com.kopaxgroup.api.userManagement.controller.PublicWebPushController : Total subscriptions where sending: 1
2020-08-18 15:06:11,672 DEBUG com.kopaxgroup.api.userManagement.controller.PublicWebPushController : NotificationEndPoint :https://updates.push.services.mozilla.com/wpush/v2/gAAAAABfO_D_DyMSNZ_rEF6.....................5VosoLky2BF589JnIi-..........................CHmeXnp0pq2dScdTcI
2020-08-18 15:06:11,672 DEBUG com.kopaxgroup.api.userManagement.controller.PublicWebPushController : PublicKey :BEk0G3whK+Cwm.....................................................awlgnp+jxk=
2020-08-18 15:06:11,672 DEBUG com.kopaxgroup.api.userManagement.controller.PublicWebPushController : Auth :SjG.xb7........P70A==
2020-08-18 15:06:11,675 DEBUG com.kopaxgroup.api.userManagement.controller.PublicWebPushController : sending start

2020-08-18 15:06:12,134 DEBUG com.kopaxgroup.api.userManagement.domain.PushService : ======= gcmApiKey ==========>null
2020-08-18 15:06:12,134 DEBUG com.kopaxgroup.api.userManagement.domain.PushService : ======= isFcm ==========>false
2020-08-18 15:06:12,138 DEBUG com.kopaxgroup.api.userManagement.domain.PushService : ======= HEADER USED ==========[TTL: 2419200, Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbG.................................................................aWNlcy5tb3ppbGxhLmNvbSIsIm3MSwic3ViIjoibWFpbH.................................................................b20ifQ.hwziqx7nJ719MnAu.................................................................s0e-oBoVihuqzZv5TtmhmiXO0t4zo1S0Ng, Content-Encoding: aesgcm, Encryption: salt=wJtF7h...BuTL...9UD5_g, Crypto-Key: dh=BO...TDInEjuaYwJVAebLrtXY.................................................................E4f_vgpZJI-YR9Bhjltfy2BTM=;p256ecdsa=BA4eDq9AC_vqPeCxEM_sfr6KQpDgPnJzW8cTZXlHRaxFrpxRyQwgW6Qk_yfgKblOQmYInisvFnFcgO33_NVj0TQ, Content-Type: application/octet-stream]
2020-08-18 15:06:12,139 DEBUG com.kopaxgroup.api.userManagement.domain.PushService : ========== privateKey ===========> EC Private Key
             S: 5cfd9195.................................................................457b2f856

2020-08-18 15:06:12,139 DEBUG com.kopaxgroup.api.userManagement.domain.PushService : ========== publicKey ===========> EC Public Key
            X: e1e0eaf4.................................................................4745ac45
            Y: ae9c51c.................................................................df7fcd563d134

This is my controller:

   @PostMapping(value = "/notify-all", produces = MediaType.APPLICATION_JSON_VALUE)
    public WebPushMessage notifyAll(@AuthenticationPrincipal Principal principal) throws JsonProcessingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
        WebPushMessage msg = new WebPushMessage(
                "test",
                "https://staging-www.icimatin.com/?clickTarget=me",
                "test message"
        );
        log.debug("Total subscriptions where sending: " + subscriptions.size());
        for (WebPushSubscription subscription : subscriptions.values()) {
            log.debug("NotificationEndPoint :" + subscription.getNotificationEndPoint());
            log.debug("PublicKey :" + subscription.getPublicKey());
            log.debug("Auth :" + subscription.getAuth());

            Notification notification = new Notification(
                    subscription.getNotificationEndPoint(),
                    subscription.getPublicKey(),
                    subscription.getAuth(),
                    objectMapper.writeValueAsBytes(msg));
            try {
                log.debug("sending start");

                HttpResponse response = pushService.send(notification);
                String responseString = EntityUtils.toString(response.getEntity(), "UTF-8");

                log.debug("Content responseString ====> " + responseString);
                int statusCode = response.getStatusLine().getStatusCode();
                log.debug("sending done: " + response.getStatusLine() + " " + Arrays.toString(response.getAllHeaders()));
                if (HttpStatus.NOT_FOUND.value() == statusCode || HttpStatus.GONE.value() == statusCode) {
                    log.debug("Subscription has expired or is no longer valid: " + response.getStatusLine() + ":" + response.getStatusLine().getReasonPhrase());
                    subscriptions.remove(subscription.getNotificationEndPoint());
                } else if (statusCode == HttpStatus.UNAUTHORIZED.value()) {
                    log.debug("Unauthorized request. This generally means your credentials are incorrect. " + response.getStatusLine().getReasonPhrase());
                } else if (statusCode == HttpStatus.PAYLOAD_TOO_LARGE.value()) {
                    log.error("Payload is too large, the minimum size payload a push service must support is 4096 bytes (or 4kb). " + response.getStatusLine().getReasonPhrase());
                } else if (statusCode == HttpStatus.BAD_REQUEST.value()) {
                    log.debug("Invalid request. This generally means one of your headers is invalid or improperly formatted. " + response.getStatusLine().getReasonPhrase());
                } else if (statusCode == HttpStatus.TOO_MANY_REQUESTS.value()) {
                    log.debug("Too many requests. Meaning your application server has reached a rate limit with a push service. The push service should include a 'Retry-After' header to indicate how long before another request can be made. " + response.getStatusLine().getReasonPhrase());
                } else if (statusCode == HttpStatus.CREATED.value()) {
                    log.debug("Created. The request to send a push message was received and accepted. " + response.getStatusLine().getReasonPhrase());
                }

            } catch (Exception e) {
                log.debug("sending produced exception :" + e.getMessage());
                log.debug("sending produced stacktrace :" + Arrays.toString(e.getStackTrace()));
            }
            log.debug("===> Sending notification is over, payload was: " + Arrays.toString(notification.getPayload()));
        }

        return msg;
    }

I added dots on keys. It seems that this module does the job but I keep getting unauthorized errors

Edited PushClient with logging bellow

Click to see relevant part with logging of original custom PushClient.java ```java /** * Prepare a HttpPost for Apache async http client * * @param notification * @param encoding * @return * @throws GeneralSecurityException * @throws IOException * @throws JoseException */ public HttpPost preparePost(Notification notification, Encoding encoding) throws GeneralSecurityException, IOException, JoseException { if (privateKey != null && publicKey != null) { if (!Utils.verifyKeyPair(privateKey, publicKey)) { throw new IllegalStateException("Public key and private key do not match."); } } Encrypted encrypted = encrypt( notification.getPayload(), notification.getUserPublicKey(), notification.getUserAuth(), encoding ); byte[] dh = Utils.encode((ECPublicKey) encrypted.getPublicKey()); byte[] salt = encrypted.getSalt(); HttpPost httpPost = new HttpPost(notification.getEndpoint()); httpPost.addHeader("TTL", String.valueOf(notification.getTTL())); if (notification.hasUrgency()) { httpPost.addHeader("Urgency", notification.getUrgency().getHeaderValue()); } if (notification.hasTopic()) { httpPost.addHeader("Topic", notification.getTopic()); } Map headers = new HashMap<>(); if (notification.hasPayload()) { headers.put("Content-Type", "application/octet-stream"); if (encoding == Encoding.AES128GCM) { headers.put("Content-Encoding", "aes128gcm"); } else if (encoding == Encoding.AESGCM) { headers.put("Content-Encoding", "aesgcm"); headers.put("Encryption", "salt=" + Base64Encoder.encodeUrlWithoutPadding(salt)); headers.put("Crypto-Key", "dh=" + Base64Encoder.encodeUrl(dh)); } httpPost.setEntity(new ByteArrayEntity(encrypted.getCiphertext())); } if (notification.isGcm()) { if (gcmApiKey == null) { throw new IllegalStateException("An GCM API key is needed to send a push notification to a GCM endpoint."); } headers.put("Authorization", "key=" + gcmApiKey); } else if (vapidEnabled()) { // if (encoding == Encoding.AES128GCM) { // if (notification.getEndpoint().startsWith("https://fcm.googleapis.com")) { // httpPost.setURI(URI.create(notification.getEndpoint().replace("fcm/send", "wp"))); // } // } JwtClaims claims = new JwtClaims(); claims.setAudience(notification.getOrigin()); claims.setExpirationTimeMinutesInTheFuture(12 * 60); if (subject != null) { claims.setSubject(subject); } JsonWebSignature jws = new JsonWebSignature(); jws.setHeader("typ", "JWT"); jws.setHeader("alg", "ES256"); jws.setPayload(claims.toJson()); jws.setKey(privateKey); jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256); byte[] pk = Utils.encode((ECPublicKey) publicKey); if (encoding == Encoding.AES128GCM) { headers.put("Authorization", "vapid t=" + jws.getCompactSerialization() + ", k=" + Base64Encoder.encodeUrlWithoutPadding(pk)); } else if (encoding == Encoding.AESGCM) { headers.put("Authorization", "WebPush " + jws.getCompactSerialization()); } if (headers.containsKey("Crypto-Key")) { headers.put("Crypto-Key", headers.get("Crypto-Key") + ";p256ecdsa=" + Base64Encoder.encodeUrlWithoutPadding(pk)); } else { headers.put("Crypto-Key", "p256ecdsa=" + Base64Encoder.encodeUrl(pk)); } } else if (notification.isFcm() && gcmApiKey != null) { headers.put("Authorization", "key=" + gcmApiKey); } for (Map.Entry entry : headers.entrySet()) { httpPost.addHeader(new BasicHeader(entry.getKey(), entry.getValue())); } log.debug("======= gcmApiKey ==========>" + gcmApiKey); log.debug("======= isFcm ==========>" + notification.isFcm()); log.debug("======= HEADER USED ==========" + Arrays.toString(httpPost.getAllHeaders())); log.debug("========== privateKey ===========> " + privateKey); log.debug("========== publicKey ===========> " + publicKey); return httpPost; } ```

I am trying hard and I can't get why the subscription does not work. Is it because the https is provided with a public front proxy and not by the http server itself?

kopax commented 4 years ago

I am trying to configure push notification with two clients (one from chrome and other one from firefox)

In chrome, pushing cause the unauthorized error:

the key in the authorization header does not correspond to the sender ID used to subscribe this user. Please ensure you are using the correct sender ID and server Key from the Firebase console.

In firefox, pushing cause the unauthorized error:

{
  "code": 401, 
  "errno": 109,
  "error": "Unauthorized",
  "more_info": "http://autopush.readthedocs.io/en/latest/http.html#error-codes", 
  "message": "Request did not validate missing authorization header"
}

I am using web-push-java to generate the headers, and I am not using a VAPID keypairs generated on firebase cloud manager, but one generated with this cli.

The headers send by the request look like this,

Chrome:

[TTL: 2419200, Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTU5NzgwODEzMywic3ViIjoibWFpbHRvOmQua29wcml3YUBnbWFpbC5jb20ifQ.k_rzKKmX0PBTr2WkGw0eYUV-0hfZ_x2YyKmTfvUu_knwC9VbVbArghOgtBsjtoMQvBf8udzRsqRWPzug4SYkJQ, Content-Encoding: aesgcm, Encryption: salt=xc4_6r19yFLN6RQgAb47DQ, Crypto-Key: dh=BEzq1bydxQKjt53e3Ui1EYRvXR3Zn7VFy7JcoGTYDCc_wkyy-E_Fn-zbqIHiAvm1UM4ar1vgrnyElmwgyXP5lHs=;p256ecdsa=BA4eDq9AC_vqPeCxEM_sfr6KQpDgPnJzW8cTZXlHRaxFrpxRyQwgW6Qk_yfgKblOQmYInisvFnFcgO33_NVj0TQ, Content-Type: application/octet-stream]

Firefox:

[TTL: 2419200, Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3VwZGF0ZXMucHVzaC5zZXJ2aWNlcy5tb3ppbGxhLmNvbSIsImV4cCI6MTU5NzgwODEzMiwic3ViIjoibWFpbHRvOmQua29wcml3YUBnbWFpbC5jb20ifQ.0amelbSf07_OmVJ6fjOdN3B8__lKl92_Cz1x9okHgiymzEPF5dt4Fsv3It2eQTVnXl8AEyNun7YtQcrwojdNdQ, Content-Encoding: aesgcm, Encryption: salt=P2Y5k9eB29SJbKrMsccRvQ, Crypto-Key: dh=BJPBR3B_QQBCTJoH4cjHfX-1_r85wQKTUPaMQFNNtD-sKGgzjLV7pOimEbLtAk-hgJpTGlv0MC2mUpEPHah1WHE=;p256ecdsa=BA4eDq9AC_vqPeCxEM_sfr6KQpDgPnJzW8cTZXlHRaxFrpxRyQwgW6Qk_yfgKblOQmYInisvFnFcgO33_NVj0TQ, Content-Type: application/octet-stream]

The response receive look like,

Chrome

HTTP/1.1 403 Forbidden [Content-Type: text/plain; charset=utf-8, X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, X-Xss-Protection: 0, Date: Tue, 18 Aug 2020 15:46:48 GMT, Content-Length: 194, Alt-Svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"]

FireFox

HTTP/1.1 401 Unauthorized [Access-Control-Allow-Headers: content-encoding,encryption,crypto-key,ttl,encryption-key,content-type,authorization, Access-Control-Allow-Methods: POST, Access-Control-Allow-Origin: *, Access-Control-Expose-Headers: location,www-authenticate, Content-Type: application/json, Date: Tue, 18 Aug 2020 15:46:47 GMT, Server: nginx, Strict-Transport-Security: max-age=31536000;includeSubDomains, Content-Length: 199, Connection: keep-alive]

This is how I register the client.

This is how I handle the server

Does anyone have a clue on where I am failing to have proper headers?

kopax commented 4 years ago

The client was using a wrong public key. Sorry about this.