WebPush.sendNotification Node.js giving 401 "header must be specified" error on googleapis endpoint - push-notification

I'm getting the following error:
WebPushError: Received unexpected response code
at IncomingMessage.<anonymous> (/Users/sepp/.../node_modules/web-push/src/web-push-lib.js:347:20)
at IncomingMessage.emit (node:events:406:35)
at endReadableNT (node:internal/streams/readable:1331:12)
at processTicksAndRejections (node:internal/process/task_queues:83:21) {
statusCode: 401,
headers: {
'content-type': 'text/plain; charset=utf-8',
'x-content-type-options': 'nosniff',
'x-frame-options': 'SAMEORIGIN',
'x-xss-protection': '0',
date: 'Wed, 01 Feb 2023 19:57:43 GMT',
'content-length': '40',
'alt-svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000',
connection: 'close'
},
body: 'authorization header must be specified.\n',
endpoint: 'https://fcm.googleapis.com/fcm/send/duj-etc-etc
The code involved is:
import * as webPush from "web-push";
const subDetails = {
endpoint: "https://fcm.googleapis.com/fcm/send/duja6etc-etc",
expirationTime: null,
keys: {
p256dh: "BHtwM-etc-etc",
auth: "aYkx0etc-etc"
}
}
await webPush.sendNotification(subDetails, "test message", );
I found this issue on Github, and there was some debilitation as to whether or not it has to do with the environment. I am running my front-end page and back-end server both locally. There is a 'x-frame-options': 'SAMEORIGIN' header in the response.
As you can see from the code above, I do not have VAPID set up.
If I use console.log(webPush.generateRequestDetails(pushSub.details, args.msg)) to see what the headers and body of the request are, I get the following details, which show that the auth header is not set:
{
method: 'POST',
headers: {
TTL: 2419200,
'Content-Length': 121,
'Content-Type': 'application/octet-stream',
'Content-Encoding': 'aes128gcm'
},
body: <Buffer ....>,
endpoint: 'https://fcm.googleapis.com/fcm/send/duj-etc-etc'
}
Questions
Are there any special requirements for localhost stuff?
What does it take for auth headers to be included?
EDIT: The browser I'm using is Opera GX. I did find a browser support table, which says that opera does not yet support push on desktop. The error still seems to imply something else may be the issue. Testing in Firefox Dev Edition, it works! Unfortunately, in Chrome the same exact error as Opera GX is given.

The issue is two-fold.
Issue #1: Opera GX does not support push notifications on desktop. Check this table for details on your browser.
Issue #2: For any push services which use a https://fcm.googleapis.com/fcm/send/ endpoint, you'll need auth headers. To create them, you'll need a VAPID. Here's how to set that up in web-push:
Create your public and private keys in command line (you many need to do ./node_modules/.bin/web-push instead):
$ web-push generate-vapid-keys --json
Store the private key somewhere safe only your server can get to it. Public key will be needed by both front and back end.
Update your code to generate auth headers and add them to the request
import * as webPush from "web-push";
const subDetails = {
endpoint: "https://fcm.googleapis.com/fcm/send/duja6etc-etc",
expirationTime: null,
keys: {
p256dh: "BHtwM-etc-etc",
auth: "aYkx0etc-etc"
}
}
const VAPID = {
publicKey: "lalalla-etc-etc-put-anywhere",
privateKey: "lCRVkwS-etc-etc-put-somewhere-safe"
}
const parsedUrl = new URL(subDetails.endpoint);
const audience = `${parsedUrl.protocol}//${parsedUrl.hostname}`;
// technically, the audience doesn't change between calls, so this can be cached in a non-minimal example
const vapidHeaders = webPush.getVapidHeaders(
audience,
'mailto: example#web-push-node.org',
VAPID.publicKey,
VAPID.privateKey,
'aes128gcm'
);
await webPush.sendNotification(subDetails, "test msg", {
headers: vapidHeaders
});
The code above should work fine in chrome and firefox. Let me know if this minimal example needs more for some other browser.

Related

How to generate billing portal link for Stripe NextJS with Firebase extension?

I'm using the Stripe extension in Firebase to create subscriptions in a NextJS web app.
My goal is to create a link for a returning user to edit their payments in Stripe without authenticating again (they are already auth in my web app and Firebase recognizes the auth).
I'm using the test mode of Stripe and I have a test customer and test products.
I've tried
The Firebase Stripe extension library does not have any function which can just return a billing portal link: https://github.com/stripe/stripe-firebase-extensions/blob/next/firestore-stripe-web-sdk/markdown/firestore-stripe-payments.md
Use the NextJS recommended import of Stripe foudn in this Vercel blog
First I setup the import for Stripe-JS: https://github.com/vercel/next.js/blob/758990dc06da4c2913f42fdfdacfe53e29e56593/examples/with-stripe-typescript/utils/get-stripejs.ts
export default function Settings() {
import stripe from "../../stripe_utils/get_stripejs"
async function editDashboard() {
const dashboardLink = await stripe.billingPortal.sessions.create({
customer: "cus_XXX",
})
}
console.log(dashboardLink.url)
return (
<Button
onClick={() => editDashboard()}>
DEBUG: See payments
</Button>
)
}
This would result in an error:
TypeError: Cannot read properties of undefined (reading 'sessions')
Use the stripe library. This seemed like the most promising solution but from what I read this is a backend library though I tried to use on the front end. There were no errors with this approach but I figure it hangs on the await
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET)
...
const session = await stripe.billingPortal.sessions.create({
customer: 'cus_XXX',
return_url: 'https://example.com/account',
})
console.log(session.url) // Does not reach here
Use a pre-made Stripe link to redirect but the user will have to authenticate on Stripe using their email (this works but I would rather have a short-lived link from Stripe)
<Button component={Link} to={"https://billing.stripe.com/p/login/XXX"}>
Edit payment info on Stripe
</Button>
Using POST HTTPS API call found at https://stripe.com/docs/api/authentication. Unlike the previous options, this optional will register a Stripe Dashboard Log event.
const response = await fetch("https://api.stripe.com/v1/billing_portal/sessions", {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json',
'Authorization': 'bearer sk_test_XXX',
'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *client
body: JSON.stringify(data), // body data type must match "Content-Type" header
})
The error is I'm missing some parameter parameter_missing -customer. So I'm closer to a resolution but I feel as if I should still be able to make the solution above work.
You should use Stripe library to create a billing portal session (your 2nd approach), and you might want to check your Dashboard logs and set the endpoint to /v1/billing_portal/sessions so that you can see if there are any errors during portal session creation.
Given my case, I chose to call the API itself instead of the libraries provided:
export default async function Stripe(payload, stripeEndpoint) {
const _ENDPOINTS = [
"/v1/billing_portal/sessions",
"/v1/customers",
]
let contentTypeHeader = "application/json"
let body = JSON.stringify(payload)
if _ENDPOINTS.includes(stripeEndpoint)) {
contentTypeHeader = "application/x-www-form-urlencoded"
body = Object.keys(payload).map(
entry => entry + "=" + payload[entry]).join("&")
}
try {
// Default options are marked with *
const stripeResponse = await fetch("https://api.stripe.com" + stripeEndpoint, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
headers: {
"Authorization": "bearer " + STRIPE_PRIVATE_KEY,
"Content-Type": contentTypeHeader,
},
redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *client
body: body, // body data type must match "Content-Type" header
})
return await stripeResponse.json()
} catch (err) {
console.error(err)
}
}

Can't do a post request using axios, returning unauthorised 401 error

I created an auth service from scratch using Redux, React and Node. Everything was working fine until I wire up my Post section o redux to my BackEnd. The redux part is ok I guess. My problem is when I send the Authorization Bearer token. I'm being able to post using insomnia. But when I try to post using the web app I can't.
This is my action:
export const createPost = ( formValues: any) => async(dispatch: any, getState: any) => {
const { userId } = getState().auth;
let token = userId
const headers = {
header: {
'Content-Type' : 'application/json',
'Accept' : 'application/json',
Authorization: `Bearer ${token}`
}
};
const response = await AlleSys.post('/posts', {...formValues, headers})
// dispatch({type: CREATE_POST, payload: response.data})
userId is my JWT token.
I already set up Cors on my backend
const corsOptions ={
origin:'http://localhost:3000',
credentials:true, //access-control-allow-credentials:true
optionSuccessStatus:200
}
app.use(cors(corsOptions))
On Insomnia. The same request on insomnia works fine.
On insomnia I'm using the same bearer token from my application, so the problem is not the JWT.
Querying an endpoint with GET, POST, PUT, DELETE from a Nodejs server or Insomnia will result in calling before checking the OPTIONS.
But browsers will limit the HTTP requests to be at the same domain which makes you run into CORS issues. Since Insomnia is not a browser and CORS is a browser security restriction only, it didn't get limited.
From docs for the CORS you are using:
Certain CORS requests are considered 'complex' and require an initial OPTIONS request (called the "pre-flight request"). An example of a 'complex' CORS request is one that uses an HTTP verb other than GET/HEAD/POST (such as DELETE) or that uses custom headers. To enable pre-flighting, you must add a new OPTIONS handler for the route you want to support:
So I think you should include app.options('*', cors()) before all routes and put it at the top of your file to be processed first.
I changed my code to:
export const createPost = ( formValues: any) => async(dispatch: any, getState: any) => {
const { userId } = getState().auth;
let token = userId
const headers = {
authorization: `Bearer ${token}`
};
const response = await AlleSys.post('/posts', {...formValues}, {headers})
And Worked!

Push notifications work on Chrome Desktop but not on PWA on Android Phone

If I visit my site on Desktop, add permission for notifications and then send a test push from server everything works perfectly.
Same steps on Android Phone (Chrome 58) leads to a notification of "This site has been updated in the background." No title, no body text.
Here's my sw.js:
self.addEventListener("push", function(event) {
if (event.data) {
const data = event.data.text().split('\t') // [0] = title , [1] = body
const options = {
body: data[1],
}
event.waitUntil(self.registration.showNotification(data[0], options))
} else {
console.log("Push event but no data")
}
})
Log on server (node.js using web-push):
{ statusCode: 201,
body: '',
headers:
{ location:
'https://fcm.googleapis.com/0:15...',
'x-content-type-options': 'nosniff',
'x-frame-options': 'SAMEORIGIN',
'x-xss-protection': '0',
date: 'Tue, 10 Sep 2019 18:13:39 GMT',
'content-length': '0',
'content-type': 'text/html; charset=UTF-8',
'alt-svc': 'quic=":443"; ma=2592000; v="46,43,39"',
connection: 'close' } }
"This site has been updated in the background." is displayed when service worker has received a push but has not displayed it.
In my code I have an else block which skips showing the notification. Turns out, that's exactly where the code is ending up as it's missing data.
See Missing push data on Android's PWA but not on Desktop

Network request failed on Android when using fetch to post to cloud functions

I am using the guide explained here
to upload images to firebase cloud storage using cloud function as a middle ware or at least that what I understood from the guide.
My issue is with the fetch request at the client side:
const body = new FormData();
body.append("picture", {
uri: imageUri,
name,
type: type
});
await fetch(`${cloudFunctionUrl}`, {
method: "POST",
body,
headers: {
Accept: "application/json",
"Content-Type": "multipart/form-data"
}
}).then(async() => {
// do things
}).catch(error => {
console.log(error);
reject(error);
});
On iOS it works like a charm but on Android I get a network request failed error.
The publisher of the guide above declared that the method works for both iOS and Android.
Some topics suggested that the problem is with the headers of the request when it comes to Android, but didn't specify exactly what is wrong.
The issue was passing type = "jpg" while it should be type= "image/jpg". Thanks to Kadi

"415 Error" when querying Spotify for tokens

I've been trying to recreate the spotify oauth connection in MeteorJS. I've gotten as far as requesting the access and refresh tokens, but I keep getting a 415 error now. Here is the relevant code:
var results = HTTP.post(
'https://accounts.spotify.com/api/token',
{
data: {
code: code,
redirect_uri: redirectURI,
grant_type: 'authorization_code',
client_id: clientID,
client_secret: clientSecret
},
headers: {
'Content-Type':'application/json'
}
}
);
I can't seem to find any other good documentation of the problem and the code in this demo:
https://github.com/spotify/web-api-auth-examples/tree/master/authorization_code
works perfectly.
I had a similar problem (but in Java). The analogous solution was
headers: {
'Content-Type':'application/x-www-form-urlencoded'
}
You need to use params instead of data when sending the JSON object. Related question: Unsupported grant type error when requesting access_token on Spotify API with Meteor HTTP
I have successfully tried getting the access token from Spotify, using the below function. As you can see, you don't need to specify Content-Type, but just need to use params instead of data (as far as axios is concerned). Also make sure that you first combine the client id and the client secret key with a ":" in between them and then convert the combined string into base 64.
let getAccessToken = () => {
let options = {
url: 'https://accounts.spotify.com/api/token',
method: 'POST',
headers: {
// 'Content-Type':'application/x-www-form-urlencoded',
'Authorization': `Basic <base64 encoded client_id:client_secret>`
},
params: {
grant_type: 'client_credentials'
}
}
axios(options)
.then((resp) => {
console.log('resp', resp.data)
})
.catch((err) => {
console.log('ERR GETTING SPOTIFY ACCESS TOKEN', err);
})
}
If youre doing this clientside its not working because you're not allowed to post to another domain from the client side because of the same origin policy.
If this is server-side I'd recommend using a pre-existing spotify api npm module instead of writing your own requests. There are plenty of spotify api implementations on npmjs.org.
Use arunoda's npm package for integrating npm packages in your meteor application

Resources