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 11 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
65 changes: 26 additions & 39 deletions spec/AuthenticationAdapters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ describe('AuthenticationProviders', function () {
expect(typeof authAdapter.validateAppId).toBe('function');
}

it('properly loads custom adapter', done => {
it('properly loads custom adapter', async () => {
const validAuthData = {
id: 'hello',
token: 'world',
Expand All @@ -377,6 +377,8 @@ describe('AuthenticationProviders', function () {
const authDataSpy = spyOn(adapter, 'validateAuthData').and.callThrough();
const appIdSpy = spyOn(adapter, 'validateAppId').and.callThrough();

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

const authenticationHandler = authenticationLoader({
customAuthentication: adapter,
});
Expand All @@ -385,71 +387,56 @@ describe('AuthenticationProviders', function () {
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);

validator(validAuthData, {}, {}).then(
() => {
expect(authDataSpy).toHaveBeenCalled();
// AppIds are not provided in the adapter, should not be called
expect(appIdSpy).not.toHaveBeenCalled();
done();
},
err => {
jfail(err);
done();
}
);
await validator(validAuthData, {}, {});
expect(authDataSpy).toHaveBeenCalled();

expect(appIdSpy).not.toHaveBeenCalled();
});

it('properly loads custom adapter module object', done => {
const authenticationHandler = authenticationLoader({
customAuthentication: path.resolve('./spec/support/CustomAuth.js'),
it('properly loads custom adapter module object', async () => {
await reconfigureServer({
auth: {
customAuthentication: {
validateAppId() {},
validateAuthData() {},
},
},
});
const authenticationHandler = authenticationLoader();

validateAuthenticationHandler(authenticationHandler);
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);
validator(
await validator(
{
token: 'my-token',
},
{},
{}
).then(
() => {
done();
},
err => {
jfail(err);
done();
}
);
});

it('properly loads custom adapter module object (again)', done => {
const authenticationHandler = authenticationLoader({
customAuthentication: {
module: path.resolve('./spec/support/CustomAuthFunction.js'),
options: { token: 'valid-token' },
it('properly loads custom adapter module object (again)', async () => {
await reconfigureServer({
auth: {
customAuthentication: {
validateAppId() {},
validateAuthData() {},
},
},
});
const authenticationHandler = authenticationLoader();

validateAuthenticationHandler(authenticationHandler);
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);

validator(
await validator(
{
token: 'valid-token',
},
{},
{}
).then(
() => {
done();
},
err => {
jfail(err);
done();
}
);
});

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 @@ -1234,6 +1236,10 @@ describe('Auth Adapter features', () => {
await user.save({ authData: { challengeAdapter: { id: 'challengeAdapter' } } });

spyOn(challengeAdapter, 'validateAuthData').and.rejectWith({});
const authenticationLoader = require('../lib/Adapters/Auth');
authenticationLoader.loadAuthAdapter('challengeAdapter', {
challengeAdapter,
});

await expectAsync(
requestWithExpectedError({
Expand Down
13 changes: 13 additions & 0 deletions spec/ParseUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,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
2 changes: 2 additions & 0 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const semver = require('semver');
const CurrentSpecReporter = require('./support/CurrentSpecReporter.js');
const { SpecReporter } = require('jasmine-spec-reporter');
const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default;
const AuthAdapters = require('../lib/Adapters/Auth');

// Ensure localhost resolves to ipv4 address first on node v17+
if (dns.setDefaultResultOrder) {
Expand Down Expand Up @@ -210,6 +211,7 @@ afterEach(function (done) {
destroyAliveConnections();
await TestUtils.destroyAllDataPermanently(true);
SchemaCache.clear();
AuthAdapters.validateAuthConfig(defaultConfiguration.auth);
if (didChangeConfiguration) {
await reconfigureServer();
} else {
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 @@ -100,16 +100,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
124 changes: 69 additions & 55 deletions src/Adapters/Auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const providers = {
ldap,
};

let authAdapters = {};

// Indexed auth policies
dblythy marked this conversation as resolved.
Show resolved Hide resolved
const authAdapterPolicies = {
default: true,
Expand Down Expand Up @@ -135,69 +137,78 @@ 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
const providerOptions = authOptions[provider];
if (
providerOptions &&
Object.prototype.hasOwnProperty.call(providerOptions, 'oauth2') &&
providerOptions['oauth2'] === true
) {
defaultAdapter = oauth2;
}

// Default provider not found and a custom auth provider was not provided
if (!defaultAdapter && !providerOptions) {
return;
}

const adapter =
defaultAdapter instanceof AuthAdapter ? defaultAdapter : Object.assign({}, defaultAdapter);
const keys = [
'validateAuthData',
'validateAppId',
'validateSetUp',
'validateLogin',
'validateUpdate',
'challenge',
'validateOptions',
'policy',
'afterFind',
];
const defaultAuthAdapter = new AuthAdapter();
keys.forEach(key => {
const existing = adapter?.[key];
function loadAuthAdapter(provider, authOptions, cached) {
if (!cached) {
let defaultAdapter = providers[provider];
// authOptions can contain complete custom auth adapters or
// a default auth adapter like Facebook
const providerOptions = authOptions[provider];
if (
existing &&
typeof existing === 'function' &&
existing.toString() === defaultAuthAdapter[key].toString()
providerOptions &&
Object.prototype.hasOwnProperty.call(providerOptions, 'oauth2') &&
providerOptions['oauth2'] === true
) {
adapter[key] = null;
defaultAdapter = oauth2;
}
});
const appIds = providerOptions ? providerOptions.appIds : undefined;

// Try the configuration methods
if (providerOptions) {
const optionalAdapter = loadAdapter(providerOptions, undefined, providerOptions);
if (optionalAdapter) {
keys.forEach(key => {
if (optionalAdapter[key]) {
adapter[key] = optionalAdapter[key];
}
});
// Default provider not found and a custom auth provider was not provided
if (!defaultAdapter && !providerOptions) {
return;
}

const keys = [
'validateAuthData',
'validateAppId',
'validateSetUp',
'validateLogin',
'validateUpdate',
'challenge',
'validateOptions',
'policy',
'afterFind',
];

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) {
keys.forEach(key => {
if (optionalAdapter[key]) {
adapter[key] = optionalAdapter[key];
}
});
}
}

if (providerOptions?.enabled !== false) {
if (adapter.validateOptions) {
adapter.validateOptions(providerOptions);
}
authAdapters[provider] = adapter;
}
}
if (adapter.validateOptions) {
adapter.validateOptions(providerOptions);
const adapter = authAdapters[provider];
if (!adapter) {
return;
}

const providerOptions = authOptions[provider];
const appIds = providerOptions ? providerOptions.appIds : undefined;
return { adapter, appIds, providerOptions };
}

function validateAuthConfig(auth) {
authAdapters = {};
if (!auth.anonymous) {
auth.anonymous = { enabled: true };
}
Object.keys(auth).forEach(key => loadAuthAdapter(key, auth));
}

module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
let _enableAnonymousUsers = enableAnonymousUsers;
const setEnableAnonymousUsers = function (enable) {
Expand All @@ -208,8 +219,10 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
if (provider === 'anonymous' && !_enableAnonymousUsers) {
return { validator: undefined };
}
const authAdapter = loadAuthAdapter(provider, authOptions);
if (!authAdapter) return;
const authAdapter = loadAuthAdapter(provider, authOptions, true);
if (!authAdapter) {
return { validator: undefined };
}
const { adapter, appIds, providerOptions } = authAdapter;
return { validator: authDataValidator(provider, adapter, appIds, providerOptions), adapter };
};
Expand Down Expand Up @@ -252,3 +265,4 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
};

module.exports.loadAuthAdapter = loadAuthAdapter;
module.exports.validateAuthConfig = validateAuthConfig;
12 changes: 6 additions & 6 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,18 +489,18 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
}
const { validator } = req.config.authDataManager.getValidatorForProvider(provider);
const authProvider = (req.config.auth || {})[provider] || {};
if (authProvider.enabled == null) {
Deprecator.logRuntimeDeprecation({
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".`,
});
}
if (!validator || authProvider.enabled === false) {
throw new Parse.Error(
Parse.Error.UNSUPPORTED_SERVICE,
'This authentication method is unsupported.'
);
}
if (authProvider.enabled == null) {
Deprecator.logRuntimeDeprecation({
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
Loading