I'm fairly new to Firebase cloud functions and I've been trying to integrate Stripe into my RN project using FB but keep getting a bad request response. I'll see a few things when I try to run this:
Request is missing body.
Invalid request, unable to process.
Bad Request (400).
Does anyone know where I should go from here?
In my index.js file:
exports.payWithStripe = functions.https.onCall(async (data, response) => {
try {
const paymentIntent = await stripe.paymentIntents.create({
data: {
amount: 100,
currency: "usd",
payment_method_types: ["card"]
}
});
const clientSecret = paymentIntent.client_secret;
response.json({
clientSecret: clientSecret,
})
} catch (e) {
console.log(e.message);
response.json({ error: e.message });
}
})
On the app side:
const fetchPaymentIntentClientSecret = async () => {
const response = await fetch(`${FUNCTION_URL}/create-payment-intent`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const { clientSecret, error } = await response.json();
return { clientSecret, error };
}
Your Cloud Function is a Callable Cloud Function, as we can see from the code: functions.https.onCall(). As explained in the doc "HTTPS callable functions are similar but not identical to HTTP functions"
You should call it from your app by using the specific methods of the JS SDK, as explained here.
It is possible to call it via fetch (or any other library used to make XMLHttpRequests from the browser) but then you need to follow the Protocol specification for https.onCall which is not straightforward. So it is highly recommended to use the SDK.
Related
I have an .onCreate cloud function, that once triggered, creates a task and runs it.
If I simply run admin.messaging().sendToDevice(payload.tokens, payload.message); inside the exports.createScheduledNotification function, then it runs fine, but I wanted to utilise Cloud Tasks so that I can schedule the task a little further into the future.
However, even if the entire payload is inside the scheduledNotification function, it still doesn't run. I have a feeling that it's actually not being called at all. However, I've checked in the Google Cloud Tasks console, and it shows that it has run, but I get no response from the Firebase Messaging at all, so from that I can deduce it's not running.
Is there something wrong with how I'm sending the task request? Is the cloud function that receives the call written incorrectly?
Cloud Functions & Tasks Set up and Imports
'use strict';
const functions = require('firebase-functions');
var admin = require("firebase-admin");
const { CloudTasksClient } = require('#google-cloud/tasks')
var serviceAccount = require("./secret_file.json");
admin.initializeApp({
credential: admin.credential.cert(placeholder_for_account_credentials)
});
Function that is meant to create and send a Google Task, which is meant to call an .onRequest function, to send a Firebase Message
exports.createScheduledNotification = functions.firestore.document('/followers/{followedUid}')
.onCreate(async (snapshot) => {
const data = snapshot.data();
const tasksClient = new CloudTasksClient();
const queuePath = tasksClient.queuePath(project, location, queue);
const url = `https://{placeholder_for_location}-{placeholder_for_project_id}.cloudfunctions.net/scheduledNotification`;
const docPath = snapshot.ref.path;
const task = {
httpRequest: {
httpMethod: 'POST',
url,
body: Buffer.from(JSON.stringify({})).toString('base64'),
headers: {
'Content-Type': 'application/json',
},
},
scheduleTime: {
seconds: 10 + Date.now() / 1000
}
};
try {
await tasksClient.createTask({ parent: queuePath, task });
} catch (error) {
console.log(error);
}
});
Function to accept a call from Google Tasks, and then sends a message to a selected device_id
exports.scheduledNotification = functions.https.onRequest((req, res) => {
const payload = {
message: {
notification: {
title: 'You have a new follower!',
body: `Moe is now following you.`,
sound: 'default'
},
},
tokens: ["placeholder_for_device_id"]
};
admin.messaging().sendToDevice(payload.tokens, payload.message);
});
I would also like to mention that I saw in another question I needed to allow the principal firebase account to be able to create tasks, and I've added Cloud Task Admin and Cloud Task Enqeuer to every single account, and that the task are showing up in the Cloud Tasks Console.
I've been working on this for a while now and feel like I've read everything I can find but still can't get it to work. I'm trying to build a Firebase callable cloud function that uses axios to get a Spotify access token through client credentials auth flow and then uses that token to get data from my own account from the Spotify API. I'm using a chained function starting with axios.post and then axios.get.
The code works when it's getting the access token through axios.post but as soon as I chain an axios.get to use the token with the API something goes wrong. I'm new to Firebase and node.js so am not sure exactly how to catch the errors properly. The most common error is either a null result or a 'Unhandled error RangeError: Maximum call stack size exceeded' in the Firebase log... can't work out what either actually means for my code... With this particular version of my code I get a null result and a mass of around 50 different error logs in Firebase.
I've tried splitting the functions, using async and await and different arrangements of the headers but not a lot really changes. I've found similar questions but nothing that seemed to solve the issue. Any help would be amazing!
const functions = require("firebase-functions");
const axios = require('axios');
const qs = require('qs');
exports.spot = functions.https.onCall( async (data, context) => {
const client_id = //REMOVED;
const client_secret = //REMOVED;
const auth_token = Buffer.from(`${client_id}:${client_secret}`, 'utf-8').toString('base64');
const token_url = 'https://accounts.spotify.com/api/token';
const stringify_data = qs.stringify({'grant_type':'client_credentials'});
const api_url = 'https://api.spotify.com/v1/recommendations'
return axios
.post(token_url, stringify_data, {
headers: {
'Authorization': `Basic ${auth_token}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
form: {
grant_type: 'client_credentials'
},
json: true
})
.then(result => {
return axios.get(api_url, {
headers: {
'Authorization': `Bearer ${result.data.access_token}`,
}
})
})
.then(result => {
return result
})
.catch(error => {
console.log(error);
})
});
I have a question about auth0 and next js.
For example, I have the next code (this code works)
//initialprops enables server-side rendering in a page and allows you to do initial data population
ModelsList.getInitialProps = async (ctx) => {
//this is static token to test from auth0.com
const accessToken = 'eyJhbG.....'
//fetching data
const res = await fetch('http://localhost:7071/api/bo/getModels', {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
const json = await res.json()
return { data: json }
}
As you can see, I have accessToken variable as a text. It's a problem for me
How can make accessToken dynamic?
Thanks a lot!
P.S please, dont reference to auth0 documentation, I have tried a lot. Provide, please, a real solution/example.
Ok, so this is what worked for me.
Let's say you've got api.example.com/resources. This where data actually is. You will need to proxy via next's api.
Inside your jsx component, you fetch next's api.
// components/Dashboard.jsx
const API_URL = "api/resources";
async function fetcher(url: any) {
const res = await fetch(url);
const json = await res.json();
return json;
}
function Dashboard() {
const { data, error } = useSWR(API_URL, fetcher);
if (error) return <div>failed to load</div>;
if (!data) return <div>loading...</div>;
return <div>show your resources here</div>;
}
and now inside the next's api file you can fetch the actual endpoint you need.
// api/resources.js
import {
getAccessToken,
getSession,
withApiAuthRequired,
} from "#auth0/nextjs-auth0";
export default withApiAuthRequired(async function healthcheck(req, res) {
const session = await getSession(req, res);
const token = session?.idToken;
const response = await fetch("https://api.example.com/resources", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
res.status(200).json(data);
});
if you get errors, check the jwts you're getting. Audience or scope mismatch errors are usually the main culprits.
I created a service account email and added cloudfunctions.invoker role to the email so I can make sure only cloud tasks can trigger cloud functions, and I removed AllUsers role. But when cloud tasks tried to run cloud function, the status code is UNAUTHENTICATED(16): HTTP status code 401 and execution failed.
My current code and console is like this.
index.ts
export const addTasks = functions.https.onCall((data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'You are not authenticated.')
}
const client = new tasks.CloudTasksClient()
const projectId = functions.config().project.id
const queue = 'queue'
const location = functions.config().project.location
const parent = client.queuePath(projectId, location, queue)
const url = `https://${location}-${projectId}.cloudfunctions.net/executeSomething`
const serviceAccountEmail = functions.config().project.email
const task: tasks.protos.google.cloud.tasks.v2.ITask = {
httpRequest: {
httpMethod: 'POST',
url: url,
oidcToken: {
serviceAccountEmail: serviceAccountEmail,
},
},
scheduleTime: {
seconds: ...,
},
}
const request: tasks.protos.google.cloud.tasks.v2.ICreateTaskRequest = {
parent: parent,
task: task,
}
return client.createTask(request)
}
My cloud function's console
I added the cloud functions invoker role to the service account email.
My firebase project environment variables
When I added AllUsers role to cloud functions, it works as expected so I am sure I made a mistake when resrticting access. What am I missing?
Update:
My cloud tasks console
for me removing audience: new URL(url).origin, from oidcToken object inside task resolved the UNAUTHENTICATED(16): HTTP status code 401 issue.
const task = {
httpRequest: {
httpMethod: 'POST',
url,
oidcToken: {
serviceAccountEmail: email,
//audience: new URL(url).origin,
},
headers: {
'Content-Type': 'application/json',
},
body,
},
};
Your OIDC token seems broken against the specification
Simply provide the email, without the attribute name like in this example, or use the snake_case like described in the spec
Adding audience did the trick for me.
Here is how my task object look like -
const task = {
httpRequest: {
httpMethod: 'POST',
url,
headers: { 'Content-Type': 'application/json' },
oidcToken: {
serviceAccountEmail: sa,
audience: aud
},
},
};
nodejs doc
I want to test a a cloud function that creates users.
In normal cases, inside the browser i generate an idToken and i send it to server via headers: Authorization : Bearer etcIdToken
But I want to test this function without the browser. In my mocha tests i have:
before(done => {
firebase = require firebase.. -- this is suppose to be like the browser lib.
admin = require admin..
idToken = null;
uid = "AY8HrgYIeuQswolbLl53pjdJw8b2";
admin.auth()
.createCustomToken(uid) -- admin creates a customToken
.then(customToken => {
return firebase.auth() -- this is like browser code. customToken get's passed to the browser.
.signInWithCustomToken(customToken) -- browser signs in.
.then(signedInUser => firebase.auth() -- now i want to get an idToken. But this gives me an error.
.currentUser.getIdToken())
})
.then(idToken_ => {
idToken = idToken_
done();
})
.catch(err => done(err));
})
The error i'm getting is:
firebase.auth(...).currentUser.getIdToken is not a function - getting the idToken like this works on client - and is documented here.
I tried directly with signedInUser.getIdToken(). Same problem:
signedInUser.getIdToken is not a function - not documented. just a test.
I think this is because firebase object is not intended for node.js use like i'm doing here. When signing in - stuff get's saved in browser local storage - and maybe this is why.
But the question still remains. How can i get an idToken inside node.js in order to be able to test:
return chai.request(myFunctions.manageUsers)
.post("/create")
.set("Authorization", "Bearer " + idToken) --- i need the idToken here - like would be if i'm getting it from the browser.
.send({
displayName: "jony",
email: "jony#gmail.com",
password: "123456"
})
am I approaching this wrong? I know that if i can get the idToken it will work. Do i rely need the browser for this? Thanks :)
From Exchange custom token for an ID and refresh token, you can transform a custom token to an id token with the api. Hence, you just have to generate a custom token first from the uid, then transform it in a custom token. Here is my sample:
const admin = require('firebase-admin');
const config = require('config');
const rp = require('request-promise');
module.exports.getIdToken = async uid => {
const customToken = await admin.auth().createCustomToken(uid)
const res = await rp({
url: `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=${config.get('firebase.apiKey')}`,
method: 'POST',
body: {
token: customToken,
returnSecureToken: true
},
json: true,
});
return res.idToken;
};
L. Meyer's Answer Worked for me.
But, the rp npm package is deprecated and is no longer used.
Here is the modified working code using axios.
const axios = require('axios').default;
const admin = require('firebase-admin');
const FIREBASE_API_KEY = 'YOUR_API_KEY_FROM_FIREBASE_CONSOLE';
const createIdTokenfromCustomToken = async uid => {
try {
const customToken = await admin.auth().createCustomToken(uid);
const res = await axios({
url: `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=${FIREBASE_API_KEY}`,
method: 'post',
data: {
token: customToken,
returnSecureToken: true
},
json: true,
});
return res.data.idToken;
} catch (e) {
console.log(e);
}
}
curl 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=<FIREBASE_KEY>' -H 'Content-Type: application/json'--data-binary '{"email": "test#test.com","password":"test","returnSecureToken":true}'
If this curl doesn't run, try running the same thing on Postman. It works!