Generic OAuth provider support

Refactors facebook login into oauth generic login

Adds additional oauth2 providers

adds ability to pass an oAuth validator in the config

Adds Twitter validation support + OAuth 1 client

Support auth_token instead of access_token for twitter

Improves code coverage of OAuth

Adds validation of oauth provider structures

Better coverage of the OAuth spec

100% coverage of OAuth1.js

Adds passing auth_token_secret for Twitter auth.

Refactors auth validation methods to include authData parameter

- Adds ability to extens oauth validator through configuration
- Adds ability to extend oauth validator through external module (file or package)
- Adds more tests
- Adds tests to login with custom auth provider

Adds more tests for REST API

fixes twitter auth_token

f
This commit is contained in:
Florent Vilmart
2016-02-04 14:03:39 -05:00
parent f8ae863a2a
commit e010fd82f2
19 changed files with 1061 additions and 87 deletions

View File

@@ -25,6 +25,7 @@ function Config(applicationId, mount) {
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
this.filesController = cacheInfo.filesController;
this.oauth = cacheInfo.oauth;
this.mount = mount;
}

View File

@@ -9,7 +9,7 @@ var cache = require('./cache');
var Config = require('./Config');
var cryptoUtils = require('./cryptoUtils');
var passwordCrypto = require('./password');
var facebook = require('./facebook');
var oauth = require("./oauth");
var Parse = require('parse/node');
var triggers = require('./triggers');
@@ -147,19 +147,26 @@ RestWrite.prototype.validateAuthData = function() {
return;
}
var facebookData = this.data.authData.facebook;
var authData = this.data.authData;
var anonData = this.data.authData.anonymous;
if (this.config.enableAnonymousUsers === true && (anonData === null ||
(anonData && anonData.id))) {
return this.handleAnonymousAuthData();
} else if (facebookData === null ||
(facebookData && facebookData.id && facebookData.access_token)) {
return this.handleFacebookAuthData();
} else {
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
'This authentication method is unsupported.');
}
// Not anon, try other providers
var providers = Object.keys(authData);
if (!anonData && providers.length == 1) {
var provider = providers[0];
var providerAuthData = authData[provider];
var hasToken = (providerAuthData && providerAuthData.id);
if (providerAuthData === null || hasToken) {
return this.handleOAuthAuthData(provider);
}
}
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
'This authentication method is unsupported.');
};
RestWrite.prototype.handleAnonymousAuthData = function() {
@@ -208,27 +215,71 @@ RestWrite.prototype.handleAnonymousAuthData = function() {
};
RestWrite.prototype.handleFacebookAuthData = function() {
var facebookData = this.data.authData.facebook;
if (facebookData === null && this.query) {
// We are unlinking from Facebook.
this.data._auth_data_facebook = null;
RestWrite.prototype.handleOAuthAuthData = function(provider) {
var authData = this.data.authData[provider];
if (authData === null && this.query) {
// We are unlinking from the provider.
this.data["_auth_data_" + provider ] = null;
return;
}
return facebook.validateUserId(facebookData.id,
facebookData.access_token)
var appIds;
var oauthOptions = this.config.oauth[provider];
if (oauthOptions) {
appIds = oauthOptions.appIds;
} else if (provider == "facebook") {
appIds = this.config.facebookAppIds;
}
var validateAuthData;
var validateAppId;
if (oauth[provider]) {
validateAuthData = oauth[provider].validateAuthData;
validateAppId = oauth[provider].validateAppId;
}
// Try the configuration methods
if (oauthOptions) {
if (oauthOptions.module) {
validateAuthData = require(oauthOptions.module).validateAuthData;
validateAppId = require(oauthOptions.module).validateAppId;
};
if (oauthOptions.validateAuthData) {
validateAuthData = oauthOptions.validateAuthData;
}
if (oauthOptions.validateAppId) {
validateAppId = oauthOptions.validateAppId;
}
}
// try the custom provider first, fallback on the oauth implementation
if (!validateAuthData || !validateAppId) {
return false;
};
return validateAuthData(authData, oauthOptions)
.then(() => {
return facebook.validateAppId(this.config.facebookAppIds,
facebookData.access_token);
if (appIds && typeof validateAppId === "function") {
return validateAppId(appIds, authData, oauthOptions);
}
// No validation required by the developer
return Promise.resolve();
}).then(() => {
// Check if this user already exists
// TODO: does this handle re-linking correctly?
var query = {};
query['authData.' + provider + '.id'] = authData.id;
return this.config.database.find(
this.className,
{'authData.facebook.id': facebookData.id}, {});
query, {});
}).then((results) => {
this.storage['authProvider'] = "facebook";
this.storage['authProvider'] = provider;
if (results.length > 0) {
if (!this.query) {
// We're signing up, but this user already exists. Short-circuit
@@ -247,7 +298,7 @@ RestWrite.prototype.handleFacebookAuthData = function() {
delete this.data.authData;
return;
}
// We're trying to create a duplicate FB auth. Forbid it
// We're trying to create a duplicate oauth auth. Forbid it
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
'this auth is already used');
} else {
@@ -256,12 +307,12 @@ RestWrite.prototype.handleFacebookAuthData = function() {
// This FB auth does not already exist, so transform it to a
// saveable format
this.data._auth_data_facebook = facebookData;
this.data["_auth_data_" + provider ] = authData;
// Delete the rest format key before saving
delete this.data.authData;
});
};
}
// The non-third-party parts of User transformation
RestWrite.prototype.transformUser = function() {

View File

@@ -3,10 +3,10 @@ var https = require('https');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateUserId(userId, access_token) {
return graphRequest('me?fields=id&access_token=' + access_token)
function validateAuthData(authData) {
return graphRequest('me?fields=id&access_token=' + authData.access_token)
.then((data) => {
if (data && data.id == userId) {
if (data && data.id == authData.id) {
return;
}
throw new Parse.Error(
@@ -16,7 +16,8 @@ function validateUserId(userId, access_token) {
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId(appIds, access_token) {
function validateAppId(appIds, authData) {
var access_token = authData.access_token;
if (!appIds.length) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
@@ -53,5 +54,5 @@ function graphRequest(path) {
module.exports = {
validateAppId: validateAppId,
validateUserId: validateUserId
validateAuthData: validateAuthData
};

View File

@@ -105,7 +105,8 @@ function ParseServer(args) {
fileKey: args.fileKey || 'invalid-file-key',
facebookAppIds: args.facebookAppIds || [],
filesController: filesController,
enableAnonymousUsers: args.enableAnonymousUsers || true
enableAnonymousUsers: args.enableAnonymousUsers || true,
oauth: args.oauth || {},
};
// To maintain compatibility. TODO: Remove in v2.1

226
src/oauth/OAuth1Client.js Normal file
View File

@@ -0,0 +1,226 @@
var https = require('https'),
crypto = require('crypto');
var OAuth = function(options) {
this.consumer_key = options.consumer_key;
this.consumer_secret = options.consumer_secret;
this.auth_token = options.auth_token;
this.auth_token_secret = options.auth_token_secret;
this.host = options.host;
this.oauth_params = options.oauth_params || {};
};
OAuth.prototype.send = function(method, path, params, body){
var request = this.buildRequest(method, path, params, body);
// Encode the body properly, the current Parse Implementation don't do it properly
return new Promise(function(resolve, reject) {
var httpRequest = https.request(request, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
data = JSON.parse(data);
resolve(data);
});
}).on('error', function(e) {
reject('Failed to make an OAuth request');
});
if (request.body) {
httpRequest.write(request.body);
}
httpRequest.end();
});
};
OAuth.prototype.buildRequest = function(method, path, params, body) {
if (path.indexOf("/") != 0) {
path = "/"+path;
}
if (params && Object.keys(params).length > 0) {
path += "?" + OAuth.buildParameterString(params);
}
var request = {
host: this.host,
path: path,
method: method.toUpperCase()
};
var oauth_params = this.oauth_params || {};
oauth_params.oauth_consumer_key = this.consumer_key;
if(this.auth_token){
oauth_params["oauth_token"] = this.auth_token;
}
request = OAuth.signRequest(request, oauth_params, this.consumer_secret, this.auth_token_secret);
if (body && Object.keys(body).length > 0) {
request.body = OAuth.buildParameterString(body);
}
return request;
}
OAuth.prototype.get = function(path, params) {
return this.send("GET", path, params);
}
OAuth.prototype.post = function(path, params, body) {
return this.send("POST", path, params, body);
}
/*
Proper string %escape encoding
*/
OAuth.encode = function(str) {
// discuss at: http://phpjs.org/functions/rawurlencode/
// original by: Brett Zamir (http://brett-zamir.me)
// input by: travc
// input by: Brett Zamir (http://brett-zamir.me)
// input by: Michael Grier
// input by: Ratheous
// bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// bugfixed by: Brett Zamir (http://brett-zamir.me)
// bugfixed by: Joris
// reimplemented by: Brett Zamir (http://brett-zamir.me)
// reimplemented by: Brett Zamir (http://brett-zamir.me)
// note: This reflects PHP 5.3/6.0+ behavior
// note: Please be aware that this function expects to encode into UTF-8 encoded strings, as found on
// note: pages served as UTF-8
// example 1: rawurlencode('Kevin van Zonneveld!');
// returns 1: 'Kevin%20van%20Zonneveld%21'
// example 2: rawurlencode('http://kevin.vanzonneveld.net/');
// returns 2: 'http%3A%2F%2Fkevin.vanzonneveld.net%2F'
// example 3: rawurlencode('http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a');
// returns 3: 'http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a'
str = (str + '')
.toString();
// Tilde should be allowed unescaped in future versions of PHP (as reflected below), but if you want to reflect current
// PHP behavior, you would need to add ".replace(/~/g, '%7E');" to the following.
return encodeURIComponent(str)
.replace(/!/g, '%21')
.replace(/'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A');
}
OAuth.signatureMethod = "HMAC-SHA1";
OAuth.version = "1.0";
/*
Generate a nonce
*/
OAuth.nonce = function(){
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < 30; i++ )
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
OAuth.buildParameterString = function(obj){
var result = {};
// Sort keys and encode values
if (obj) {
var keys = Object.keys(obj).sort();
// Map key=value, join them by &
return keys.map(function(key){
return key + "=" + OAuth.encode(obj[key]);
}).join("&");
}
return "";
}
/*
Build the signature string from the object
*/
OAuth.buildSignatureString = function(method, url, parameters){
return [method.toUpperCase(), OAuth.encode(url), OAuth.encode(parameters)].join("&");
}
/*
Retuns encoded HMAC-SHA1 from key and text
*/
OAuth.signature = function(text, key){
crypto = require("crypto");
return OAuth.encode(crypto.createHmac('sha1', key).update(text).digest('base64'));
}
OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_token_secret){
oauth_parameters = oauth_parameters || {};
// Set default values
if (!oauth_parameters.oauth_nonce) {
oauth_parameters.oauth_nonce = OAuth.nonce();
}
if (!oauth_parameters.oauth_timestamp) {
oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime()/1000);
}
if (!oauth_parameters.oauth_signature_method) {
oauth_parameters.oauth_signature_method = OAuth.signatureMethod;
}
if (!oauth_parameters.oauth_version) {
oauth_parameters.oauth_version = OAuth.version;
}
if(!auth_token_secret){
auth_token_secret="";
}
// Force GET method if unset
if (!request.method) {
request.method = "GET"
}
// Collect all the parameters in one signatureParameters object
var signatureParams = {};
var parametersToMerge = [request.params, request.body, oauth_parameters];
for(var i in parametersToMerge) {
var parameters = parametersToMerge[i];
for(var k in parameters) {
signatureParams[k] = parameters[k];
}
}
// Create a string based on the parameters
var parameterString = OAuth.buildParameterString(signatureParams);
// Build the signature string
var url = "https://"+request.host+""+request.path;
var signatureString = OAuth.buildSignatureString(request.method, url, parameterString);
// Hash the signature string
var signatureKey = [OAuth.encode(consumer_secret), OAuth.encode(auth_token_secret)].join("&");
var signature = OAuth.signature(signatureString, signatureKey);
// Set the signature in the params
oauth_parameters.oauth_signature = signature;
if(!request.headers){
request.headers = {};
}
// Set the authorization header
var signature = Object.keys(oauth_parameters).sort().map(function(key){
var value = oauth_parameters[key];
return key+'="'+value+'"';
}).join(", ")
request.headers.Authorization = 'OAuth ' + signature;
// Set the content type header
request.headers["Content-Type"] = "application/x-www-form-urlencoded";
return request;
}
module.exports = OAuth;

57
src/oauth/facebook.js Normal file
View File

@@ -0,0 +1,57 @@
// Helper functions for accessing the Facebook Graph API.
var https = require('https');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return graphRequest('me?fields=id&access_token=' + authData.access_token)
.then((data) => {
if (data && data.id == authData.id) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Facebook auth is invalid for this user.');
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId(appIds, access_token) {
if (!appIds.length) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Facebook auth is not configured.');
}
return graphRequest('app?access_token=' + access_token)
.then((data) => {
if (data && appIds.indexOf(data.id) != -1) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Facebook auth is invalid for this user.');
});
}
// A promisey wrapper for FB graph requests.
function graphRequest(path) {
return new Promise(function(resolve, reject) {
https.get('https://graph.facebook.com/v2.5/' + path, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
data = JSON.parse(data);
resolve(data);
});
}).on('error', function(e) {
reject('Failed to validate this access token with Facebook.');
});
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};

51
src/oauth/github.js Normal file
View File

@@ -0,0 +1,51 @@
// Helper functions for accessing the github API.
var https = require('https');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request('user', authData.access_token)
.then((data) => {
if (data && data.id == authData.id) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Github auth is invalid for this user.');
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path, access_token) {
return new Promise(function(resolve, reject) {
https.get({
host: 'api.github.com',
path: '/' + path,
headers: {
'Authorization': 'bearer '+access_token,
'User-Agent': 'parse-server'
}
}, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
data = JSON.parse(data);
resolve(data);
});
}).on('error', function(e) {
reject('Failed to validate this access token with Github.');
});
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};

44
src/oauth/google.js Normal file
View File

@@ -0,0 +1,44 @@
// Helper functions for accessing the google API.
var https = require('https');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request("tokeninfo?access_token="+authData.access_token)
.then((response) => {
if (response && response.user_id == authData.id) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Google auth is invalid for this user.');
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path) {
return new Promise(function(resolve, reject) {
https.get("https://www.googleapis.com/oauth2/v1/" + path, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
data = JSON.parse(data);
resolve(data);
});
}).on('error', function(e) {
reject('Failed to validate this access token with Google.');
});
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};

17
src/oauth/index.js Normal file
View File

@@ -0,0 +1,17 @@
var facebook = require('./facebook');
var instagram = require("./instagram");
var linkedin = require("./linkedin");
var meetup = require("./meetup");
var google = require("./google");
var github = require("./github");
var twitter = require("./twitter");
module.exports = {
facebook: facebook,
github: github,
google: google,
instagram: instagram,
linkedin: linkedin,
meetup: meetup,
twitter: twitter
}

44
src/oauth/instagram.js Normal file
View File

@@ -0,0 +1,44 @@
// Helper functions for accessing the instagram API.
var https = require('https');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request("users/self/?access_token="+authData.access_token)
.then((response) => {
if (response && response.data && response.data.id == authData.id) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Instagram auth is invalid for this user.');
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path) {
return new Promise(function(resolve, reject) {
https.get("https://api.instagram.com/v1/" + path, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
data = JSON.parse(data);
resolve(data);
});
}).on('error', function(e) {
reject('Failed to validate this access token with Instagram.');
});
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};

51
src/oauth/linkedin.js Normal file
View File

@@ -0,0 +1,51 @@
// Helper functions for accessing the linkedin API.
var https = require('https');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request('people/~:(id)', authData.access_token)
.then((data) => {
if (data && data.id == authData.id) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Meetup auth is invalid for this user.');
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path, access_token) {
return new Promise(function(resolve, reject) {
https.get({
host: 'api.linkedin.com',
path: '/v1/' + path,
headers: {
'Authorization': 'Bearer '+access_token,
'x-li-format': 'json'
}
}, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
data = JSON.parse(data);
resolve(data);
});
}).on('error', function(e) {
reject('Failed to validate this access token with Linkedin.');
});
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};

50
src/oauth/meetup.js Normal file
View File

@@ -0,0 +1,50 @@
// Helper functions for accessing the meetup API.
var https = require('https');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request('member/self', authData.access_token)
.then((data) => {
if (data && data.id == authData.id) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Meetup auth is invalid for this user.');
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
// A promisey wrapper for api requests
function request(path, access_token) {
return new Promise(function(resolve, reject) {
https.get({
host: 'api.meetup.com',
path: '/2/' + path,
headers: {
'Authorization': 'bearer '+access_token
}
}, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
data = JSON.parse(data);
resolve(data);
});
}).on('error', function(e) {
reject('Failed to validate this access token with Meetup.');
});
});
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};

30
src/oauth/twitter.js Normal file
View File

@@ -0,0 +1,30 @@
// Helper functions for accessing the meetup API.
var OAuth = require('./OAuth1Client');
var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData, options) {
var client = new OAuth(options);
client.host = "api.twitter.com";
client.auth_token = authData.auth_token;
client.auth_token_secret = authData.auth_token_secret;
return client.get("/1.1/account/verify_credentials.json").then((data) => {
if (data && data.id == authData.id) {
return;
}
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Twitter auth is invalid for this user.');
});
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId() {
return Promise.resolve();
}
module.exports = {
validateAppId: validateAppId,
validateAuthData: validateAuthData
};

View File

@@ -55,21 +55,21 @@ export function transformKeyValue(schema, className, restKey, restValue, options
case '_wperm':
return {key: key, value: restValue};
break;
case 'authData.anonymous.id':
if (options.query) {
return {key: '_auth_data_anonymous.id', value: restValue};
}
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
'can only query on ' + key);
break;
case 'authData.facebook.id':
if (options.query) {
// Special-case auth data.
return {key: '_auth_data_facebook.id', value: restValue};
}
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
'can only query on ' + key);
break;
// case 'authData.anonymous.id':
// if (options.query) {
// return {key: '_auth_data_anonymous.id', value: restValue};
// }
// throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
// 'can only query on ' + key);
// break;
// case 'authData.facebook.id':
// if (options.query) {
// // Special-case auth data.
// return {key: '_auth_data_facebook.id', value: restValue};
// }
// throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
// 'can only query on ' + key);
// break;
case '$or':
if (!options.query) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
@@ -97,6 +97,18 @@ export function transformKeyValue(schema, className, restKey, restValue, options
});
return {key: '$and', value: mongoSubqueries};
default:
// Other auth data
var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
if (authDataMatch) {
if (options.query) {
var provider = authDataMatch[1];
// Special-case auth data.
return {key: '_auth_data_'+provider+'.id', value: restValue};
}
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
'can only query on ' + key);
break;
};
if (options.validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
'invalid key name: ' + key);
@@ -646,15 +658,16 @@ function untransformObject(schema, className, mongoObject) {
case '_expiresAt':
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key])).iso;
break;
case '_auth_data_anonymous':
restObject['authData'] = restObject['authData'] || {};
restObject['authData']['anonymous'] = mongoObject[key];
break;
case '_auth_data_facebook':
restObject['authData'] = restObject['authData'] || {};
restObject['authData']['facebook'] = mongoObject[key];
break;
default:
// Check other auth data keys
var authDataMatch = key.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
if (authDataMatch) {
var provider = authDataMatch[1];
restObject['authData'] = restObject['authData'] || {};
restObject['authData'][provider] = mongoObject[key];
break;
}
if (key.indexOf('_p_') == 0) {
var newKey = key.substring(3);
var expected;