Make http cloud function executable only by the project owner - javascript

I am using http cloud function ( https://firebase.google.com/docs/functions/http-events ) to write documents to a firestore collection:
exports.hello = functions.https.onRequest(
(req: { query: { name: string } }, res: { send: (arg0: string) => void }) => {
console.log(req.query.name);
var name = req.query.name || 'unknown';
res.send('hello' + name);
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true });
}
);
this is a test. The problem is that, once you deploy and get the link to the function, it is executable by everyone. I would like instead that only I (project owner) can use it . Is it possible to do this?

One possible solution is to restrict your HTTPS Cloud Function to only a specific "Admin" user of your app.
There is an official Cloud Function sample which shows how to restrict an HTTPS Function to only the Firebase users of the app/Firebase project: Authorized HTTPS Endpoint sample.
You need to adapt it to check if the user is the Admin user. For example by checking the userID in the try/catch block at line 60 of the index.js file (untested).
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
if (decodedToken.uid !== "<the admin user uid>") {
throw new Error("Wrong user");
} else {
req.user = decodedIdToken;
next();
}
return;
} catch (error) {
functions.logger.error('Error while verifying Firebase ID token:', error);
res.status(403).send('Unauthorized');
return;
}
The two drawbacks of this approach are:
Your Admin user needs to be declared as a Firebase project user in the Authentication service
You hard code the Admin userID in your Cloud Function (you could use the Google Cloud Secret Manager service to securely store it as a configuration value, see the doc).
IMPORTANT SIDE NOTE:
In your Cloud Function you call the send() method before the asynchronous work is complete:
res.send('hello' + name);
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true });
By calling the send() method, you actually terminate the Cloud Function, indicating to the Cloud Functions instance running your function that it can shut down. Therefore in the majority of the cases the asynchronous set() operation will not be executed.
You need to do as follows:
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true })
.then(() => {
res.send('hello' + name);
})
I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series as well as read this page of the documentation which explain this key point.

Related

Authenticated firebase functions with custom OpenId Connect provider

I have implemented a custom oidc authentication provider with firebase. (Which was very easy!)
For reference, the oidc provider I implemented is for Xero (accounting app)
I want to implement authenticated httpsCallable functions that use the accessToken that is returned from the callback but I can't seem to access it in the firebase function.
Ultimately the getTokenSetFromDatabase function from this example is what I need to recreate somehow in firebase function:
https://github.com/XeroAPI/xero-node#accounting-api
The context.auth info in the firebase functions contains some authentication data but not any jwts or tokens.
export const getTenants = functions.https.onCall(async (data, context) => {
await xero.initialize()
// Can I get the token set somehow from the context
// Or do I need to save a users token in the firebase database when they login from the front end?
const tokenSet = getTokenSetFromDatabase(context.auth?.uid)
await xero.setTokenSet(tokenSet)
if (tokenSet.expired()) {
const validTokenSet = await xero.refreshToken()
// save the new tokenset
}
await xero.updateTenants()
const activeTenantId = xero.tenants[0].tenantId
return activeTenantId
})
The console log of context.auth.token is:
{
"name": "Jialx",
"iss": "https://securetoken.google.com/firebase-app-name",
"aud": "firebase-app-name",
"auth_time": 1658994364,
"user_id": "0000000000000000000000",
"sub": "0000000000000000000000",
"iat": 1659007170,
"exp": 1659010770,
"email": "example#email.com",
"email_verified": false,
"firebase": {
"identities": { "oidc.xero": [], "email": [] },
"sign_in_provider": "oidc.xero",
"sign_in_attributes": {
"at_hash": "xx-xxxx-xxxx",
"preferred_username": "example#gmail.com",
"sid": "000000000000000000000000000",
"global_session_id": "000000000000000000000000000",
"xero_userid": "000000000000000000000000000"
}
},
"uid": "0000000000000000000000"
}
Discovery
So i've stumbled across the blocking functions feature when beforeSignIn function can access these oAuth credentials; so I figure that this would be a great place to save them to the DB and retrieve them later (what its built for).
However this doesn't seem to work with my custom OIDC auth provider config:
It does work but its buggy (See answer for details)
Okay so bit of strange behaviour in implementing the blocking function, It either takes a bit of time to apply that change, or its critically important to select the function AND THEN click the checkbox's AND THEN click save...
Either way, I've come back after lunch and now can see my refresh, id, and access tokens inside of the beforeSignIn context parameter.
Further down the Rabbit hole
I deployed a new version of my beforeSignIn function and found that my Additional provider token credentials options had been automatically toggled off...
SUMMARY
For completeness, this is how i've implemented OIDC Authenticated firebase functions that enable me to make authenticated calls and use my providers services (In this case a users accounting data).
Setup OpenID Connect following this guide:
Setup Firebase Functions following this guide
Build out your firebase functions with the requirements you have, This should give you a good start:
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
import { TokenSetParameters, XeroClient } from 'xero-node'
admin.initializeApp()
const scope = 'offline_access openid profile email accounting.transactions'
const xero = new XeroClient({
clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
clientSecret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
redirectUris: ['https://somefirebaseapp.firebaseapp.com/__/auth/handler'],
scopes: scope.split(' '),
httpTimeout: 3000,
})
const getTokenSetFromDatabase = (uid: string) =>
new Promise<TokenSetParameters>(async (res, rej) => {
await admin
.database()
.ref('user/' + uid + '/tokenSet')
.once('value', (snapshot) => {
const val = snapshot.val()
res(val)
})
})
export const getTenants = functions.https.onCall(async (data, context) => {
await xero.initialize()
const uid = context.auth?.uid
if (!uid) return null
const tokenSet = await getTokenSetFromDatabase(uid)
xero.setTokenSet(tokenSet)
try {
const newTokenSet = await xero.refreshToken()
await admin
.database()
.ref('user/' + uid + '/tokenSet')
.set(newTokenSet)
} catch (error) {
console.log(error)
}
await xero.updateTenants()
return xero.tenants
})
export const beforeSignIn = functions.auth.user().beforeSignIn(async (user, context) => {
if (!context.credential) return
const { accessToken, expirationTime, idToken, refreshToken } = context.credential
if (!accessToken || !expirationTime || !idToken || !refreshToken) return
const tokenSet: TokenSetParameters = {
access_token: accessToken,
expires_at: new Date(expirationTime).valueOf(),
id_token: idToken,
refresh_token: refreshToken,
scope,
}
await admin
.database()
.ref('user/' + user.uid + '/tokenSet')
.set(tokenSet)
})
Deploy your firebase functions and link your beforeSignIn function to the authentication > settings > blocking functions.
Keep in mind that if you deploy your function again you will need to reenable this. (Or it might be something that gets fixed 🤞)
You can actually enable this in the code...
export const beforeCreate = functions.auth
.user({ blockingOptions: { accessToken: true, idToken: true, refreshToken: true } })
.beforeCreate(async (user, conte....
Hopefully this helps the next person.. My codes not bulletproof of course, missing all the error handling and logging etc but you get what you pay for.

check if the email is real one before signup in firebase

am working on a little project and i did finish all the authentication work but one thing,am wondering how to check if the email is real before going into the process of signup,
by the way am using react and Firebase and i did look online and i did find a package called email-existence i did try it and it dose return true if the email is real and false if the email dosent exist but thats not working when i use it with react it return an error
import firebase from '../util/firebase';
const emailExistence = require('email-existence');
export const normalSignup = (props, setSign, email, password, confirmPassword, username) => {
emailExistence.check(email, function (error, response) { // return error here addresses.sort is not a function
console.log('res: ' + response);
});
}
anyway am wondering if there's a way to do it with Firebase without external packages thanx in advance
PS:am not using cloud functions
Well assuming you want to check if the email is a verified email address you can write the code in the following way
import firebase from '../util/firebase';
const App = {
firebase: firebase,
getLoggedInUser: () => {
const currentUser = App.firebase.auth().currentUser
if (currentUser) {
return {
email: currentUser.email,
userId: currentUser.uid,
isEmailVerified: currentUser.emailVerified
}
} else {
return undefined
}
},
isAuthenticated: () => {
return (App.getLoggedInUser() && App.getLoggedInUser().isEmailVerified)
},
authenticate: async (email, password) => {
await App.firebase.auth().signInWithEmailAndPassword(email, password)
},
signup: async (email, password) => {
const userCredential = await App.firebase.auth().createUserWithEmailAndPassword(email, password)
await userCredential.user.sendEmailVerification()
return `Check your email for verification mail before logging in`
},
Here the following happens
When a user signs up the signup method is called and an email verification is sent by firebase as shown in the above code
When a user logs in the authenticate method is called so according to firebase you are logged in
However to redirect or render a certain page say after log in you can use the isAuthenticated method to display a page to a certain user
So you can pass method isAuthenticated as a prop to react-router and render your web application how you want.
This way only real and authentic email id which are verified will have access to your app
Note
This method is working already in prod but its using VueJS and is an opensource project on github let me know if you want to reference it
Maybe just use a regex to check if the email is valid?
According to this webpage for JavaScript you just need:
const emailRegex = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (emailRegex.test(email)) {
console.log('Email valid!');
}
This won't stop people entering emails for incorrect domains, but ensures that if someone uses a mail server that isn't widely known, it will get accepted too.
Your only option on the client side (if you are on Firebase I suppose you don't have the luxury to run a Node backend) to fetch a similar service as email-existence which returns a "valid" or "invalid" response if you GET the endpoint with the email address.
These are usually premium services, but if you have low traffic you can try out a free one. In my example it is Mailboxlayer.
Their endpoint can be called like this (and of course if you are stick to the client side it means anyone can steal your api key from production via browser network tab!):
GET http://apilayer.net/api/check?access_key=YOUR_ACCESS_KEY&email=richard#example.com
Which returns a JSON:
{
"email": "richard#example.com",
"did_you_mean": "",
"user": "support",
"domain": "apilayer.net",
"format_valid": true,
"mx_found": true,
"smtp_check": true,
"catch_all": false,
"role": true,
"disposable": false,
"free": false,
"score": 0.8
}
Best to use score, which:
[...] returns a numeric score between 0 and 1 reflecting the quality and deliverability of the requested email address.
In React:
const [data, setData] = useState(null)
const [emailToVerify, setEmailToVerify] = useState('richard#example.com') // just for the sake of example
const apiKey = process.env.API_KEY
const fetchEmailVerificationApi = useCallback(async () => {
try {
const response = await fetch(`http://apilayer.net/api/check?access_key=${apiKey}&email=${emailToVerify}`)
const json = await response.json()
setData(json.score) // returns a numeric score between 0 and 1 reflecting the quality and deliverability of the requested email address.
} catch (e) {
console.error(e)
}
}, [apiKey, emailToVerify])
useEffect(() => {
fetchEmailVerificationApi()
}, [fetchEmailVerificationApi])

Extract specific node value via Firebase Cloud functions

Ok so I'm going to start with some background (skip to MY ISSUE for tl;dr):
I have an application in development that passes data from a Google Sheet into a Firebase Realtime Database in the form of a 2d Array. The data layout of the Google sheet is as shown below:
This data is passed into a Firebase Realtime Database under the node masterSheet via an Apps Script function result shown below:
Which is used as the live database for my mobile web application I am developing using the Ionic Framework (preview below):
I have functions which deal with the setting of "Y" and "N" flags at the correct positions for the sub tasks of each job and a function which sets the overall job completion status flag to "Y" when all sub tasks are done working as intended.
I am trying to add in an automatic email service via Firebase Cloud Functions that sends off a "job completion notification" whenever a job's overall "Completed" status is set to "Y" (i.e the value at ref: 'masterSheet/0/1' is equal to "Y").
So far I have managed to get it to successfully send off the emails via a Firebase Cloud Function using nodemailer and the Firebase Admin SDK to all registered users of the Firebase app whenever a job's overall completed status is changed from an "N" to a "Y" via the onUpdate() method and the .ref() of the location to listen at.
Below is my Index.js file containing the cloud function I am using:
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp();
// The mail service used
const nodemailer = require('nodemailer');
// Cloud Fucntion to export:
exports.onMessageUpdate = functions.database.ref('/masterSheet/{subArray}/1')
.onUpdate((change) => {
var changeRef = change.after.ref.path;
console.log('changeRef: ' + changeRef);
var newVal = change.after.val();
if (newVal == "Y"){
getUsers();
}
})
// Fucntion to get all registers users of the Firebase Project
function getUsers(){
var userEmails = [];
admin.auth().listUsers()
.then(function(listUsersResult) {
listUsersResult.users.forEach(function(userRecord) {
console.log(userRecord);
userEmails.push(userRecord.email);
sendCompletionEmail(userRecord.email)
});
})
.catch(function(error) {
console.log("Error listing users:", error);
});
}
// Function to send automatic emails
function sendCompletionEmail(email){
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
type: 'OAuth2',
user: 'xxxxxxxx#gmail.com',
clientId: 'xxxxxxxx.apps.googleusercontent.com',
clientSecret: 'xxxxxxxxxxxxxxx',
refreshToken: 'xxxxxxxxxxxxxx'
}
})
// Email details:
var mailOptions = {
from: 'xxxxxxx',
to: email,
subject: 'Job completion notification',
text: 'This is an automated message to inform you that a job has been fully completed ' +
'with all of its tasks marked as done. \n\nYou can view this (along with any others) from the Completed ' +
'Jobs page within the app.'
}
transporter.sendMail(mailOptions, function (err, res) {
if(err){
console.log('Error');
} else {
console.log('Email Sent');
}
})
}
MY ISSUE:
I want to be able to include the job title in this automatic email that is sent.
logging the result of change.after.ref.path used in the snippet below:
// Cloud Fucntion to export:
exports.onMessageUpdate = functions.database.ref('/masterSheet/{subArray}/1')
.onUpdate((change) => {
var changeRef = change.after.ref.path;
console.log('changeRef: ' + changeRef);
var newVal = change.after.val();
if (newVal == "Y"){
getUsers();
}
})
Produces this log output:
which contains exactly what I want within it... But I don't know how to get it out...
How can I retrieve the second value from the changeRef variable so that I can pass this onto the sendCompletionEmail() function and use it to refer to the item at position [0] for that node?
something like:
var subArray = changeRef[1]
to get the value: 0 out of masterSheet/0/1
which i can store as a variable and use to refer to the job title of the job that has just been completed in the sent off email.
Thanks for any help!
If you're looking for the 0 from the request, that is available from the second parameter that is passed into your Cloud Function (but that you're not declaring).
exports.onMessageUpdate = functions.database.ref('/masterSheet/{subArray}/1')
.onUpdate((change, context) => {
console.log(context.params.subArray);
})
See the Firebase documentation on handling event data and the reference docs for onUpdate.

Is there any way to get the Cognito username in AWS Lambda?

As far as I can tell, there is no way for an AWS Lambda function to look up a username of my Cognito users (I am using UserPools).
This seems extremely strange as I would have thought that applications everywhere depends almost always on manipulating the username.
I have been successful in getting the Cognito IdentityId but I can't see any way to relate the IdentityId to anything that looks up the Cognito User that the IdentityId relates to.
Is there any way of getting the username? What is the relationship between IdentityId and username?
I struggled to find an answer to this problem for a while because there just aren't any concise responses on any of these threads online.
It sounds like you're trying to come up with an effective Authorization strategy after the user has Authenticated their credentials against your Cognito User Pool using custom attributes.
I created a library that I use to export a few functions that allow me to capture the UserPoolId and the Username for the authenticated user so that I can capture the custom:<attribute> I need within my lambda so that the conditions I have implemented can then consume the API to the remaining AWS Services I need to provide authorization to for each user that is authenticated by my app.
Here is My library:
import AWS from "aws-sdk";
// ensure correct AWS region is set
AWS.config.update({
region: "us-east-2"
});
// function will parse the user pool id from a string
export function parseUserPoolId(str) {
let regex = /[^[/]+(?=,)/g;
let match = regex.exec(str)[0].toString();
console.log("Here is the user pool id: ", match);
return match.toString();
}
// function will parse the username from a string
export function parseUserName(str) {
let regex = /[a-z,A-Z,0-9,-]+(?![^:]*:)/g;
let match = regex.exec(str)[0].toString();
console.log("Here is the username: ", match);
return match.toString();
}
// function retries UserAttributes array from cognito
export function getCustomUserAttributes(upid, un) {
// instantiate the cognito IdP
const cognito = new AWS.CognitoIdentityServiceProvider({
apiVersion: "2016-04-18"
});
const params = {
UserPoolId: upid,
Username: un
};
console.log("UserPoolId....: ", params.UserPoolId);
console.log("Username....: ", params.Username);
try {
const getUser = cognito.adminGetUser(params).promise();
console.log("GET USER....: ", getUser);
// return all of the attributes from cognito
return getUser;
} catch (err) {
console.log("ERROR in getCustomUserAttributes....: ", err.message);
return err;
}
}
With this library implemented it can now be used by any lambda you need to create an authorization strategy for.
Inside of your lambda, you need to import the library above (I have left out the import statements below, you will need to add those so you can access the exported functions), and you can implement their use as such::
export async function main(event, context) {
const upId = parseUserPoolId(
event.requestContext.identity.cognitoAuthenticationProvider
);
// Step 2 --> Get the UserName from the requestContext
const usrnm = parseUserName(
event.requestContext.identity.cognitoAuthenticationProvider
);
// Request body is passed to a json encoded string in
// the 'event.body'
const data = JSON.parse(event.body);
try {
// TODO: Make separate lambda for AUTHORIZATION
let res = await getCustomUserAttributes(upId, usrnm);
console.log("THIS IS THE custom:primaryAccountId: ", res.UserAttributes[4].Value);
console.log("THIS IS THE custom:ROLE: ", res.UserAttributes[3].Value);
console.log("THIS IS THE custom:userName: ", res.UserAttributes[1].Value);
const primaryAccountId = res.UserAttributes[4].Value;
} catch (err) {
// eslint-disable-next-line
console.log("This call failed to getattributes");
return failure({
status: false
});
}
}
The response from Cognito will provide an array with the custom attributes you need. Console.log the response from Cognito with console.log("THIS IS THE Cognito response: ", res.UserAttributes); and check the index numbers for the attributes you want in your CloudWatch logs and adjust the index needed with:
res.UserAttributes[n]
Now you have an authorization mechanism that you can use with different conditions within your lambda to permit the user to POST to DynamoDB, or use any other AWS Services from your app with the correct authorization for each authenticated user.
In the response that you can see in res.UserAttributes[n] you will see the attribute for sub which is what you are looking for.
You can get the JWT token from the Authorization header and then decode it with some library for your language.
In the payload of the JWT is the username.
Or you can call listUsers on CognitoIdentityServiceProvider (http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html#listUsers-property) with a filter of the sub that you get in {...}authorizer.claims.sub.
I got user details in lambda after adding Cognito Authorizer in Api gateway which gives decoded Authorization token passed in header in event.requestContext.authorizer.claims object.
elaborating on #doorstuck's answer, If you are using Lambda invoked by APIG with AWS_IAM Authorization. Then, you can get the username and other attributes as follows:
The event.requestContext.identity.cognitoAuthenticationProvider is a string that looks like
"cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxxx,cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxx:CognitoSignIn:SSSSSSSS"
The SSSSSSSS is the sub of the user in User Pool. You can easily decode the string to get the sub and use it in the filter of listUsers.
Example:
const provider =
event.requestContext.identity.cognitoAuthenticationProvider;
const sub=provider.split(':')[2];
const Params = {
UserPoolId: 'xxxxxxxxx', /* required */
Filter: "sub=\""+ sub + "\"",
Limit: 1
};
cognitoidentityserviceprovider.listUsers(Params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data.Users[0].Attributes);
});
The data includes useful information about the returned user where data.Users[0].Attributes has all your user attributes.
The result is
[ { Username: 'xxxxx',
Attributes: [Object],
UserCreateDate: 2017-09-12T04:52:50.589Z,
UserLastModifiedDate: 2017-10-24T01:50:00.109Z,
Enabled: true,
UserStatus: 'CONFIRMED' } ] }
data.Users[0].Attributes is
[ { Name: 'sub', Value: 'SSSSSSS' },
{ Name: 'address', Value: 'xxxxxxxxi' },
{ Name: 'email_verified', Value: 'true' },
..... ]
Note that you can also filter the returned attributes by using
AttributesToGet: [
'STRING_VALUE',
/* more items */
],
in Params.
If you front your Lambda function with API Gateway you can use the Cognito Authorizer to authenticate your User Pools tokens directly and pass in the username extracted from the token via $context.authorizer.claims.preferred_username
More details on this integration is here: http://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html
We can manipulate context in body mapping template also to get the sub (username) and it works fine for me. Try out this rather than splitting in you lamda function.
#set($sub = $context.identity.cognitoAuthenticationProvider.split(':')[2])
{
"tenantId": "$sub"
}

Parse Server / JS SDK, error 206 when saving a user object

I am having trouble using the Parse Server JS SDK to edit and save a user.
I am signing in, logging in and retrieving the user just fine, I can call without exception user.set and add/edit any field I want, but when I try to save, even when using the masterKey, I get Error 206: Can t modify user <id>.
I also have tried to use save to direcly set the fields, same result.
A interesting thing is that in the DB, the User's Schema get updated with the new fields and types.
Here is my update function:
function login(user, callback) {
let username = user.email,
password = user.password;
Parse.User.logIn(username, password).then(
(user) => {
if(!user) {
callback('No user found');
} else {
callback(null, user);
}
},
(error) => {
callback(error.message, null);
}
);
}
function update(user, callback) {
login(user, (error, user) => {
if(error) {
callback('Can t find user');
} else {
console.log('save');
console.log('Session token: ' + user.getSessionToken());
console.log('Master key: ' + Parse.masterKey);
user.set('user', 'set');
user.save({key: 'test'}, {useMasterKey: true}).then(
(test) => {
console.log('OK - ' + test);
callback();
}, (err) => {
console.log('ERR - ' + require('util').inspect(err));
callback(error.message);
}
);
}
});
}
And a exemple of the error:
update
save
Session token: r:c29b35a48d144f146838638f6cbed091
Master key: <my master key>
ERR- ParseError { code: 206, message: 'cannot modify user NPubttVAYv' }
How can I save correctly my edited user?
I had the exact same problem when using Parse Server with migrated data from an existing app.
The app was created before March 2015 when the new Enhanced Sessions was introduced. The app was still using legacy session tokens and the migration to the new revocable sessions system was never made. Parse Server requires revocable sessions tokens and will fail when encountering legacy session tokens.
In the app settings panel, the Require revocable sessions setting was not enabled before the migration and users sessions were not migrated to the new system when switching to Parse Server. The result when trying to edit a user was a 400 Bad Request with the message cannot modify user xxxxx (Code: 206).
To fix the issue, I followed the Session Migration Tutorial provided by Parse which explain how to upgrade from legacy session tokens to revocable sessions. Multiple methods are described depending on your needs like enableRevocableSession() to enable these sessions on a mobile app, if you're only having a web app, you can enforce that any API requests with a legacy session token to return an invalid session token error, etc.
You should also check if you're handling invalid session token error correctly during the migration to prompt the user to login again and therefore obtain a new session token.
I had the same error and neither useMasterKey nor sessionToken worked for me either. :(
Here's my code:
console.log("### attempt 1 sessionToken: " + request.user.getSessionToken());
var p1 = plan.save();
var p2 = request.user.save(null, {sessionToken: request.user.getSessionToken()});
return Parse.Promise.when([p1, p2]).then(function(savedPlan) {
...
}
I see the matching session token in log output:
2016-08-21T00:19:03.318662+00:00 app[web.1]: ### attempt 1 sessionToken: r:506deaeecf8a0299c9a4678ccac47126
my user object has the correct ACL values:
"ACL":{"*":{"read":true},"PC7AuAVDLY":{"read":true,"write":true}}
I also see a bunch of beforeSave and afterSave logs with user being "undefined". not sure whether that's related.
beforeSave triggered for _User for user undefined:
I'm running latest parser-server version 2.2.18 on Heroku (tried it on AWS and results are the same)
function login(logInfo, callback) {
let username = logInfo.email,
password = logInfo.password;
Parse.User.logIn(username, password).then(
(user) => {
if(!user) {
callback('No user found');
} else {
callback(null, user);
}
},
(error) => {
callback(error.message, null);
}
);
}
function update(userInfo, data, callback) {
login(userInfo, (error, user) => {
if(error) {
callback('Can t find user');
} else {
getUpdatedData(user.get('data'), data, (error, updateData) => {
if(error) {
callback(error);
} else {
user.save({data: updateData}, /*{useMasterKey: true}*/ {sessionToken: user.get("sessionToken")}).then(
(test) => {
callback();
}, (err) => {
callback(error.message);
}
);
}
});
}
});
}
For some reason, retrying to use sessionToken worked.
This is not how asynchronous functions work in JavaScript. When createUser returns, the user has not yet been created. Calling user.save kicks off the save process, but it isn't finished until the success or error callback has been executed. You should have createUser take another callback as an argument, and call it from the user.save success callback.
Also, you can't create a user with save. You need to use Parse.User.signUp.
The function returns long before success or error is called.

Categories