diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index f975914bd7..d499f9307f 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -575,6 +575,27 @@ describe('google auth adapter', () => { expect(result).toEqual(fakeClaim); }); + it('(using client id as array with multiple items) should verify id_token (google.com)', async () => { + const fakeClaim = { + iss: 'https://accounts.google.com', + aud: 'secret', + exp: Date.now(), + sub: 'the_user_id', + }; + const fakeDecodedToken = { kid: '123', alg: 'RS256' }; + const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; + spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); + spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + + await expectAsync( + google.validateAuthData( + { id: 'the_user_id', id_token: 'the_token' }, + { clientId: ['secret', 'other-client-id'] } + ) + ).toBeResolvedTo(fakeClaim); + }); + it('(using client id as string) should throw error with with invalid jwt issuer (google.com)', async () => { const fakeClaim = { iss: 'https://not.google.com', @@ -655,6 +676,14 @@ describe('google auth adapter', () => { expect(e.message).toBe('Google auth is not configured.'); } }); + + it('should throw error when clientId is an empty array', async () => { + await expectAsync( + google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, { clientId: [] }) + ).toBeRejectedWith( + new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Google auth is not configured.') + ); + }); }); describe('keycloak auth adapter', () => { @@ -1470,6 +1499,14 @@ describe('apple signin auth adapter', () => { expect(e.message).toBe('Apple auth is not configured.'); } }); + + it('should throw error when clientId is an empty array', async () => { + await expectAsync( + apple.validateAuthData({ id: 'the_user_id', token: 'the_token' }, { clientId: [] }) + ).toBeRejectedWith( + new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple auth is not configured.') + ); + }); }); describe('phant auth adapter', () => { diff --git a/src/Adapters/Auth/apple.js b/src/Adapters/Auth/apple.js index 35cb59291f..78a1c9e2b7 100644 --- a/src/Adapters/Auth/apple.js +++ b/src/Adapters/Auth/apple.js @@ -3,7 +3,7 @@ * * @class AppleAdapter * @param {Object} options - Configuration options for the adapter. - * @param {string} options.clientId - Your Apple App ID. + * @param {string|string[]} options.clientId - Your Apple App ID, or an array of App IDs to accept tokens for multiple bundle IDs (e.g. separate iPhone and iPad apps sharing one Parse app). * * @param {Object} authData - The authentication data provided by the client. * @param {string} authData.id - The user ID obtained from Apple. @@ -21,6 +21,10 @@ * } * } * ``` + * `clientId` also accepts an array of App IDs to accept tokens issued for any of several bundle IDs: + * ```json + * { "auth": { "apple": { "clientId": ["12345", "67890"] } } } + * ``` * * ## Expected `authData` from the Client * The adapter expects the client to provide the following `authData` payload: @@ -73,7 +77,7 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => { }; const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => { - if (!clientId) { + if (!clientId || (Array.isArray(clientId) && !clientId.length)) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, 'Apple auth is not configured.' diff --git a/src/Adapters/Auth/google.js b/src/Adapters/Auth/google.js index f1a47f6c1c..68ef900789 100644 --- a/src/Adapters/Auth/google.js +++ b/src/Adapters/Auth/google.js @@ -3,7 +3,7 @@ * * @class GoogleAdapter * @param {Object} options - The adapter configuration options. - * @param {string} options.clientId - Your Google application Client ID. + * @param {string|string[]} options.clientId - Your Google application Client ID, or an array of Client IDs to accept tokens issued for any of them. * @param {number} [options.cacheMaxEntries] - Maximum number of JWKS cache entries. Default: 5. * @param {number} [options.cacheMaxAge] - Maximum age of JWKS cache entries in ms. Default: 3600000 (1 hour). * @@ -19,6 +19,10 @@ * } * } * ``` + * `clientId` also accepts an array of Client IDs to accept tokens issued for any of them: + * ```json + * { "auth": { "google": { "clientId": ["id-1", "id-2"] } } } + * ``` * * The adapter requires the following `authData` fields: * - **id**: The Google user ID. @@ -74,7 +78,7 @@ const getGoogleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => { }; async function verifyIdToken({ id_token: token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) { - if (!clientId) { + if (!clientId || (Array.isArray(clientId) && !clientId.length)) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, 'Google auth is not configured.'