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

perf: Initialize authentication adapters only once at server start #8464

Open
wants to merge 21 commits into
base: alpha
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions spec/AuthenticationAdapters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,33 @@ describe('AuthenticationProviders', function () {
);
});

it('should cache adapter', async () => {
const adapter = {
validateAppId() {
return Promise.resolve();
},
validateAuthData() {
return Promise.resolve();
},
validateOptions() {},
};

const authDataSpy = spyOn(adapter, 'validateAuthData').and.callThrough();
const optionsSpy = spyOn(adapter, 'validateOptions').and.callThrough();

await reconfigureServer({ auth: { customAuthentication: adapter } });

expect(optionsSpy).toHaveBeenCalled();
await Parse.User.logInWith('customAuthentication', {
authData: { id: 'user1', token: 'fakeToken1' },
});
await Parse.User.logInWith('customAuthentication', {
authData: { id: 'user2', token: 'fakeToken2' },
});
expect(authDataSpy).toHaveBeenCalled();
expect(optionsSpy).toHaveBeenCalledTimes(1);
});

it('properly loads custom adapter module object', done => {
const authenticationHandler = authenticationLoader({
customAuthentication: path.resolve('./spec/support/CustomAuth.js'),
Expand Down
10 changes: 8 additions & 2 deletions spec/AuthenticationAdaptersV2.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ describe('Auth Adapter features', () => {
it('should strip out authData if required', async () => {
const spy = spyOn(modernAdapter3, 'validateOptions').and.callThrough();
const afterSpy = spyOn(modernAdapter3, 'afterFind').and.callThrough();
await reconfigureServer({ auth: { modernAdapter3 } });
await reconfigureServer({ auth: { modernAdapter3 }, silent: false });
expect(spy).toHaveBeenCalled();
spy.calls.reset();
const user = new Parse.User();
await user.save({ authData: { modernAdapter3: { id: 'modernAdapter3Data' } } });
await user.fetch({ sessionToken: user.getSessionToken() });
Expand All @@ -366,7 +368,7 @@ describe('Auth Adapter features', () => {
{ id: 'modernAdapter3Data' },
undefined
);
expect(spy).toHaveBeenCalled();
expect(spy).not.toHaveBeenCalled();
});

it('should throw if no triggers found', async () => {
Expand Down Expand Up @@ -1262,6 +1264,10 @@ describe('Auth Adapter features', () => {

spyOn(challengeAdapter, 'validateAuthData').and.rejectWith({});

await reconfigureServer({
auth: { challengeAdapter, soloAdapter },
});

await expectAsync(
requestWithExpectedError({
headers: headers,
Expand Down
13 changes: 13 additions & 0 deletions spec/ParseUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,19 @@ describe('Parse.User testing', () => {
);
});

it('cannot connect to unconfigured adapter', async () => {
await reconfigureServer({
auth: {},
});
const provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
const user = new Parse.User();
user.set('foo', 'bar');
await expectAsync(user._linkWith('facebook', {})).toBeRejectedWith(
new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, 'This authentication method is unsupported.')
);
});

it('should not call beforeLogin with become', async done => {
const provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Expand Down
20 changes: 15 additions & 5 deletions src/Adapters/Auth/AuthAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,26 @@ export class AuthAdapter {
* @param {Object} options additional adapter options
* @returns {Promise<Object>} Any overrides required to authData
*/
afterFind(authData, options) {
return Promise.resolve({});
}
afterFind(authData, options) {}

/**
* Triggered when the adapter is first attached to Parse Server
* @param {Object} options Adapter Options
*/
validateOptions(options) {
/* */
validateOptions(options) {}

_clearDefaultKeys(keys) {
const defaultAdapter = new AuthAdapter();
keys.forEach(key => {
const existing = this[key];
if (
existing &&
typeof existing === 'function' &&
existing.toString() === defaultAdapter[key].toString()
) {
this[key] = null;
}
});
}
}

Expand Down
51 changes: 30 additions & 21 deletions src/Adapters/Auth/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import loadAdapter from '../AdapterLoader';
import Parse from 'parse/node';
import AuthAdapter from './AuthAdapter';

const apple = require('./apple');
const gcenter = require('./gcenter');
const gpgames = require('./gpgames');
Expand Down Expand Up @@ -142,7 +141,6 @@ function authDataValidator(provider, adapter, appIds, options) {
}

function loadAuthAdapter(provider, authOptions) {
// providers are auth providers implemented by default
let defaultAdapter = providers[provider];
// authOptions can contain complete custom auth adapters or
// a default auth adapter like Facebook
Expand All @@ -160,8 +158,6 @@ function loadAuthAdapter(provider, authOptions) {
return;
}

const adapter =
defaultAdapter instanceof AuthAdapter ? defaultAdapter : Object.assign({}, defaultAdapter);
const keys = [
'validateAuthData',
'validateAppId',
Expand All @@ -173,20 +169,13 @@ function loadAuthAdapter(provider, authOptions) {
'policy',
'afterFind',
];
const defaultAuthAdapter = new AuthAdapter();
keys.forEach(key => {
const existing = adapter?.[key];
if (
existing &&
typeof existing === 'function' &&
existing.toString() === defaultAuthAdapter[key].toString()
) {
adapter[key] = null;
}
});
const appIds = providerOptions ? providerOptions.appIds : undefined;

// Try the configuration methods
let adapter = Object.assign({}, defaultAdapter);
if (defaultAdapter instanceof AuthAdapter) {
adapter = new defaultAdapter.constructor();
defaultAdapter._clearDefaultKeys(keys);
}

if (providerOptions) {
const optionalAdapter = loadAdapter(providerOptions, undefined, providerOptions);
if (optionalAdapter) {
Expand All @@ -197,25 +186,44 @@ function loadAuthAdapter(provider, authOptions) {
});
}
}
if (adapter.validateOptions) {
adapter.validateOptions(providerOptions);

if (providerOptions?.enabled !== false) {
if (adapter.validateOptions) {
adapter.validateOptions(providerOptions);
}
}

const appIds = providerOptions ? providerOptions.appIds : undefined;
return { adapter, appIds, providerOptions };
}

function validateAuthConfig(auth) {
const authCache = new Map();
if (!auth.anonymous) {
auth.anonymous = { enabled: true };
}
Object.keys(auth).forEach(key => {
const authObject = loadAuthAdapter(key, auth);
authCache.set(key, authObject);
});
return authCache;
}

module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
let _enableAnonymousUsers = enableAnonymousUsers;
const setEnableAnonymousUsers = function (enable) {
_enableAnonymousUsers = enable;
};
const authCache = validateAuthConfig(authOptions);
// To handle the test cases on configuration
const getValidatorForProvider = function (provider) {
if (provider === 'anonymous' && !_enableAnonymousUsers) {
return { validator: undefined };
}
const authAdapter = loadAuthAdapter(provider, authOptions);
if (!authAdapter) return;
const authAdapter = authCache.get(provider);
if (!authAdapter) {
return { validator: undefined };
}
const { adapter, appIds, providerOptions } = authAdapter;
return { validator: authDataValidator(provider, adapter, appIds, providerOptions), adapter };
};
Expand Down Expand Up @@ -257,6 +265,7 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
getValidatorForProvider,
setEnableAnonymousUsers,
runAfterFind,
authCache,
});
};

Expand Down
6 changes: 6 additions & 0 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,12 @@
'This authentication method is unsupported.'
);
}
if (authProvider.enabled == null) {
Deprecator.logRuntimeDeprecation({

Check failure on line 532 in src/Auth.js

View workflow job for this annotation

GitHub Actions / Lint

'Deprecator' is not defined
usage: `Using the authentication adapter "${provider}" without explicitly enabling it`,
solution: `Enable the authentication adapter by setting the Parse Server option "auth.${provider}.enabled: true".`,
});
}
let validationResult = await validator(authData[provider], req, user, requestObject);
method = validationResult && validationResult.method;
requestObject.triggerName = method;
Expand Down
2 changes: 1 addition & 1 deletion src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ export class UsersRouter extends ClassesRouter {
for (const provider of Object.keys(challengeData).sort()) {
try {
const authAdapter = req.config.authDataManager.getValidatorForProvider(provider);
if (!authAdapter) {
if (!authAdapter?.validator) {
continue;
}
const {
Expand Down
Loading