access a variable from callback function NodeJS - javascript

Here is a simplified code of my payment service in node.js:
async function pay({ package, via }) {
const options = {
api: 'test',
factorNumber: '123456789',
description: `package-${package}`,
redirect: 'http://localhost:4000/accounts/pay/callback', // this will hit router.get('/pay/callback', payCallback);
}
const response = await axios.post('https://pay.ir/pg/send', options, {
headers: {'content-type': 'text/json'}
});
return { redirect: `https://pay.ir/pg/${response.data.token}` };
}
// this function is executed when we redirect to http://localhost:4000/accounts/pay/callback as I explained above
async function payCallback(req, res) {
// how can I access 'via' here
}
As you see in the first function named pay I have access to via variable which is the email or phone number of the user who wants to pay, ok?
The payment API I'm using just allow the options in the pay function to be accessible from the payCallback (this one is a function which fires at successful payment).
But I need to know who paid and check the database to insert the new payment for the user right?
So I need to access via inside the payCallback...
How can I access via inside payCallback function?

I assume the required data would be sent by your payment API in the payCallback's request body. Check their documentation.
If that isn't actually the case, you could insert the pending payment into a database with a unique ID, then add that unique ID to your redirect url:
async function pay({ package, via }) {
const paymentId = insertIntoDatabase(...);
const options = {
api: 'test',
factorNumber: '123456789',
description: `package-${package}`,
redirect: `http://localhost:4000/accounts/pay/callback?paymentId=${paymentId}`,
}
const response = await axios.post('https://pay.ir/pg/send', options, {
headers: { 'content-type': 'text/json' }
});
return { redirect: `https://pay.ir/pg/${response.data.token}` };
}
async function payCallback(req, res) {
const { paymentId } = req.query;
if (!paymentId) {
// Unexpected, log an error or so. Tell customer to contact customer service
return;
}
const paymentInfo = getFromDatabase(paymentId);
if (!paymentInfo) {
// Also unexpected, so again log the error and tell the customer to contact you
return;
}
// Do whatever with paymentInfo
}
If strictly speaking you only need via, you could add that as a query parameter instead of working with a database. But when it comes to payments, having some logging is a good idea anyway.

Related

How can I locally test cloud function invocation from third-party in Firebase?

I am developing an API for a third-party application not related to Firebase. This API consist of cloud functions to create and add users to database, retrieve user information and so on. These functions are created using the admin SDK. Example of a function that adds a user looks like this:
export const getUser = functions.https.onRequest(async (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.set('Access-Control-Max-Age', '3600');
res.status(204).send('');
} else {
const utils = ethers.utils;
const method = req.method;
const body = req.body;
const address = body.address;
const userAddress = utils.getAddress(address);
let logging = "received address: " + address + " checksum address: " + userAddress;
let success = false;
const db = admin.firestore();
const collectionRef = db.collection('users');
// Count all matching documents
const query = collectionRef.where("userAddress", "==", userAddress);
const snapshot = await query.get();
// If no documents match, there is no matching user
console.log(snapshot.docs.length);
if (snapshot.docs.length != 1) {
logging += "User does not exist in database.";
res.send({success: success, logging: logging});
return;
}
const data = snapshot.docs[0].data();
if (data != undefined) {
const createdAt = data.createdAt;
const emailAddress = data.emailAddress;
const userAddress = data.userAddress;
const updatedAt = data.updatedAt;
const userName = data.userName;
success = true;
res.send({success: success, createdAt: createdAt, emailAddress: emailAddress, userAddress: userAddress, updatedAt: updatedAt, userName: userName, logging: logging});
}
}
});
NOTE: These functions are NOT going to be called by the third-party application users, only by the third-party application itself.
I am pretty new at programming so I understand that this may not be the best way to code this functionality and I'm greatful for any tips you might have here as well. Anyway, back to my question. I'm trying to mimic the way that my customer is going to invoke these functions. So to test it, I'm using the following code:
function runGetUser() {
// test values
const address = 'myMetaMaskWalletAddress';
axios({
method: 'POST',
url: 'http://127.0.0.1:5001/cloud-functions/us-central1/user-getUser',
data: { "address": address },
}).then((response) => {
console.log(response.data);
}).catch((error) => {
console.log(error);
});
};
This works fine. However, I do not want anyone to be able to invoke these functions when I actually deploy them later. So I have been reading Firebase docs and googling on how to setup proper authentication and authorization measures. What I have found is setting up a service account and using gcloud CLI to download credentials and then invoke the functions with these credentials set. Is there not a way that I could configure this so that I query my API for an authorization token (from the file where the axios request is) that I then put in the axios request and then invoke the function with this? How do I do this in that case? Right now also, since I'm testing locally, on the "cloud function server-side" as you can see in my cloud function example, I'm allowing all requests. How do I filter here so that only the axios request with the proper authorization token/(header?) is authorized to invoke this function?
Thank you for taking the time to read this. Best regards,
Aliz
I tried following the instructions on this page: https://cloud.google.com/functions/docs/securing/authenticating#gcloud where I tried to just invoke the functions from the Gcloud CLI. I followed the instructions and ran the command "gcloud auth login --update-adc", and got the response: "Application default credentials (ADC) were updated." Then I tried to invoke a function I have "helloWorld" to just see that it works with the following command: curl -H "Authorization: bearer $(gcloud auth print-identity-token)" \http://127.0.0.1:5001/cloud-functions/us-central1/helloWorld", and I got the following response: "curl: (3) URL using bad/illegal format or missing URL". So I don't know what to do more.

Different headers used in Axios patch

I spent an hour looking in the Chrome console and I cannot see where this bug comes from.
I am finishing an update of OAuth implementation in my Vue app.
The story begins when socialLink.js finds out that a new user must be created. Vue component Vue-authentication depends on the presence of access_token in a response so I return some dummy text:
return api.sendResponse(res, { email, name, socialId, access_token: 'abcd' });
The library stores this value in localStorage:
After a redirect, the SignUp.vue is rendered and I complete the form. The first communication with the server is a Vuex call to create a new user:
response = await this.$store.dispatch('CREATE_USER_PROFILE', payload);
Which returns a real short lived JWT token:
const token = auth.createToken(userId, nickname, new Date(), null, false, '1m');
return api.sendCreated(res, api.createResponse(token));
Which I store in the Vue page afterwards:
const { data } = response;
const token = data.data;
if (token === undefined) {
this.error = this.$t('sign-up.something-went-wrong');
return false;
}
I checked that the token contains what the server returned:
Request URL: https://beta.mezinamiridici.cz/api/v1/users
Request Method: POST
Status Code: 201 Created
{"success":true,"data":"eyJhbGciOiJIUzI1NiIs...Tl8JFw2HZ3VMXJk"}
Then I call another Vuex method and pass the current JWT token:
await this.$store.dispatch('UPDATE_USER_PROFILE', {
I checked in the Vuex devtools that there really is the correct JWT token. I then pass it further to api.js.
Here I create an Axios configuration holding an Authorization header:
function getAuthHeader(context, jwt = undefined, upload) {
const config = { headers: { } };
if (jwt || (context && context.rootState.users.userToken)) {
config.headers.Authorization = `bearer ${jwt || context.rootState.users.userToken}`;
}
Again, I checked that the correct JWT token is used there.
Finally, I pass all data to Axios:
function patch(endpoint, url, body, context, jwt) {
const headers = getAuthHeader(context, jwt);
console.log(headers);
if (endpoint === 'BFF') {
return axios.patch(`${VUE_APP_BFF_ENDPOINT}${url}`, body, headers);
} else {
return axios.patch(`${VUE_APP_API_ENDPOINT}${url}`, body, headers);
}
}
Which I log and can confirm the correct JWT is still there:
bearer eyJhbGciOiJIUzI1N....8JFw2HZ3VMXJk
There is nothing that could change the header now to abcd, but, the 'Network' tab shows it:
And the server fails with a parse error.
Has anybody got an idea why Axios uses the Authorization header with a different value than I pass it?
Ok, mystery solved. vue-authenticate is the reason, because, it creates Axios interceptors and handles the Authorization header itself.
vue-authenticate.common.js:
var defaultOptions = {
bindRequestInterceptor: function ($auth) {
var tokenHeader = $auth.options.tokenHeader;
$auth.$http.interceptors.request.use(function (config) {
if ($auth.isAuthenticated()) {
config.headers[tokenHeader] = [
$auth.options.tokenType, $auth.getToken()
].join(' ');
} else {
delete config.headers[tokenHeader];
}
return config
});
},
My code is more complex and it supports internal accounts with email/password so this code is breaking mine. The interceptor must be present and be a function, so the solution was:
Vue.use(VueAuthenticate, {
tokenName: 'jwt',
baseUrl: process.env.VUE_APP_API_ENDPOINT,
storageType: 'localStorage',
bindRequestInterceptor() {},
bindResponseInterceptor() {},
providers: {
facebook: {
clientId: process.env.VUE_APP_FACEBOOK_CLIENT_ID,
redirectUri: process.env.VUE_APP_FACEBOOK_REDIRECT_URI,
},

Where and how to change session user object after signing in?

I have weird problem and I don't know where the problem is. I use next-auth library to make authentication system in my Next.js app.
Everything is OK - I can sign in by checking if there is account in google firebase with submitted credentials, session is being created properly, but when the authorize callback is initialized I pass data received from google firestore after correct sign in. User object contains whole data and it's passed forward.
Then, when I want to read data from session, some data I passed before is missing.
Code:
/pages/api/auth/[...nextauth.js]
export default (req, res) =>
NextAuth(req, res, {
providers: [
Providers.Credentials({
name: 'Credentials',
credentials: {
phone: { label: "Phone number", type: "text" },
password: { label: "Password", type: "password" }
},
authorize: async (loginData) => {
const { csrfToken, phone, password } = loginData;
// checking if there is account with these credentials
let res = await login({
phone,
password: sha1(md5(password)).toString()
})
// 200 = OK
if(res.status == 200){
// collect account data
const user = {
phone,
...res.data.info
}
// user object is created correctly, whole data is stored there
console.log('received account data from firestore', user)
return Promise.resolve(user);
}
else {
// wrong credentials
return Promise.resolve(null);
}
}
})
],
callbacks: {
session: async (session, user) => {
console.log('data passed to object when signed in', user)
// user object there doesn't have all data passed before
return Promise.resolve(session)
}
},
debug: false
})
Console logged objects:
received account data from firestore
{
phone: '123123123',
id: 'w2zh88BZzSv5BJeXZeZX',
email: 'jan#gmail.com',
name: 'Jan',
surname: 'Kowalski'
}
data passed to object when signed in
{
name: 'Jan',
email: 'jan#gmail.com',
iat: 1603900133,
exp: 1606492133
}
The best thing is, the object (above) always has the same properties. I can pass any object in authorize callback, but in session, user object always has "name, email, iat, exp" ALWAYS. The only thing that changes are values of these two properties in object (name, email). (rest properties - "phone, id, surname" - are missing).
Below there is console logged session object in any react component:
import {
signIn,
signOut,
useSession
} from 'next-auth/client'
const [ session, loading ] = useSession();
console.log(session)
Photo of console logged session object
What can I do? Do I have to receive data from firestore separately in session callback? Is the server-side rendering of Next.js causing the problem?
I resolved that problem by myself.
This issue thread helped me a lot!
https://github.com/nextauthjs/next-auth/issues/764
Below is the explanation:
callbacks: {
jwt: async (token, user, account, profile, isNewUser) => {
// "user" parameter is the object received from "authorize"
// "token" is being send below to "session" callback...
// ...so we set "user" param of "token" to object from "authorize"...
// ...and return it...
user && (token.user = user);
return Promise.resolve(token) // ...here
},
session: async (session, user, sessionToken) => {
// "session" is current session object
// below we set "user" param of "session" to value received from "jwt" callback
session.user = user.user;
return Promise.resolve(session)
}
}
EDIT: Due to NextAuth update to v4
Version 4 of NextAuth brings some changes to callbacks shown above. Now there is only one argument assigned to jwt and session functions. However, you can destructure it to separate variables. Rest of the code stays the same as before.
https://next-auth.js.org/configuration/callbacks#jwt-callback
https://next-auth.js.org/configuration/callbacks#session-callback
// api/auth/[...nextauth].js
...
callbacks: {
jwt: async ({ token, user }) => {
user && (token.user = user)
return token
},
session: async ({ session, token }) => {
session.user = token.user
return session
}
}
...
I had this problem too, and it turned out to be the functions in the Adapter that caused the problem. In my case, I wanted to use v4 of next-auth, and I wanted to use DynamoDB. As there is no official v4 adapter for Dynamo, I had to write my own, basing it on the publicly available v3 adapter.
One of the functions you have to provide when creating your own adapter is:
async updateUser(user) {
// use ddb client to update the user record
// client returns `data`
return { ...user, ...data.Attributes }
}
It turns out that the data in data.Attributes is what gets passed to your jwt() callback as user. This seems to be different to the v3 implementation.
Therefore in my case I had to structure the dynamodb adapter's updateUser() function to instruct the client to return ALL_NEW and not merely UPDATED_NEW (which is what the v3 adapter does).
My complete updateUser() function is as follows - bear in mind that at this point, I used the recommended dynamodb table structure (which I don't necessarily agree with, especially in my use case, but that's another story)
async updateUser(user) {
const now = new Date()
const data = await client.update({
TableName: tableName,
Key: {
pk: `USER#${user.id}`,
sk: `USER#${user.id}`,
},
UpdateExpression:
"set #emailVerified = :emailVerified, #updatedAt = :updatedAt",
ExpressionAttributeNames: {
"#emailVerified": "emailVerified",
"#updatedAt": "updatedAt",
},
ExpressionAttributeValues: {
":emailVerified": user.emailVerified?.toISOString() ?? null,
":updatedAt": now.toISOString(),
},
ReturnValues: "ALL_NEW",
}).promise()
return { ...user, ...data.Attributes }
},
As a result, I see user fully populated in the jwt() callback:
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
console.debug("callback jwt user:", user)
user && (token.user = user)
return token
},
The debug output is the complete record for the user from DynamoDB, and can be used as described in #MateuszWawrzynski's answer.

Asynchronously authenticate before request

So I have an API and I am trying to authenticate by hitting an endpoint with credentials (this part I've gotten working) and then save the received token and use it in all subsequent requests.
My problem is that the authenticate() method is asynchronous, but all other request methods like get() need the token from the authenticate() method. So I can't just export my get() method because the export is synchronous (as I've read) and it will be exported before authentication happens. I could authenticate for every request but that seems wasteful and inefficient.
I am not sure what to do here, I'm using axios, what's the proper way of doing this?
Edit
I'll be a bit more specific here. I have created an axios instance:
var instance = axios.create({
baseURL: `http://${config.server}:${config.port}`,
timeout: 1000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
I want to get the authentication token, and include it in the instance header:
async function authenticate(instance) {
const result = await instance.post(
'/session',
{
'username': config.username,
'password': config.password
}
)
instance['X-Token'] = result.data.token
}
Now I want to export that instance to be used in other files
You can use async/await. This is semi-pseudocode:
async function doStuff() {
const result = await axios.authenticate();
const token = // extract token from whatever format of result is
const data = await axios.get(/* supply token to get */);
}
Alternatively, you can just use then:
function doStuff(token) {
const token = // extract token from whatever format of result is
const data = await axios.get(/* supply token to get */);
}
axios.authenticate().then(result => {
const token = // extract token from whatever format of result is
doStuff(token);
}
With Axios you have the ability to set default values for all requests.
So for just a single axios instance you can do...
async function authenticate(instance) {
const result = await instance.post(
'/session',
{
'username': config.username,
'password': config.password
}
)
instance.defaults.headers.common['X-Token'] = result.data.token;
}
Alternatively, (which it sounds like you want to do) you can add it for the default Axios export. Then all requests will automatically have the header.
async function authenticate(endpoint, username, password) {
const res = await axios.post(`${endpoint}/session`, { username, password });
axios.defaults.headers.common['X-Token'] = result.data.token;
}
Then you don't have to worry about passing around an instance between all parts of your app and can just use import * as axios from 'axios' and have the header set.
Axios also provides and extremely helpful function called interceptors which you can use to inspect a request prior to making it. You can use to check to make sure that the request has the auth header and if it doesn't you can perform that logic. I came up with this and it seems to work well!
axios.interceptors.request.use(async (config) => {
// request intercepted, check (1) the header is missing and (2) that the intercepted request isn't authorizing
if (!config.headers.common['X-Token'] && config.authorizing !== true) {
const { endpoint, username, password } = appConfig;
// make a request to get your token AND pass our custom config
const result = await axios.post(`${endpoint}/session`, { username, password }, { authorizing: true });
// update axios to include the header for future requests
axios.defaults.headers.common['X-Token'] = result.data.token;
}
return config;
});
Two things that you'll want to note -- not only do I check for the existence of your X-token header I also check for a new authorization value in the config. You want to check for that config value, because we are going to use it as a flag to let the interceptor know if it should skip a request. If you don't do this, the authorization request will trigger another authorization request and infinite loop.

FB Graph API: Grabbing all mutual friends - (#200) You do not have sufficient permissions to perform this action

I am using AWS Lambda to ask for all mutual friends between current user and another user as stated here: https://developers.facebook.com/docs/graph-api/reference/user-context/all_mutual_friends/
I have looked through all the other stackoverflow answers and can't really find one close enough and have followed each of their answers to still get errors like these:
Facebook Graph All Mutual Friends
and
"Requires a valid user is specified ..." error when using getCount
and my code looks like the below in AWS, note that I have used this node.js third-party sdk (https://github.com/node-facebook/facebook-node-sdk):
// Imports third-party fb graph api node.js sdk: https://github.com/node-facebook/facebook-node-sdk
const fbGraph = require('fb');
// Grabs app secret and app ID
const appSecret = process.env.APP_SECRET;
const appID = process.env.APP_ID;
// Uses user access token and grabs total count of all relevant mutual friends
exports.handler = (event, context, callback) => {
// Grabs relevant details from request
// const accessToken = event.accessToken;
const friendUserID = event.friendUserID;
fbGraph.api('oauth/access_token', {
client_id: appID,
client_secret: appSecret,
grant_type: 'client_credentials'
}, function (res) {
if(!res || res.error) {
console.log(!res ? 'error occurred' : res.error);
return;
}
let access_token = res.access_token;
// Sets options for fb graph api to work
fbGraph.options({accessToken: access_token, appId: appID, appSecret: appSecret, timeout: 12000});
// Sets up fields and query string for request
const friendUserIDQueryString = '/' + friendUserID;
const fields = {
fields: 'context.fields(all_mutual_friends.limit(10))'
};
// Sends api request to get all mutual friends
fbGraph.api(friendUserIDQueryString, 'post', fields, function(response) {
console.log(response);
// Error handling
if (response.error) {
// Logs error
context.done(null, { MutualFriendsTotalCountError: 'Error: not able to receive response' });
// Returns error message
return callback(new Error(`Mutual friends total count error: ${response.error}`));
}
// Logs successful operation
context.done(null, { MutualFriendsTotalCountSuccess: `Success: grabbed total mutual friend count for ${friendUserID}`});
// Returns total count of mutual friends between current and other user as integer
return callback(null, { total_count: response.context.all_mutual_friends.summary.total_count });
});
});
}
but my response is now this:
{ error:
{ message: '(#200) You do not have sufficient permissions to perform this action',
type: 'OAuthException',
code: 200,
...
}
I don't really understand it, I've used my own access token on my IOS app I'm using this for, the application app token through OAuth, and also the other user ID who exists on my app and is the one thats specific for my app and other recommended solutions to no avail.
Ok now it works, I followed exactly with the example from this stackoverflow:
Facebook Graph All Mutual Friends
I don't know whats explicitly the difference as it still looks similar to something I had before but this is what my current code looks like and works:
// Imports third-party fb graph api node.js sdk: https://github.com/node-facebook/facebook-node-sdk
const fbGraph = require('fb');
// Grabs app secret and app ID
const appSecret = process.env.APP_SECRET;
const appID = process.env.APP_ID;
// Uses user access token and grabs total count of all relevant mutual friends
exports.handler = (event, context, callback) => {
// Grabs relevant details from request
const accessToken = event.accessToken;
const friendUserID = event.friendUserID;
// Sets options for fb graph api to work
fbGraph.options({accessToken: accessToken, appId: appID, appSecret: appSecret, timeout: 12000});
// Sends api request to get all mutual friends
fbGraph.api('/' + friendUserID, { fields: 'context.fields(all_mutual_friends.limit(0))'}, function(response) {
// Error handling
if (!response || response.error) {
// Logs error
console.log('MutualFriendsTotalCountError: Error: not able to receive response');
// Returns error message
return callback(new Error(`Mutual friends total count error: ${response.error.message}`));
}
// Logs successful operation
console.log(`Success: grabbed total mutual friend count for ${friendUserID}`);
// Returns total count of mutual friends between current and other user as integer
return callback(null, { total_count: response.context.all_mutual_friends.summary.total_count });
});
}
By the way, this returns the total number of app-using and non-app using mutual friends between current user and another user on AWS Lambda

Categories

Resources