Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error 206 cannot modify user for new Facebook user #6511

Open
ashish-naik opened this issue Mar 15, 2020 · 53 comments · May be fixed by #8740
Open

Error 206 cannot modify user for new Facebook user #6511

ashish-naik opened this issue Mar 15, 2020 · 53 comments · May be fixed by #8740

Comments

@ashish-naik
Copy link

ashish-naik commented Mar 15, 2020

Hello

I am getting error 206 after new Facebook user is saved and reason seems to be nil sessionToken.
I am facing this for Apple sign in also randomly. This is probably getting fixed by #6416. Don't know whether Facebook issue will also be fixed.

I have one old account created on Heroku Parse server which works fine ie i am able to save attributes after login.

I was running server v3.9.0. tried upgrading till 4.0.2 but still failing.

I am updating PFUser object post login locally in iOS client using Swift. Not using cloud code for this.

Also failing on back4app v3.9.0. Sign in with Apple also failing on back4app but works randomly.

Works on local setup consistently. Local server installed using bootstrap.sh script.

I have seen some issued dated 2016 fixing this error but server code seems to have changed lot so cannot view the same code referred in #952

using FBSDKCoreKit 5.15.1, FBSDKLoginKit 5.15.1

Steps to reproduce

on iOS, Try login using a facebook account that is not yet created on Parse.
Running iOS 13.3.1

Expected Results

After successful login, should be able to save other attributes in PFUser.current() object

Actual Outcome

The login is successful but saving any attributes fails with error 206 "cannot modify user xxxxx"
Fails on back4app also. v3.9.0
Both Apple and Facebook new user registration and update work fine in local environment.

Environment Setup

  • Server

    • parse-server version (Be specific! Don't say 'latest'.) : 3.9.0 to 4.0.2 (Local 3.10.0)
    • Operating System: heroku-18 (Local MacOS 10.15.3)
    • Hardware: heroku-18 (Local MacOS 10.15.3)
    • Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): Both as mentioned above
  • Database

    • MongoDB version: mLab 3.6.12 (Local 4.2.3)
    • Storage engine: mLab
    • Hardware: mLab
    • Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): both

Logs/Trace

Mar 15 02:51:40 yovl app/web.1 error: Parse error: Cannot modify user kc30Xbk1wA. {"code":206,"stack":"Error: Cannot modify user kc30Xbk1wA.\n at RestWrite.runDatabaseOperation (/app/node_modules/parse-server/lib/RestWrite.js:1170:11)\n at /app/node_modules/parse-server/lib/RestWrite.js:127:17\n at processTicksAndRejections (internal/process/task_queues.js:97:5)"}

package.json used for deployment to Heroku

{
  "name": "parse-server-example",
  "version": "1.4.0",
  "description": "Based on example Parse API server using the parse-server module",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "https://github.com/ashish-naik/parse-server.git"
  },
  "license": "MIT",
  "dependencies": {
    "express": "4.17.1",
    "parse-server": "4.1.0",
    "underscore":"*",
    "parse": "2.11.0"
  },
  "scripts": {
    "start": "node index.js"
  },
  "engines": {
    "node": ">= 8",
    "npm": ">= 5.7.1"
  }
}

auth section in index.js

auth: {
    facebook: {
      appIds: process.env.FACEBOOK_APP_ID
    },
    apple: {
      
      client_id: process.env.IOS_BUNDLE_ID 
    }
  },

@henrytkirk
Copy link

I'm having a similar issue too - perhaps related:
If I login with Facebook successfully, then logout and try Sign-in with Apple using the same email associated with Facebook, I get a 500 returned - internal server error.

@dplewis
Copy link
Member

dplewis commented Mar 22, 2020

@henrytkirk Can you open a new issue and fill out the template. Any logs or sample code would help.

@dplewis
Copy link
Member

dplewis commented Mar 22, 2020

@ashish-naik Can you try facebook login on the backend with masterKey: true?

@ashish-naik
Copy link
Author

I am not using cloud code for login. Call from swift code.

@dplewis
Copy link
Member

dplewis commented Mar 22, 2020

I wanted you to try it and see if that would fix this

If login is successful does your current user have a sessionToken? To change a user you need to have permission to do so.

@ashish-naik
Copy link
Author

I will try. May take some time due to workplace priorities.

Yes sessionToken is nil after successful login.

@ashish-naik
Copy link
Author

@dplewis I tried below. Not good with cloud code so hope code is not wrong.

Cloud function

Parse.Cloud.define("loginWithFacebook", function(request) {

  var params = request.params
  var authData = params.authData
  var id = params.id
  var expiredBy = params.expiredBy

  var facebookAuthData = {
    "id": id,
    "access_token": authData,
    "expiration_date": expiredBy
  }

  Parse.FacebookUtils.logIn(facebookAuthData, { useMasterKey: true }) 
    .then(function(user) {
        if (user.isNew()) {
            console.log("NEW User logged in with id " + user.id + " with session token : " + user.sessionToken)
        } else {
          console.log("Existing User logged in with id " + user.id + " with session token : " + user.sessionToken)
        }
    })
    .catch(function(error) {
      console.log('ERROR','loginWithFacebook() - Error with facebook login : '+ error);
      throw error
    })
  
})

ios Code

func processFBLogin() {
        
        if let accessToken = AccessToken.current {
            
            logger.debug("Facebook user logged in with toke \(accessToken.appID)- \(accessToken.tokenString)")
            
            PFCloud.callFunction(inBackground: "loginWithFacebook", withParameters: ["id":accessToken.userID, "authData":accessToken.tokenString, "expiredBy":accessToken.expirationDate], block: {
                
                (response: Any?, error: Error?) -> Void in
                
                if error == nil {

                    if let loggedinUser = PFUser.current() {
                        logger.debug("FB login successful - sessiontoken \(loggedinUser.sessionToken) ")
                    } else {
                        
                        logger.debug("FB login failed ")
                        
                    }

                } else {
                    
                    logger.error("Error with facebook login \(String(describing: error?.localizedDescription))")
                }
            })
            
        } else {
            
            logger.info("No FB token available")
        }

    }

Server log

 info: Ran cloud function loginWithFacebook for user undefined with:
   Input: {"id":"xxxxxxxxxx","expiredBy":"2020-05-24T15:48:42.018Z","authData":"adasdasdasdasdasdasda"}
   Result: undefined {"functionName":"loginWithFacebook","params":{"id":"xxxxxxxxxx","expiredBy":"2020-05-24T15:48:42.018Z","authData":"adasdasdasdasdasdasda"}}
 verbose: RESPONSE from [POST] /parse/functions/loginWithFacebook: {
   "response": {}
 } {"result":{"response":{}}}
 verbose: REQUEST for [POST] /parse/users: {
   "authData": {
     "facebook": {
       "id": "xxxxxxxxxx",
       "access_token": "adasdasdasdasdasdasda",
       "expiration_date": {
         "__type": "Date",
         "iso": "2020-05-24T15:48:42.018Z"
       }
     }
   }
 } {"method":"POST","url":"/parse/users","headers":{"host":"myApp.herokuapp.com","connection":"close","user-agent":"node-XMLHttpRequest, Parse/js2.11.0 (NodeJS 13.11.0)","accept":"*/*","content-type":"text/plain","x-request-id":"c9ec003f-70c6-4b03-bb2e-acbda997836b","x-forwarded-for":"54.92.152.225","x-forwarded-proto":"https","x-forwarded-port":"443","via":"1.1 vegur","connect-time":"1","x-request-start":"1585152709435","total-route-time":"0","content-length":"569"},"body":{"authData":{"facebook":{"id":"xxxxxx","access_token":"adasdasdasdasdasdasda","expiration_date":{"__type":"Date","iso":"2020-05-24T15:48:42.018Z"}}}}}
 heroku/router at=info method=POST path="/parse/functions/loginWithFacebook" host=myApp.herokuapp.com request_id=2d5de5a0-f465-4387-aa7a-e4c3f575566c fwd="122.169.14.163" dyno=web.1 connect=1ms service=14ms status=200 bytes=625 protocol=https
 heroku/router at=info method=POST path="/parse/users" host=myApp.herokuapp.com request_id=c9ec003f-70c6-4b03-bb2e-acbda997836b fwd="54.92.152.225" dyno=web.1 connect=1ms service=298ms status=201 bytes=794 protocol=https
 verbose: RESPONSE from [POST] /parse/users: {
   "status": 201,
   "response": {
     "objectId": "efsyICJCAq",
     "createdAt": "2020-03-25T16:11:49.440Z",
     "username": "hDTtBCA6oSpZGDR7a9kpemoFC"
   },
   "location": "http://myApp.herokuapp.com/parse/users/efsyICJCAq"
 } {"result":{"status":201,"response":{"objectId":"efsyICJCAq","createdAt":"2020-03-25T16:11:49.440Z","username":"sdfsdfsdfsdf"},"location":"http://myApp.herokuapp.com/parse/users/efsyICJCAq"}}
 Existing User logged in with id efsyICJCAq with session token : undefined

if i run code for a registered user without master key, i get sessionToken printed in logs.

But i am getting undefined for sessionToken for user.sessionToken or user.getsessionToken.
Not sure to fetch access token.

Moreover, in Xcode i get PFUser.current as nil.

Does above help?

@ashish-naik
Copy link
Author

@dplewis Did you get a chance to look at my code?

Hope you are safe and doing well in this difficult times.

@dplewis
Copy link
Member

dplewis commented Mar 29, 2020

I can look into it, can you open a similar post in the iOS SDK?

@dplewis dplewis added needs investigation type:bug Impaired feature or lacking behavior that is likely assumed labels Mar 29, 2020
@ashish-naik
Copy link
Author

@ashish-naik
Copy link
Author

@dplewis I had opened 1477 post awhile back for Sign in with Apple. The symptoms are same as Facebook login issue that i created for you to look at. I have added some details of investigation there but realized it is marked closed. Can you please have a look? Should i remove the details from there and repost here?

@ashish-naik
Copy link
Author

@dplewis did you get a chance to check? Although you asked me to open this issue in iOS SDK repo (which i did, linked above), i think this has to do with server.

I am stuck at this since many days. Really appreciate if you could take a look.

Today i tested on local server on my Mac with same config as Heroku ie
npm v 6.13.7
node v 9.4.0
parse server 4.2.0

it worked on local but didnt on Heroku. I noticed that this.storage['authprovider'] is undefined in createSessionTokenIfNeeded to createSessionToken() isnt called.

RestWrite.prototype.createSessionTokenIfNeeded = function() {

logger.info('Message - createSessionTokenIfNeeded - For class - ' + this.className)
  if (this.className !== '_User') {
    return;
  }
  // Don't generate session for updating user (this.query is set) unless authData exists
  if (this.query && !this.data.authData) {
    return;
  }
  logger.info('Message - createSessionTokenIfNeeded - Not update call - going ahead')

  // Don't generate new sessionToken if linking via sessionToken
  if (this.auth.user && this.data.authData) {
    return;
  }
  logger.info('Message - createSessionTokenIfNeeded - not linking so going ahead')

  if (
    !this.storage['authProvider'] && // signup call, with (THIS IS FAILING)
    this.config.preventLoginWithUnverifiedEmail && // no login without verification
    this.config.verifyUserEmails
  ) {
    // verification is on
    logger.info('Message - createSessionTokenIfNeeded - returning as no auth provider, preventLoginWihoutEmail, verifyEmail')   
    logger.info('Message - createSessionTokenIfNeeded - authProvider = ' + this.storage['authProvider'] + ' preventLoginWihoutEmail = ' + this.config.preventLoginWithUnverifiedEmail + ' verifyUserEmails = ' + this.config.verifyUserEmails)
    return; // do not create the session token in that case!
  }
  logger.info('Message - createSessionTokenIfNeeded - before calling - return this.createSessionToken()' )

  return this.createSessionToken();
};

@ashish-naik
Copy link
Author

Some more logs from RestWrite.js from Heroku.

RestWrite.js.txt

I noticed that validateAuthData is called twice. First time findUsersWithAuthData finds nothing and sign up fails.
Second time findUsersWithAuthData find row but createSessionTokenIfNeeded() isnt called.

Message - before return this.getUserAndRoleACL()
Message - before return this.validateClientClassCreation()
Message - before return this.handleInstallation()
Message - before return this.handleSession()
Message - before return this.validateAuthData()
Message - validateAuthData - is a _User class with valid username
Message - validateAuthData - auth method supported. Going ahead
MESSAGE - validateAuthData - provider = apple
Message - validateAuthData - Abt to call handleAuthData with auData [object Object]
MESSAGE - findUsersWithAuthData - providers length 1providers - apple
MESSAGE - findUsersWithAuthData - queryKey authData.apple.id
MESSAGE - findUsersWithAuthData - authData[provider].id xyz
MESSAGE - findUsersWithAuthData - query.length 1
MESSAGE - findUsersWithAuthData - findPromise length undefined
MESSAGE - findUsersWithAuthData - before return findPromise
MESSAGE - handleAuthData - Results count is 0
Message - before return this.runBeforeSaveTrigger()
Message - before return this.deleteEmailResetTokenIfNeeded()
Message - before return this.validateSchema()
Message - before return this.setRequiredFieldsIfNeeded()
Message - before return this.transformUser()
Message - before return this.expandFilesForExistingObjects()
Message - before return this.destroyDuplicatedSessions()
Message - before return this.runDatabaseOperation()
Message - before return this.createSessionTokenIfNeeded()
Message - createSessionTokenIfNeeded - For class - _User
Message - createSessionTokenIfNeeded - Not update call - going ahead for _User
Message - createSessionTokenIfNeeded - not linking so going ahead for _User
Message - createSessionTokenIfNeeded - returning as no auth provider, preventLoginWihoutEmail, verifyEmail for _User
Message - createSessionTokenIfNeeded - authProvider = undefined preventLoginWihoutEmail = true verifyUserEmails = true for _User
Message - before return this.handleFollowup()
Message - before return this.runAfterSaveTrigger()
Message - before return this.cleanUserAuthData()
Message - before return this.response()
Message - before return this.getUserAndRoleACL()
Message - before return this.validateClientClassCreation()
Message - before return this.handleInstallation()
Message - before return this.handleSession()
Message - before return this.validateAuthData()
Message - validateAuthData - is a _User class with valid username
Message - validateAuthData - auth method supported. Going ahead
MESSAGE - validateAuthData - provider = apple
Message - validateAuthData - Abt to call handleAuthData with auData [object Object]
MESSAGE - findUsersWithAuthData - providers length 1providers - apple
MESSAGE - findUsersWithAuthData - queryKey authData.apple.id
MESSAGE - findUsersWithAuthData - authData[provider].id xyz
MESSAGE - findUsersWithAuthData - query.length 1
MESSAGE - findUsersWithAuthData - findPromise length undefined
MESSAGE - findUsersWithAuthData - before return findPromise
MESSAGE - handleAuthData - Results count is 1
MESSAGE - handleAuthData - storage authProvider = apple
Message - before return this.runBeforeSaveTrigger()
Message - before return this.deleteEmailResetTokenIfNeeded()
Message - before return this.validateSchema()
Message - before return this.setRequiredFieldsIfNeeded()
Message - before return this.transformUser()
Message - before return this.expandFilesForExistingObjects()
Message - before return this.destroyDuplicatedSessions()
Message - before return this.runDatabaseOperation()

@ashish-naik
Copy link
Author

@dplewis

I had opened a post in under iOS but seems the issue was at server side.
I have tried to explain the probable cause and fix below.

When findUsersWithAuthData returns no result which means this user had not signed up earlier, below handleAuthDataValidation block handles auth validation and returns success since it is a valid auth.

return this.handleAuthDataValidation(authData).then(() => {
      if (results.length > 1) {
        // More than 1 user with the passed id's
        throw new Parse.Error(
          Parse.Error.ACCOUNT_ALREADY_LINKED,
          'this auth is already used'
        );
      } 
    });

But in this flow, this.storage['authProvider'] is not set so below if statement in createSessionTokenIfNeeded evaluates to true and returns without calling this.createSessionToken().

if (
    !this.storage['authProvider'] && // signup call, with
    this.config.preventLoginWithUnverifiedEmail && // no login without verification
    this.config.verifyUserEmails
  ) {
    // verification is on
    return; // do not create the session token in that case!
  }
  return this.createSessionToken();

So i tried this fix
Move this line

this.storage['authProvider'] = Object.keys(authData).join(',');
from inside

if (results.length == 1) {

to just before it.
and then set verifyUserEmails back to true. (This was suggested to be turned off by Nathan)

After this change sign up with facebook and login both worked, sign up and login with email also worked. I have not added function for linkWith in my app so cannot test it yet.

is this is a valid solution?

I tried to request you to check on iOS post but didnt get response hence raising again.

-Ashish

@ashish-naik
Copy link
Author

Hello

Can someone check this please?

@ashish-naik
Copy link
Author

@mtrezza when is this bug likely to be prioritized?

@mtrezza
Copy link
Member

mtrezza commented Nov 29, 2020

This thread has quite a potpourri of information.

I suggest to simplify and restate the issue as a comment, using the issue template, including:

  • Verifying that the issue still exists with the latest version of Parse Server and the FB SDK.
  • A step-by-step description (in list form) to replicate the issue, including the related code used in Parse Server Cloud Code and/or iOS.

I am also reclassifying this issue as "needs more info" until we can confirm that this is actually a bug.

@mtrezza mtrezza added needs more info and removed type:bug Impaired feature or lacking behavior that is likely assumed labels Nov 29, 2020
@ashish-naik
Copy link
Author

@mtrezza i tested on local as well as Heroku with v4.4.0 and found Facebook login with new user is still failing.

Issue
Login with new Facebook user fails with error code 206
Login with existing user works.

Steps to reproduce

  1. Install Parse server using package.json mentioned below.
  2. Use index.js mentioned below.
  3. Use new user to login for the first time. (i tried with test user)

Log
Optional

  • some : Error Domain=Parse Code=206 "Cannot modify user o7GT48TZZT." UserInfo={code=206, temporary=0, error=Cannot modify user o7GT48TZZT., NSLocalizedDescription=Cannot modify user o7GT48TZZT.}

I cannot provide Parse Server log because somehow i am not able to enable even after setting VERBOSE=1 in heroku config vars. (this is another issue i can't solve)

Environment Setup

Server

parse-server version (Be specific! Don't say 'latest'.) : 3.9.0 to 4.4.0 (Local 4.4.0)
Operating System: heroku-18 (Local MacOS 11.0.1)
Hardware: heroku-18 (Local MacOS 11.0.1)
Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): Both as mentioned above

Database

MongoDB version: MongoDb Atlas Sandbox cluster 4.2.10 (Local 4.2.3)
Storage engine: mongoDB Atlas
Hardware: MongoDB
Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): both

Parse iOS SDK 1.91.1
FBSDKCoreKit 6.0.0

I don't login using cloud code so below is iOS code.

iOS Code snippet for Facebook login

PFFacebookUtils.logInInBackground(withReadPermissions: facebookPermissions) {
               (loggedInUser: PFUser?, error: Error?) in
          
           if error == nil && loggedInUser != nil {
                //fetch email, name etc details from Facebook 
                 saveUserDetailsOnParse(...)
           }

func saveUserDetailsOnParse(isNewUser: Bool, user: PFUser?, socialId: String, email: String, name: String, profileImageURL: String?, loginProvider: String, callback: @escaping (String, NSError?) -> ())  {
       
       if name != "" {
          //if email based user logged in then name will be blank so update/add only if not blank ie for social login
           PFUser.current()?["name"] = name
       }
       if profileImageURL != nil {
           
           //if email based or Apple user logged in then URL will be blank so update/add only if not blank ie for social login
           PFUser.current()?["profile_image_url"] = profileImageURL!
       }
               
       PFUser.current()?["email"] = email
       
       if isNewUser {
          //if logged in user isNew  property is true
           self.saveNewUserToParse(socialId: socialId, loginProvider: loginProvider) { (errorMessage, error) in
               
               callback(errorMessage, error)
           }

       } else {
           //save details for existing user
           PFUser.current()?.saveInBackground(block: { (successSaveUser, error) in
              
             
               if successSaveUser {
                   logger.debug("User attributes updated to Parse")
                   callback("User attributes updated to Parse", nil)
                   
               } else {
                   
                   logger.debug("Error saving current PFUser post login to parse")
                   callback("Error saving current PFUser post login to parse", error! as NSError)
                   
               }
           })
       }
       
   }

index.js

var express = require('express');
var cors = require('cors');
var ParseServer = require('parse-server').ParseServer;
var path = require('path');

var databaseUri = process.env.DATABASE_URI || process.env.MONGODB_URI;

var api = new ParseServer({
  databaseURI: databaseUri,
  cloud: process.env.CLOUD_CODE_MAIN || __dirname + '/cloud/main.js',
  appId: process.env.APP_ID,
  masterKey: process.env.MASTER_KEY, //Add your master key here. Keep it secret!
  serverURL: process.env.SERVER_URL ,  // Don't forget to change to https if needed
  javascriptKey: process.env.JAVASCRIPT_KEY,// || '',  //** add this line no need to set values, they will be overwritten by heroku config vars
  restAPIKey: process.env.REST_API_KEY,// || '', //** add this line
  clientKey: process.env.CLIENT_KEY,// || '', //** add this line
  verbose:process.env.VERBOSE,
  logLevel: process.env.LOG_LEVEL, // VERBOSE, INFO, ERROR, NONE, defaults to INFO
  push: {
        ios: {
            pfx: 'certs/ApplePush.p12', // the path and filename to the .p12 file you exported earlier. 
            passphrase: process.env.APPLE_PUSH_PASSPHRASE,
            topic: process.env.APPLE_PUSH_TOPIC, // The bundle identifier associated with your app
            production: false //
        }
    },
  auth: {
    facebook: {
      appIds: process.env.FACEBOOK_APP_ID
    },
    apple: {
      
      client_id: process.env.IOS_BUNDLE_ID // optional (for extra validation), use the Service ID from Apple.
    }
  },
  verifyUserEmails: true,
  emailVerifyTokenValidityDuration: 2 * 60 * 60, 
  preventLoginWithUnverifiedEmail: true, 

  publicServerURL: process.env.SERVER_URL,
  appName: process.env.APP_NAME,
  emailAdapter: {
    module: '@parse/simple-mailgun-adapter',
    options: {
      fromAddress: process.env.APP_SUPPORT_EMAIL,
      domain: process.env.MAILGUN_DOMAIN,
      apiKey: process.env.MAILGUN_API_KEY,
    }
  },
  passwordPolicy: {
    validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, 
    validationError: 'Password must have at least 8 characters, contain at least 1 digit, 1 upper case and 1 lower case character.',
    doNotAllowUsername: true, 
    maxPasswordAge: 90, 
    maxPasswordHistory: 5, 
    resetTokenValidityDuration: 24*60*60, 
  }

});
var app = express();
app.use(cors()); 

// Serve static assets from the /public folder
app.use('/public', express.static(path.join(__dirname, '/public')));

// Serve the Parse API on the /parse URL prefix
var mountPath = process.env.PARSE_MOUNT || '/parse';
app.use(mountPath, api);

// Parse Server plays nicely with the rest of your web routes
app.get('/', function(req, res) {
  res.status(200).send('Make sure to star the parse-server repo on GitHub!');
});

// There will be a test page available on the /test path of your server url
// Remove this before launching your app
app.get('/test', function(req, res) {
  res.sendFile(path.join(__dirname, '/public/test.html'));
});

var port = process.env.PORT || 1337;
var httpServer = require('http').createServer(app);
httpServer.listen(port, function() {
    console.log('parse-server-example running on port ' + port + '.');
});

package.json

{
  "name": "name",
  "version": "1.0.0",
  "description": " example Parse API server using the parse-server module",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "https://github.com/myrepo/my-parse-server"
    
  },
  "license": "MIT",
  "dependencies": {
    "express": "4.17.1",
    "underscore":"*",
    "parse": "2.12.0",
    "cryptoutils":"*",
    "cors":"*",
    "parse-server": "4.4.0  "
    
  },
  "scripts": {
    "start": "node index.js"
  },
  "engines": {
    "node": ">9.0.0",
    "npm": "6.13.7"
  }
}

The code change i have tried and that works is in above comments.
Also uploaded here ReadWrite.js

Thanks
Ashish

@mtrezza
Copy link
Member

mtrezza commented Dec 2, 2020

Thanks for the details.

Can you please reduce the iOS code to a minimum example by removing all code that is not necessary to reproduce the issue?
The actual code path is not clear, there is also a function saveNewUserToParse which is not posted here.

Then, in that minimum example, at which step in the iOS code does the server log 206 "Cannot modify user o7GT48TZZT."?

@ashish-naik
Copy link
Author

sorry forgot that method where error occurs actually.

func saveNewUserToParse(socialId: String?, loginProvider: String?, callback: @escaping (String, NSError?) -> ())  {
                            
        // Setup some seed values for the newly created user
 
        PFUser.current()?.saveInBackground(block: { (successSaveUser, error) in

                //MARK: Getting error 206 here
           if successSaveUser {
                callback("New user's attributes saved to Parse", nil)
            } else {
              
                    logger.debug("Error saving current PFUser post login/signup to parse  \(error!.localizedDescription)")
                    callback("Error saving current PFUser post login/signup to parse", error! as NSError)
            }
        })
        
    }

@mtrezza
Copy link
Member

mtrezza commented Dec 2, 2020

Can you please reduce the iOS code to a minimum example by removing all code that is not necessary to reproduce the issue?

@ashish-naik
Copy link
Author

I haven't tested this but removed all non-essential part.

import Parse

func loginWithFacebook() {
	
	PFFacebookUtils.logInInBackground(withReadPermissions: facebookPermissions) {
		(loggedInUser: PFUser?, error: Error?) in
		
		if error == nil && loggedInUser != nil {
			
			saveUserDetailsOnParse(isNewUser: loggedInUser!.isNew, user: loggedInUser! ,socialId: socialId, email: userEmail, name: userName, profileImageURL: profileImageURL, loginProvider: "facebook", callback: { (message, error) in
				
				if error == nil {
					//proceed to home screen
				} else {
					//show error
				}
			})
		}
	}
}



func saveUserDetailsOnParse(isNewUser: Bool, user: PFUser?, socialId: String, email: String, name: String,  loginProvider: String, callback: @escaping (String, NSError?) -> ())  {
	
	if name != "" && PFUser.current()?["name"] == nil {
		
		PFUser.current()?["name"] = name
	}
	PFUser.current()?["email"] = email
	
	if isNewUser {
		//if logged in user isNew  property is true
		self.saveNewUserToParse() { (errorMessage, error) in
			
			callback(errorMessage, error)
		}
		
	} else {
		//save details for existing user
		PFUser.current()?.saveInBackground(block: { (successSaveUser, error) in
			if successSaveUser {
				logger.debug("User attributes updated to Parse")
				callback("User attributes updated to Parse", nil)
				
			} else {
				
				logger.debug("Error saving current PFUser post login to parse")
				callback("Error saving current PFUser post login to parse", error! as NSError)
				
			}
		})
	}
	
}

func saveNewUserToParse(callback: @escaping (String, NSError?) -> ())  {
	
	//Set some initial values in PFUser.current()
	
	PFUser.current()?.saveInBackground(block: { (successSaveUser, error) in
		
		//MARK: Getting error 206 here
		if successSaveUser {
			callback("New user's attributes saved to Parse", nil)
		} else {
			
			logger.debug("Error saving current PFUser post login/signup to parse  \(error!.localizedDescription)")
			callback("Error saving current PFUser post login/signup to parse", error! as NSError)
		}
	})
	
}

@davimacedo
Copy link
Member

could you please try to pass down the loggedInUser var and save it instead of PFUser.current()? I'm wondering that at that point the PFUser.current() was not yet updated by the PFFacebookUtils. Anyways I believe this is an issue with the iOS SDK and not Parse Server.

@ashish-naik
Copy link
Author

i tried to debug further by stripping down those methods.
Somehow setting email attribute causes the error 206.

Earlier i had loggedInUser!["email"] = email which i corrected to loggedInUser!.email = email

After loginWith call, a row is created in User object (authData (id and access_token not nil) and username attributes are updated) and below is state of loggedInUser

  • sessionToken = nil
  • isNew = true
  • email = nil
  • authenticated = true

If i assign value to email attribute before save call then fails with error 206 but save is successful if email is not updated.

Please see below code.

func loginWithFB() {
		
		PFFacebookUtils.logInInBackground(withReadPermissions: facebookPermissions) {
				(loggedInUser: PFUser?, error: Error?) in
		   
			if error == nil && loggedInUser != nil {

				//this causes the issue
				loggedInUser!.email = "[email protected]"

				loggedInUser!.saveInBackground { (savedStatus, error) in

					if error != nil {
							
						logger.error("Enountered error \(error)")
					} else {
						
						logger.info("Saved successfully")
					}
				}
			}
		}

	}

@mtrezza
Copy link
Member

mtrezza commented Dec 7, 2020

Thanks for reducing the code, this is interesting indeed. I will try to reproduce this issue.

@ashish-naik
Copy link
Author

Hi, were you able to reproduce?

@ashish-naik
Copy link
Author

I tested with v4.10.4 and still getting the issue.

Could this be looked at please?

@ashish-naik
Copy link
Author

@mtrezza Getting this on 5.4.0 also.
had tried to fix it #6511 (comment)

Cna you please take a look?

@mtrezza
Copy link
Member

mtrezza commented Dec 16, 2022

Is this a Parse Server or Parse SDK issue? Could you give a short summary of where we are on this issue?

@ashish-naik
Copy link
Author

It is server issue. I had am using fixed code in my instance.

Explained here.

@mtrezza
Copy link
Member

mtrezza commented Dec 17, 2022

If I understand correctly, you discovered the bug using the Parse iOS SDK, but you determined that the bug is actually server side. So let's focus on the server. Could you open a PR with a test that demonstrates the issue? The SDK is just sending a REST request, so the issue should be replicable in a test. Then, when we have the failing test, could you add your fix to the PR to show that the test does not fail anymore. Then we can merge the fix.

@ashish-naik
Copy link
Author

ashish-naik commented Dec 17, 2022

I am afraid I don't have skills to write tests !

But i forked the repo here and made the same change mentioned above to RestWrite.js. Facebook new login is working after that.

@mtrezza
Copy link
Member

mtrezza commented Dec 17, 2022

If you would like we will guide you to write a test, it's fairly easy. You can just copy an existing test and modify it. In any case you could open a PR against the alpha branch here, so others can see what changes you made.

@ashish-naik
Copy link
Author

ashish-naik commented Dec 17, 2022

Sure, i am ready to try. Please guide me.

Please guide me abt opening PR too. This is first time as well.
I tried to create PR but probably chose incorrect base and head branches. What do i need to choose?

@mtrezza
Copy link
Member

mtrezza commented Dec 17, 2022

Sure, please take a look at the New Contributors Guide. A step that is different there though is that the new branch is not created based on the master but the alpha branch.

@dblythy How can that blog post be updated?

@ashish-naik
Copy link
Author

okay so i cloned the repo, created new branch and created a test as below (from 'test facebook signup and login')
Added config.verifyUserEmails = true which according to my earlier investigation was causing error 206.

However, the test ran successfully.

Environment on my Mac is same as test instance on DigitalOcean App Platform.
node - v18.1.0
npm - 8.8.0

Same issue was on Heroku also. i am not using Heroku now but DigitalOcean uses Heroku build pack for node docker image.

Only difference is parse server is 6.alpha and not 5..x.x

it('test facebook signup with verifyUserEmails=true', done => {

   config.verifyUserEmails = true //added this to test

   const data = {
     authData: {
       facebook: {
         id: '8675309',
         access_token: 'jenny',
       },
     },
   };
   let newUserSignedUpByFacebookObjectId;
   rest
     .create(config, auth.nobody(config), '_User', data)
     .then(r => {
       expect(typeof r.response.objectId).toEqual('string');
       expect(typeof r.response.createdAt).toEqual('string');
       expect(typeof r.response.sessionToken).toEqual('string');
       newUserSignedUpByFacebookObjectId = r.response.objectId;
       return rest.create(config, auth.nobody(config), '_User', data);
     })
     .then(r => {
       expect(typeof r.response.objectId).toEqual('string');
       expect(typeof r.response.createdAt).toEqual('string');
       expect(typeof r.response.username).toEqual('string');
       expect(typeof r.response.updatedAt).toEqual('string');
       expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);
       return rest.find(config, auth.master(config), '_Session', {
         sessionToken: r.response.sessionToken,
       });
     })
     .then(response => {
       expect(response.results.length).toEqual(1);
       const output = response.results[0];
       expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
       done();
     })
     .catch(err => {
       jfail(err);
       done();
     });
 });

output

> [email protected] pretest
> cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start

  ◜ Starting a MongoDB deployment to test against...
> [email protected] test
> npm run testonly "spec/rest.spec.js"


> [email protected] testonly
> cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine "spec/rest.spec.js"

Randomized with seed 86934
Started
Jasmine started
warn: DeprecationWarning: The Parse Server option 'allowClientClassCreation' default will change to 'false' in a future version.
warn: DeprecationWarning: The Parse Server option 'allowExpiredAuthDataToken' default will change to 'false' in a future version.
.
  rest create
    ✓ handles object and subdocument
.    ✓ should generate objectId when not set by client with allowCustomObjectId true
.    ✓ is backwards compatible when _id size changes
.    ✓ can create a session with no expiration
.    ✓ handles anonymous user signup and upgrade to new user
.    ✓ handles create on non-existent class when disabled client class creation
.    ✓ test specified session length
.    ✓ handles create on existent class when disabled client class creation
.    ✓ handles user signup
.    ✓ sets current user in new sessions
.    ✓ handles no anonymous users config
.    ✓ test default session length
.    ✓ stores pointers
.    ✓ cannot set objectId
.    ✓ should use objectId from client when allowCustomObjectId true
.    ✓ can create object in volatileClasses if masterKey
.    ✓ handles anonymous user signup
.    ✓ should throw on invalid objectId when allowCustomObjectId true
.    ✓ test facebook signup and login
.    ✓ handles _id
.    ✓ stores pointers to objectIds larger than 10 characters
.    ✓ cannot set id
.    ✓ locks down session
.    ✓ test facebook signup with verifyUserEmails=true
.    ✓ can use custom _id size
.    ✓ cannot create object in volatileClasses if not masterKey
.    ✓ handles array, object, date

.  rest update
    ✓ ignores createdAt

.  read-only masterKey
    ✓ properly blocks writes
.    ✓ properly throws on rest.create, rest.update and rest.del
.    ✓ should throw when trying to create schema
.    ✓ should throw when trying to create schema with a name
.    ✓ should throw when masterKey and readOnlyMasterKey are the same
.    ✓ should throw when trying to create RestWrite
.    ✓ should throw when trying to update the global config
.    ✓ should throw when trying to send push
.    ✓ should throw when trying to update schema
.    ✓ should throw when trying to delete schema




38 specs, 0 failures
Finished in 2.061 seconds
Randomized with seed 86934 (jasmine --random=true --seed=86934)
Executed 38 of 38 specs SUCCESS in 2 secs.
Randomized with seed 86934.

> [email protected] posttest
> cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner stop

@ashish-naik
Copy link
Author

Realised setting verifyUserEmails was incorrect.

tried below but gives error Unhandled promise rejection: Error: Function requires an adapter
How to fix it?

it('test facebook signup with verifyUserEmails=true', done => {

    reconfigureServer({
      appName: 'unused',
      verifyUserEmails: true,
      publicServerURL: 'http://localhost:8378/1',
    })
    .then(() => {

      const data = {
        authData: {
          facebook: {
            id: '8675309',
            access_token: 'jenny',
          },
        },
      };
      let newUserSignedUpByFacebookObjectId;
      rest
        .create(config, auth.nobody(config), '_User', data)
        .then(r => {
          expect(typeof r.response.objectId).toEqual('string');
          expect(typeof r.response.createdAt).toEqual('string');
          expect(typeof r.response.sessionToken).toEqual('string');
          newUserSignedUpByFacebookObjectId = r.response.objectId;
          return rest.create(config, auth.nobody(config), '_User', data);
        })
        .then(r => {
          expect(typeof r.response.objectId).toEqual('string');
          expect(typeof r.response.createdAt).toEqual('string');
          expect(typeof r.response.username).toEqual('string');
          expect(typeof r.response.updatedAt).toEqual('string');
          expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);
          return rest.find(config, auth.master(config), '_Session', {
            sessionToken: r.response.sessionToken,
          });
        })
        .then(response => {
          expect(response.results.length).toEqual(1);
          const output = response.results[0];
          expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
          done();
        })
        .catch(err => {
          jfail(err);
          done();
        });
    });
    
  })

@mtrezza
Copy link
Member

mtrezza commented Dec 18, 2022

On which line does it throw that?

@ashish-naik
Copy link
Author

Line where PublicServerURL is mentioned.

@ashish-naik
Copy link
Author

I fixed the error by adding adapter constant.

The test passes successfully so wondering why actual code is failing and work with my fix explained above.
Am i missing something in configuring the test to emulate the scenario?

it('test facebook signup with verifyUserEmails=true', done => {

   const emailAdapter = {
     sendVerificationEmail: options => {
       sendEmailOptions = options;
     },
     sendPasswordResetEmail: () => Promise.resolve(),
     sendMail: () => {},
   };


   reconfigureServer({
     appName: 'unused',
     verifyUserEmails: true,
     emailAdapter: emailAdapter,
     emailVerifyTokenValidityDuration: 0.5, 
     publicServerURL: 'http://localhost:8378/1',
   })
   .then(() => {

     const data = {
       authData: {
         facebook: {
           id: '8675309',
           access_token: 'jenny',
         },
       },
     };
     let newUserSignedUpByFacebookObjectId;
     rest
       .create(config, auth.nobody(config), '_User', data)
       .then(r => {
         console.warn('Error206: New FB user created ' + r.response.objectId);

         expect(typeof r.response.objectId).toEqual('string');
         expect(typeof r.response.createdAt).toEqual('string');
         expect(typeof r.response.sessionToken).toEqual('string');
         newUserSignedUpByFacebookObjectId = r.response.objectId;
         return rest.create(config, auth.nobody(config), '_User', data);
       })
       .then(r => {
         console.warn('Error206: FB user queried ' + r.response.objectId);

         expect(typeof r.response.objectId).toEqual('string');
         expect(typeof r.response.createdAt).toEqual('string');
         expect(typeof r.response.username).toEqual('string');
         expect(typeof r.response.updatedAt).toEqual('string');
         expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);
         return rest.find(config, auth.master(config), '_Session', {
           sessionToken: r.response.sessionToken,
         });
       })
       .then(response => {
         expect(response.results.length).toEqual(1);
         const output = response.results[0];
         expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
         done();
       })
       .catch(err => {
         jfail(err);
         done();
       });
   });
   
 })

@ashish-naik
Copy link
Author

I got error on version 5.5.4 also. I haven't changed my app's login code.

However don't get error if i run on my Mac that has same version of Parse Server, NodeJS, npm as on Digital Ocean.
Only change between two setup is, Digital Ocean App platform uses Heroku build pack. I used to get same error on Heroku also on past build packs also. Not sure if this changes anything.

@mtrezza
Copy link
Member

mtrezza commented Aug 29, 2023

What version of Parse Server are you using? If this issue cannot be reproduced on Parse Server 6, we'll go ahead and close this issue, since PS5 doesn't receive any support anymore other than security related issues, and even that only 4 more months.

@ashish-naik
Copy link
Author

ashish-naik commented Aug 30, 2023

Upgraded server to 6.2.1 and still got error. On local.

Works if preventLoginWithUnverifiedEmail: false works but fails if preventLoginWithUnverifiedEmail: true

By the way, unrelated question but running this in index.js fails. Asking because i am not able to deploy on Digital Ocean so unable to test. Appreciate if you can point the mistake. World in prior to v5

const autoWithDrawRequestJob = schedule.scheduleJob({rule: '* * 0 * *', tz: "Asia/Kolkata"}, function(){
  console.log('Running resetPriceDropDate job');
  Parse.Cloud.run("cloudJob",null,{useMasterKey:true});
});

@ashish-naik
Copy link
Author

Correction : I get error irrespective of value set for preventLoginWithUnverifiedEmail on both local or Digital ocean.

@mtrezza
Copy link
Member

mtrezza commented Sep 2, 2023

This thread has gotten quite long. I'd suggest to first identify whether it's a Parse Server, Parse SDK, or non-Parse issue. To do this let's boil this issue down into a failing test in a pull request, using the Parse JS SDK. The test should also help others to understand the issue. It seems you have already written a test. Please try to further reduce the test if possible and remove any code or special config that is not necessary for the test to fail (like email adapter, etc.). Use the standard APIs of the Parse JS SDK, no REST APIs. Don't reconfigure the server if not necessary, or reconfigure only with the options you need to change. Use async/await syntax, no chained promises. Please take a look at other auth-related tests how they are written and adapt for your issue. We will then see in our CI with a plain Parse Server config whether this is reproducible.

@ashish-naik
Copy link
Author

I ran the test under server on latest version and it ran without failing. My original test also succeeded. Worked after removing all additional config too.

I cloned JS SDK, ran the default tests.
I am not well versed with JS so not sure where all i have to change in my test below.

it('test async facebook signup with verifyUserEmails=true', async done => {
    const emailAdapter = {
      sendVerificationEmail: options => {
        sendEmailOptions = options;
      },
      sendPasswordResetEmail: () => Promise.resolve(),
      sendMail: () => {},
    };


    await reconfigureServer({
      appName: 'unused',
      verifyUserEmails: false,
      emailAdapter: emailAdapter,
      emailVerifyTokenValidityDuration: 0.5, // 0.5 second
      publicServerURL: 'http://localhost:8378/1',
    })

      const data = {
        authData: {
          facebook: {
            id: '8675309',
            access_token: 'jenny',
          },
        },
      };

    let newUserSignedUpByFacebookObjectId;
    rest
      .create(config, auth.nobody(config), '_User', data)
      .then(r => {
        expect(typeof r.response.objectId).toEqual('string');
        expect(typeof r.response.createdAt).toEqual('string');
        expect(typeof r.response.sessionToken).toEqual('string');
        newUserSignedUpByFacebookObjectId = r.response.objectId;
        return rest.create(config, auth.nobody(config), '_User', data);
      })
      .then(r => {
        expect(typeof r.response.objectId).toEqual('string');
        expect(typeof r.response.createdAt).toEqual('string');
        expect(typeof r.response.username).toEqual('string');
        expect(typeof r.response.updatedAt).toEqual('string');
        expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);
        return rest.find(config, auth.master(config), '_Session', {
          sessionToken: r.response.sessionToken,
        });
      })
      .then(response => {
        expect(response.results.length).toEqual(1);
        const output = response.results[0];
        expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
        done();
      })
      .catch(err => {
        jfail(err);
        done();
      });

  });

@mtrezza
Copy link
Member

mtrezza commented Sep 3, 2023

Could you please open a PR and try to make the changes suggested in my previous comment? Some suggestions are not test specific, like using away/async instead of chained promises and removing code that is unnecessary for the failing test. We can then continue the discussion in the PR to adapt the test. If we cannot reproduce the issue, we cannot investigate this issue further and we have to assume a custom configuration on your side being the cause for this.

@ashish-naik
Copy link
Author

Created PR #8737

ashish-naik added a commit to ashish-naik/parse-server-FB-signup-error206 that referenced this issue Sep 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants