fix: Concurrent signup with same authentication creates duplicate users (#10150)

This commit is contained in:
Manuel
2026-03-09 02:05:49 +00:00
committed by GitHub
parent f2058fb5e3
commit fac8f338ec
11 changed files with 452 additions and 20 deletions
+210
View File
@@ -0,0 +1,210 @@
'use strict';
const request = require('../lib/request');
const Config = require('../lib/Config');
describe('AuthData Unique Index', () => {
const fakeAuthProvider = {
validateAppId: () => Promise.resolve(),
validateAuthData: () => Promise.resolve(),
};
beforeEach(async () => {
await reconfigureServer({ auth: { fakeAuthProvider } });
});
it('should prevent concurrent signups with the same authData from creating duplicate users', async () => {
const authData = { fakeAuthProvider: { id: 'duplicate-test-id', token: 'token1' } };
// Fire multiple concurrent signup requests with the same authData
const concurrentRequests = Array.from({ length: 5 }, () =>
request({
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
},
url: 'http://localhost:8378/1/users',
body: { authData },
}).then(
response => ({ success: true, data: response.data }),
error => ({ success: false, error: error.data || error.message })
)
);
const results = await Promise.all(concurrentRequests);
const successes = results.filter(r => r.success);
const failures = results.filter(r => !r.success);
// All should either succeed (returning the same user) or fail with "this auth is already used"
// The key invariant: only ONE unique objectId should exist
const uniqueObjectIds = new Set(successes.map(r => r.data.objectId));
expect(uniqueObjectIds.size).toBe(1);
// Failures should be "this auth is already used" errors
for (const failure of failures) {
expect(failure.error.code).toBe(208);
expect(failure.error.error).toBe('this auth is already used');
}
// Verify only one user exists in the database with this authData
const query = new Parse.Query('_User');
query.equalTo('authData.fakeAuthProvider.id', 'duplicate-test-id');
const users = await query.find({ useMasterKey: true });
expect(users.length).toBe(1);
});
it('should prevent concurrent signups via batch endpoint with same authData', async () => {
const authData = { fakeAuthProvider: { id: 'batch-race-test-id', token: 'token1' } };
const response = await request({
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
},
url: 'http://localhost:8378/1/batch',
body: {
requests: Array.from({ length: 3 }, () => ({
method: 'POST',
path: '/1/users',
body: { authData },
})),
},
});
const results = response.data;
const successes = results.filter(r => r.success);
const failures = results.filter(r => r.error);
// All successes should reference the same user
const uniqueObjectIds = new Set(successes.map(r => r.success.objectId));
expect(uniqueObjectIds.size).toBe(1);
// Failures should be "this auth is already used" errors
for (const failure of failures) {
expect(failure.error.code).toBe(208);
expect(failure.error.error).toBe('this auth is already used');
}
// Verify only one user exists in the database with this authData
const query = new Parse.Query('_User');
query.equalTo('authData.fakeAuthProvider.id', 'batch-race-test-id');
const users = await query.find({ useMasterKey: true });
expect(users.length).toBe(1);
});
it('should allow sequential signups with different authData IDs', async () => {
const user1 = await Parse.User.logInWith('fakeAuthProvider', {
authData: { id: 'user-id-1', token: 'token1' },
});
const user2 = await Parse.User.logInWith('fakeAuthProvider', {
authData: { id: 'user-id-2', token: 'token2' },
});
expect(user1.id).toBeDefined();
expect(user2.id).toBeDefined();
expect(user1.id).not.toBe(user2.id);
});
it('should still allow login with authData after successful signup', async () => {
const authPayload = { authData: { id: 'login-test-id', token: 'token1' } };
// Signup
const user1 = await Parse.User.logInWith('fakeAuthProvider', authPayload);
expect(user1.id).toBeDefined();
// Login again with same authData — should return same user
const user2 = await Parse.User.logInWith('fakeAuthProvider', authPayload);
expect(user2.id).toBe(user1.id);
});
it('should skip startup index creation when createIndexAuthDataUniqueness is false', async () => {
const config = Config.get('test');
const adapter = config.database.adapter;
const spy = spyOn(adapter, 'ensureAuthDataUniqueness').and.callThrough();
// Temporarily set the option to false
const originalOptions = config.database.options.databaseOptions;
config.database.options.databaseOptions = { createIndexAuthDataUniqueness: false };
await config.database.performInitialization();
expect(spy).not.toHaveBeenCalled();
// Restore original options
config.database.options.databaseOptions = originalOptions;
});
it('should handle calling ensureAuthDataUniqueness multiple times (idempotent)', async () => {
const config = Config.get('test');
const adapter = config.database.adapter;
// Both calls should succeed (index creation is idempotent)
await adapter.ensureAuthDataUniqueness('fakeAuthProvider');
await adapter.ensureAuthDataUniqueness('fakeAuthProvider');
});
it('should log warning when index creation fails due to existing duplicates', async () => {
const config = Config.get('test');
const adapter = config.database.adapter;
// Spy on the adapter to simulate a duplicate value error
spyOn(adapter, 'ensureAuthDataUniqueness').and.callFake(() => {
return Promise.reject(
new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'duplicates exist')
);
});
const logSpy = spyOn(require('../lib/logger').logger, 'warn');
// Re-run performInitialization — should warn but not throw
await config.database.performInitialization();
expect(logSpy).toHaveBeenCalledWith(
jasmine.stringContaining('Unable to ensure uniqueness for auth data provider'),
jasmine.anything()
);
});
it('should prevent concurrent signups with same anonymous authData', async () => {
const anonymousId = 'anon-race-test-id';
const authData = { anonymous: { id: anonymousId } };
const concurrentRequests = Array.from({ length: 5 }, () =>
request({
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
},
url: 'http://localhost:8378/1/users',
body: { authData },
}).then(
response => ({ success: true, data: response.data }),
error => ({ success: false, error: error.data || error.message })
)
);
const results = await Promise.all(concurrentRequests);
const successes = results.filter(r => r.success);
const failures = results.filter(r => !r.success);
// All successes should reference the same user
const uniqueObjectIds = new Set(successes.map(r => r.data.objectId));
expect(uniqueObjectIds.size).toBe(1);
// Failures should be "this auth is already used" errors
for (const failure of failures) {
expect(failure.error.code).toBe(208);
expect(failure.error.error).toBe('this auth is already used');
}
// Verify only one user exists in the database with this authData
const query = new Parse.Query('_User');
query.equalTo('authData.anonymous.id', anonymousId);
const users = await query.find({ useMasterKey: true });
expect(users.length).toBe(1);
});
});
+10
View File
@@ -415,6 +415,11 @@ describe('DatabaseController', function () {
email_1: { email: 1 },
_email_verify_token: { _email_verify_token: 1 },
_perishable_token: { _perishable_token: 1 },
_auth_data_custom_id: { '_auth_data_custom.id': 1 },
_auth_data_facebook_id: { '_auth_data_facebook.id': 1 },
_auth_data_myoauth_id: { '_auth_data_myoauth.id': 1 },
_auth_data_shortLivedAuth_id: { '_auth_data_shortLivedAuth.id': 1 },
_auth_data_anonymous_id: { '_auth_data_anonymous.id': 1 },
});
}
);
@@ -441,6 +446,11 @@ describe('DatabaseController', function () {
email_1: { email: 1 },
_email_verify_token: { _email_verify_token: 1 },
_perishable_token: { _perishable_token: 1 },
_auth_data_custom_id: { '_auth_data_custom.id': 1 },
_auth_data_facebook_id: { '_auth_data_facebook.id': 1 },
_auth_data_myoauth_id: { '_auth_data_myoauth.id': 1 },
_auth_data_shortLivedAuth_id: { '_auth_data_shortLivedAuth.id': 1 },
_auth_data_anonymous_id: { '_auth_data_anonymous.id': 1 },
});
}
);
+57 -6
View File
@@ -337,7 +337,7 @@ describe('Parse.User testing', () => {
expect(newUser).not.toBeUndefined();
});
it('should be let masterKey lock user out with authData', async () => {
it_only_db('mongo')('should reject duplicate authData when masterKey locks user out (mongo)', async () => {
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/classes/_User',
@@ -353,15 +353,13 @@ describe('Parse.User testing', () => {
});
const body = response.data;
const objectId = body.objectId;
const sessionToken = body.sessionToken;
expect(sessionToken).toBeDefined();
expect(body.sessionToken).toBeDefined();
expect(objectId).toBeDefined();
const user = new Parse.User();
user.id = objectId;
const ACL = new Parse.ACL();
user.setACL(ACL);
await user.save(null, { useMasterKey: true });
// update the user
const options = {
method: 'POST',
url: `http://localhost:8378/1/classes/_User/`,
@@ -377,8 +375,61 @@ describe('Parse.User testing', () => {
},
},
};
const res = await request(options);
expect(res.data.objectId).not.toEqual(objectId);
try {
await request(options);
fail('should have thrown');
} catch (err) {
expect(err.data.code).toBe(208);
expect(err.data.error).toBe('this auth is already used');
}
});
it_only_db('postgres')('should reject duplicate authData when masterKey locks user out (postgres)', async () => {
await reconfigureServer();
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/classes/_User',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
},
body: {
key: 'value',
authData: { anonymous: { id: '00000000-0000-0000-0000-000000000001' } },
},
});
const body = response.data;
const objectId = body.objectId;
expect(body.sessionToken).toBeDefined();
expect(objectId).toBeDefined();
const user = new Parse.User();
user.id = objectId;
const ACL = new Parse.ACL();
user.setACL(ACL);
await user.save(null, { useMasterKey: true });
const options = {
method: 'POST',
url: `http://localhost:8378/1/classes/_User/`,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
},
body: {
key: 'otherValue',
authData: {
anonymous: { id: '00000000-0000-0000-0000-000000000001' },
},
},
};
try {
await request(options);
fail('should have thrown');
} catch (err) {
expect(err.data.code).toBe(208);
expect(err.data.error).toBe('this auth is already used');
}
});
it('user login with files', done => {
@@ -530,6 +530,13 @@ export class MongoStorageAdapter implements StorageAdapter {
if (matches && Array.isArray(matches)) {
err.userInfo = { duplicated_field: matches[1] };
}
// Check for authData unique index violations
if (!err.userInfo) {
const authDataMatch = error.message.match(/index:\s+(_auth_data_[a-zA-Z0-9_]+_id)/);
if (authDataMatch) {
err.userInfo = { duplicated_field: authDataMatch[1] };
}
}
}
throw err;
}
@@ -605,10 +612,27 @@ export class MongoStorageAdapter implements StorageAdapter {
.then(result => mongoObjectToParseObject(className, result, schema))
.catch(error => {
if (error.code === 11000) {
throw new Parse.Error(
logger.error('Duplicate key error:', error.message);
const err = new Parse.Error(
Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided'
);
err.underlyingError = error;
if (error.message) {
const matches = error.message.match(
/index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/
);
if (matches && Array.isArray(matches)) {
err.userInfo = { duplicated_field: matches[1] };
}
if (!err.userInfo) {
const authDataMatch = error.message.match(/index:\s+(_auth_data_[a-zA-Z0-9_]+_id)/);
if (authDataMatch) {
err.userInfo = { duplicated_field: authDataMatch[1] };
}
}
}
throw err;
}
throw error;
})
@@ -764,6 +788,32 @@ export class MongoStorageAdapter implements StorageAdapter {
.catch(err => this.handleError(err));
}
// Creates a unique sparse index on _auth_data_<provider>.id to prevent
// race conditions during concurrent signups with the same authData.
ensureAuthDataUniqueness(provider: string) {
return this._adaptiveCollection('_User')
.then(collection =>
collection._mongoCollection.createIndex(
{ [`_auth_data_${provider}.id`]: 1 },
{ unique: true, sparse: true, background: true, name: `_auth_data_${provider}_id` }
)
)
.catch(error => {
if (error.code === 11000) {
throw new Parse.Error(
Parse.Error.DUPLICATE_VALUE,
'Tried to ensure field uniqueness for a class that already has duplicates.'
);
}
// Ignore "index already exists with same name" or "index already exists with different options"
if (error.code === 85 || error.code === 86) {
return;
}
throw error;
})
.catch(err => this.handleError(err));
}
// Used in tests
_rawFind(className: string, query: QueryType) {
return this._adaptiveCollection(className)
@@ -1479,9 +1479,15 @@ export class PostgresStorageAdapter implements StorageAdapter {
);
err.underlyingError = error;
if (error.constraint) {
const matches = error.constraint.match(/unique_([a-zA-Z]+)/);
if (matches && Array.isArray(matches)) {
err.userInfo = { duplicated_field: matches[1] };
// Check for authData unique index violations first
const authDataMatch = error.constraint.match(/_User_unique_authData_([a-zA-Z0-9_]+)_id/);
if (authDataMatch) {
err.userInfo = { duplicated_field: `_auth_data_${authDataMatch[1]}` };
} else {
const matches = error.constraint.match(/unique_([a-zA-Z]+)/);
if (matches && Array.isArray(matches)) {
err.userInfo = { duplicated_field: matches[1] };
}
}
}
error = err;
@@ -1801,7 +1807,30 @@ export class PostgresStorageAdapter implements StorageAdapter {
const whereClause = where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
const qs = `UPDATE $1:name SET ${updatePatterns.join()} ${whereClause} RETURNING *`;
const promise = (transactionalSession ? transactionalSession.t : this._client).any(qs, values);
const promise = (transactionalSession ? transactionalSession.t : this._client)
.any(qs, values)
.catch(error => {
if (error.code === PostgresUniqueIndexViolationError) {
const err = new Parse.Error(
Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided'
);
err.underlyingError = error;
if (error.constraint) {
const authDataMatch = error.constraint.match(/_User_unique_authData_([a-zA-Z0-9_]+)_id/);
if (authDataMatch) {
err.userInfo = { duplicated_field: `_auth_data_${authDataMatch[1]}` };
} else {
const matches = error.constraint.match(/unique_([a-zA-Z]+)/);
if (matches && Array.isArray(matches)) {
err.userInfo = { duplicated_field: matches[1] };
}
}
}
throw err;
}
throw error;
});
if (transactionalSession) {
transactionalSession.batch.push(promise);
}
@@ -2044,6 +2073,31 @@ export class PostgresStorageAdapter implements StorageAdapter {
});
}
// Creates a unique index on authData-><provider>->>'id' to prevent
// race conditions during concurrent signups with the same authData.
async ensureAuthDataUniqueness(provider: string) {
const indexName = `_User_unique_authData_${provider}_id`;
const qs = `CREATE UNIQUE INDEX IF NOT EXISTS $1:name ON "_User" (("authData"->$2::text->>'id')) WHERE "authData"->$2::text->>'id' IS NOT NULL`;
await this._client.none(qs, [indexName, provider]).catch(error => {
if (
error.code === PostgresDuplicateRelationError &&
error.message.includes(indexName)
) {
// Index already exists. Ignore error.
} else if (
error.code === PostgresUniqueIndexViolationError &&
error.message.includes(indexName)
) {
throw new Parse.Error(
Parse.Error.DUPLICATE_VALUE,
'Tried to ensure field uniqueness for a class that already has duplicates.'
);
} else {
throw error;
}
});
}
// Executes a count.
async count(
className: string,
+24
View File
@@ -1838,6 +1838,30 @@ class DatabaseController {
throw error;
});
}
// Create unique indexes for authData providers to prevent race conditions
// during concurrent signups with the same authData
if (
databaseOptions.createIndexAuthDataUniqueness !== false &&
typeof this.adapter.ensureAuthDataUniqueness === 'function'
) {
const authProviders = Object.keys(this.options.auth || {});
if (this.options.enableAnonymousUsers !== false) {
if (!authProviders.includes('anonymous')) {
authProviders.push('anonymous');
}
}
await Promise.all(
authProviders.map(provider =>
this.adapter.ensureAuthDataUniqueness(provider).catch(error => {
logger.warn(
`Unable to ensure uniqueness for auth data provider "${provider}": `,
error
);
})
)
);
}
await this.adapter.updateSchemaWithIndexes();
}
+8 -1
View File
@@ -110,7 +110,7 @@ module.exports.ParseServerOptions = {
auth: {
env: 'PARSE_SERVER_AUTH_PROVIDERS',
help:
'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication',
"Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication<br><br>Provider names must start with a letter and contain only letters, digits, and underscores (`/^[A-Za-z][A-Za-z0-9_]*$/`). This is because each provider name is used to construct a database field (`_auth_data_<provider>`), which must comply with Parse Server's field naming rules.",
action: parsers.objectParser,
},
cacheAdapter: {
@@ -1248,6 +1248,13 @@ module.exports.DatabaseOptions = {
'The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout.',
action: parsers.numberParser('connectTimeoutMS'),
},
createIndexAuthDataUniqueness: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_AUTH_DATA_UNIQUENESS',
help:
'Set to `true` to automatically create unique indexes on the authData fields of the _User collection for each configured auth provider on server start, including `anonymous` when anonymous users are enabled. These indexes prevent race conditions during concurrent signups with the same authData. Set to `false` to skip index creation. Default is `true`.<br><br>\u26A0\uFE0F When setting this option to `false` to manually create the indexes, keep in mind that the otherwise automatically created indexes may change in the future to be optimized for the internal usage by Parse Server.',
action: parsers.booleanParser,
default: true,
},
createIndexRoleName: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME',
help:
+2 -1
View File
@@ -21,7 +21,7 @@
* @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics
* @property {String} appId Your Parse Application ID
* @property {String} appName Sets the app name
* @property {Object} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
* @property {Object} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication<br><br>Provider names must start with a letter and contain only letters, digits, and underscores (`/^[A-Za-z][A-Za-z0-9_]*$/`). This is because each provider name is used to construct a database field (`_auth_data_<provider>`), which must comply with Parse Server's field naming rules.
* @property {Adapter<CacheAdapter>} cacheAdapter Adapter module for the cache
* @property {Number} cacheMaxSize Sets the maximum size for the in memory cache, defaults to 10000
* @property {Number} cacheTTL Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)
@@ -278,6 +278,7 @@
* @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.
* @property {Union} compressors The MongoDB driver option to specify an array or comma-delimited string of compressors to enable network compression for communication between this client and a mongod/mongos instance.
* @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout.
* @property {Boolean} createIndexAuthDataUniqueness Set to `true` to automatically create unique indexes on the authData fields of the _User collection for each configured auth provider on server start, including `anonymous` when anonymous users are enabled. These indexes prevent race conditions during concurrent signups with the same authData. Set to `false` to skip index creation. Default is `true`.<br><br>⚠️ When setting this option to `false` to manually create the indexes, keep in mind that the otherwise automatically created indexes may change in the future to be optimized for the internal usage by Parse Server.
* @property {Boolean} createIndexRoleName Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
* @property {Boolean} createIndexUserEmail Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
* @property {Boolean} createIndexUserEmailCaseInsensitive Set to `true` to automatically create a case-insensitive index on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
+4 -1
View File
@@ -164,7 +164,7 @@ export interface ParseServerOptions {
:ENV: PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID
:DEFAULT: false */
allowCustomObjectId: ?boolean;
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication<br><br>Provider names must start with a letter and contain only letters, digits, and underscores (`/^[A-Za-z][A-Za-z0-9_]*$/`). This is because each provider name is used to construct a database field (`_auth_data_<provider>`), which must comply with Parse Server's field naming rules.
:ENV: PARSE_SERVER_AUTH_PROVIDERS */
auth: ?{ [string]: AuthAdapter };
/* Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
@@ -775,6 +775,9 @@ export interface DatabaseOptions {
/* Set to `true` to automatically create a case-insensitive index on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
:DEFAULT: true */
createIndexUserUsernameCaseInsensitive: ?boolean;
/* Set to `true` to automatically create unique indexes on the authData fields of the _User collection for each configured auth provider on server start, including `anonymous` when anonymous users are enabled. These indexes prevent race conditions during concurrent signups with the same authData. Set to `false` to skip index creation. Default is `true`.<br><br>⚠️ When setting this option to `false` to manually create the indexes, keep in mind that the otherwise automatically created indexes may change in the future to be optimized for the internal usage by Parse Server.
:DEFAULT: true */
createIndexAuthDataUniqueness: ?boolean;
/* Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>⚠️ When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.
:DEFAULT: true */
createIndexRoleName: ?boolean;
+27 -6
View File
@@ -514,6 +514,16 @@ RestWrite.prototype.getUserId = function () {
}
};
RestWrite.prototype._throwIfAuthDataDuplicate = function (error) {
if (
this.className === '_User' &&
error?.code === Parse.Error.DUPLICATE_VALUE &&
error.userInfo?.duplicated_field?.startsWith('_auth_data_')
) {
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used');
}
};
// Developers are allowed to change authData via before save trigger
// we need after before save to ensure that the developer
// is not currently duplicating auth data ID
@@ -644,12 +654,17 @@ RestWrite.prototype.handleAuthData = async function (authData) {
// uses the `doNotSave` option. Just update the authData part
// Then we're good for the user, early exit of sorts
if (Object.keys(this.data.authData).length) {
await this.config.database.update(
this.className,
{ objectId: this.data.objectId },
{ authData: this.data.authData },
{}
);
try {
await this.config.database.update(
this.className,
{ objectId: this.data.objectId },
{ authData: this.data.authData },
{}
);
} catch (error) {
this._throwIfAuthDataDuplicate(error);
throw error;
}
}
}
}
@@ -1545,6 +1560,10 @@ RestWrite.prototype.runDatabaseOperation = function () {
false,
this.validSchemaController
)
.catch(error => {
this._throwIfAuthDataDuplicate(error);
throw error;
})
.then(response => {
response.updatedAt = this.updatedAt;
this._updateResponseWithData(response, this.data);
@@ -1579,6 +1598,8 @@ RestWrite.prototype.runDatabaseOperation = function () {
throw error;
}
this._throwIfAuthDataDuplicate(error);
// Quick check, if we were able to infer the duplicated field name
if (error && error.userInfo && error.userInfo.duplicated_field === 'username') {
throw new Parse.Error(
+1
View File
@@ -38,6 +38,7 @@ export const DefaultMongoURI = DefinitionDefaults.databaseURI;
// before passing to MongoDB client
export const ParseServerDatabaseOptions = [
'allowPublicExplain',
'createIndexAuthDataUniqueness',
'createIndexRoleName',
'createIndexUserEmail',
'createIndexUserEmailCaseInsensitive',