diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index c6834dcf45..ae7e08be05 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -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'), diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index e9486187ef..a506852fbd 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -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() }); @@ -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 () => { @@ -1262,6 +1264,10 @@ describe('Auth Adapter features', () => { spyOn(challengeAdapter, 'validateAuthData').and.rejectWith({}); + await reconfigureServer({ + auth: { challengeAdapter, soloAdapter }, + }); + await expectAsync( requestWithExpectedError({ headers: headers, diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index e97db08a6c..42c73eeb4b 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -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); diff --git a/src/Adapters/Auth/AuthAdapter.js b/src/Adapters/Auth/AuthAdapter.js index e739df3f54..2ff1989fd6 100644 --- a/src/Adapters/Auth/AuthAdapter.js +++ b/src/Adapters/Auth/AuthAdapter.js @@ -102,16 +102,26 @@ export class AuthAdapter { * @param {Object} options additional adapter options * @returns {Promise} 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; + } + }); } } diff --git a/src/Adapters/Auth/index.js b/src/Adapters/Auth/index.js index da65b24ba5..411d4f46dc 100755 --- a/src/Adapters/Auth/index.js +++ b/src/Adapters/Auth/index.js @@ -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'); @@ -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 @@ -160,8 +158,6 @@ function loadAuthAdapter(provider, authOptions) { return; } - const adapter = - defaultAdapter instanceof AuthAdapter ? defaultAdapter : Object.assign({}, defaultAdapter); const keys = [ 'validateAuthData', 'validateAppId', @@ -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) { @@ -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 }; }; @@ -257,6 +265,7 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) { getValidatorForProvider, setEnableAnonymousUsers, runAfterFind, + authCache, }); }; diff --git a/src/Auth.js b/src/Auth.js index f21ba96c6c..5e10f5e1fa 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -528,6 +528,12 @@ const handleAuthDataValidation = async (authData, req, foundUser) => { '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; diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 8af8dc5434..490a3d75a9 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -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 {