diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index cafb309f0b..759aa478b8 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -1702,4 +1702,32 @@ describe('Auth Adapter features', () => { expect(res.status).toBeLessThan(500); }); }); + + it('does not crash fetching a _User with stale authData from a removed built-in provider (#10526)', async () => { + // Configure a built-in code-auth provider, then simulate a _User that still carries + // its `_auth_data_` after the provider is later removed from config. + await reconfigureServer({ auth: { line: { clientId: 'x', clientSecret: 'y' } } }); + const config = Config.get(Parse.applicationId); + const user = new Parse.User(); + await user.save({ username: 'stale-line-user', password: 'pass' }); + // Inject stale built-in-provider authData directly, bypassing auth validation — this + // reproduces a row created while `line` was still configured. + const staleAuthData = { id: 'stale-line-id', access_token: 'stale-token' }; + await config.database.update( + '_User', + { objectId: user.id }, + { authData: { line: staleAuthData } } + ); + + // Remove the provider from config. `line` is still a built-in adapter, so loadAuthAdapter + // reaches validateOptions with no options and throws during afterFind. + await reconfigureServer({ auth: {} }); + + // Full fetch (no keys selected) with the master key mirrors Parse Dashboard opening _User. + const fetched = new Parse.User(); + fetched.id = user.id; + await expectAsync(fetched.fetch({ useMasterKey: true })).toBeResolved(); + // Skip-and-keep-raw: the stale provider's authData is returned unchanged, not stripped. + expect(fetched.get('authData')).toEqual({ line: staleAuthData }); + }); }); diff --git a/src/Adapters/Auth/index.js b/src/Adapters/Auth/index.js index 51dd5342ef..b03ef8c6e3 100755 --- a/src/Adapters/Auth/index.js +++ b/src/Adapters/Auth/index.js @@ -1,6 +1,7 @@ import loadAdapter from '../AdapterLoader'; import Parse from 'parse/node'; import AuthAdapter from './AuthAdapter'; +import { logger } from '../../logger'; const apple = require('./apple'); const digits = require('./twitter'); // digits tokens are validated by twitter @@ -228,7 +229,20 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) { const adapters = Object.keys(authData); await Promise.all( adapters.map(async provider => { - const authAdapter = getValidatorForProvider(provider); + let authAdapter; + try { + authAdapter = getValidatorForProvider(provider); + } catch (e) { + // The adapter could not be resolved — typically a built-in code-auth provider such as + // `line`/`instagram` that was removed from config while a _User still carries stale + // `_auth_data_` (loadAuthAdapter throws from validateOptions when a built-in + // adapter has no options). Skip afterFind for this provider instead of failing the + // whole _User fetch, but log so a genuine adapter error is not silently swallowed. (#10526) + logger.warn( + `Skipping afterFind auth adapter for provider "${provider}": ${e.message || e}` + ); + return; + } if (!authAdapter) { return; }