fix: JWT audience validation bypass in Google, Apple, and Facebook authentication adapters ([GHSA-x6fw-778m-wr9v](https://github.com/parse-community/parse-server/security/advisories/GHSA-x6fw-778m-wr9v)) (#10114)

This commit is contained in:
Manuel
2026-03-06 05:12:09 +00:00
committed by GitHub
parent 79bd170d53
commit 1da3123118
5 changed files with 73 additions and 149 deletions
+47 -135
View File
@@ -484,7 +484,7 @@ describe('google auth adapter', () => {
it('should throw error with missing id_token', async () => {
try {
await google.validateAuthData({}, {});
await google.validateAuthData({}, { clientId: 'secret' });
fail();
} catch (e) {
expect(e.message).toBe('id token is invalid for this user.');
@@ -493,7 +493,7 @@ describe('google auth adapter', () => {
it('should not decode invalid id_token', async () => {
try {
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, { clientId: 'secret' });
fail();
} catch (e) {
expect(e.message).toBe('provided token does not decode as JWT');
@@ -646,6 +646,15 @@ describe('google auth adapter', () => {
expect(e.message).toBe('auth data is invalid for this user.');
}
});
it('should throw error when clientId is not configured', async () => {
try {
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
fail('should have thrown');
} catch (e) {
expect(e.message).toBe('Google auth is not configured.');
}
});
});
describe('keycloak auth adapter', () => {
@@ -1202,6 +1211,15 @@ describe('apple signin auth adapter', () => {
expect(e.message).toBe('auth data is invalid for this user.');
}
});
it('should throw error when clientId is not configured', async () => {
try {
await apple.validateAuthData({ id: 'the_user_id', token: 'the_token' }, {});
fail('should have thrown');
} catch (e) {
expect(e.message).toBe('Apple auth is not configured.');
}
});
});
describe('phant auth adapter', () => {
@@ -1237,19 +1255,9 @@ describe('facebook limited auth adapter', () => {
const authUtils = require('../lib/Adapters/Auth/utils');
// TODO: figure out a way to run this test alongside facebook classic tests
xit('(using client id as string) should throw error with missing id_token', async () => {
xit('should throw error with missing id_token', async () => {
try {
await facebook.validateAuthData({}, { clientId: 'secret' });
fail();
} catch (e) {
expect(e.message).toBe('Facebook auth is not configured.');
}
});
// TODO: figure out a way to run this test alongside facebook classic tests
xit('(using client id as array) should throw error with missing id_token', async () => {
try {
await facebook.validateAuthData({}, { clientId: ['secret'] });
await facebook.validateAuthData({}, { appIds: ['secret'] });
fail();
} catch (e) {
expect(e.message).toBe('Facebook auth is not configured.');
@@ -1260,7 +1268,7 @@ describe('facebook limited auth adapter', () => {
try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
{ appIds: ['secret'] }
);
fail();
} catch (e) {
@@ -1277,7 +1285,7 @@ describe('facebook limited auth adapter', () => {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
{ appIds: ['secret'] }
);
fail();
} catch (e) {
@@ -1302,7 +1310,7 @@ describe('facebook limited auth adapter', () => {
const result = await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
{ appIds: ['secret'] }
);
expect(result).toEqual(fakeClaim);
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
@@ -1323,7 +1331,7 @@ describe('facebook limited auth adapter', () => {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
{ appIds: ['secret'] }
);
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
});
@@ -1337,7 +1345,7 @@ describe('facebook limited auth adapter', () => {
try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
{ appIds: ['secret'] }
);
fail();
} catch (e) {
@@ -1345,19 +1353,7 @@ describe('facebook limited auth adapter', () => {
}
});
it('(using client id as array) should not verify invalid id_token', async () => {
try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: ['secret'] }
);
fail();
} catch (e) {
expect(e.message).toBe('provided token does not decode as JWT');
}
});
it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('using client id as string) should verify id_token (facebook.com)', async () => {
it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('should verify id_token (facebook.com)', async () => {
const fakeClaim = {
iss: 'https://www.facebook.com',
aud: 'secret',
@@ -1372,12 +1368,12 @@ describe('facebook limited auth adapter', () => {
const result = await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
{ appIds: ['secret'] }
);
expect(result).toEqual(fakeClaim);
});
it_id('c521a272-2ac2-4d8b-b5ed-ea250336d8b1')(it)('(using client id as array) should verify id_token (facebook.com)', async () => {
it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using multiple appIds) should verify id_token (facebook.com)', async () => {
const fakeClaim = {
iss: 'https://www.facebook.com',
aud: 'secret',
@@ -1392,32 +1388,12 @@ describe('facebook limited auth adapter', () => {
const result = await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: ['secret'] }
{ appIds: ['secret', 'secret 123'] }
);
expect(result).toEqual(fakeClaim);
});
it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using client id as array with multiple items) should verify id_token (facebook.com)', async () => {
const fakeClaim = {
iss: 'https://www.facebook.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { 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);
const result = await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: ['secret', 'secret 123'] }
);
expect(result).toEqual(fakeClaim);
});
it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('(using client id as string) should throw error with with invalid jwt issuer (facebook.com)', async () => {
it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('should throw error with with invalid jwt issuer (facebook.com)', async () => {
const fakeClaim = {
iss: 'https://not.facebook.com',
sub: 'the_user_id',
@@ -1431,7 +1407,7 @@ describe('facebook limited auth adapter', () => {
try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
{ appIds: ['secret'] }
);
fail();
} catch (e) {
@@ -1443,87 +1419,14 @@ describe('facebook limited auth adapter', () => {
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
// and a private key
xit('(using client id as array) should throw error with with invalid jwt issuer', async () => {
const fakeClaim = {
iss: 'https://not.facebook.com',
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { 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);
try {
await facebook.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
},
{ clientId: ['INSERT CLIENT ID HERE'] }
);
fail();
} catch (e) {
expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
);
}
});
it('(using client id as string) with token', async () => {
const fakeClaim = {
iss: 'https://not.facebook.com',
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { 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);
try {
await facebook.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
},
{ clientId: 'INSERT CLIENT ID HERE' }
);
fail();
} catch (e) {
expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
);
}
});
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
// and a private key
xit('(using client id as string) should throw error with invalid jwt clientId', async () => {
xit('should throw error with invalid jwt audience', async () => {
try {
await facebook.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT FACEBOOK TOKEN HERE',
},
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('jwt audience invalid. expected: secret');
}
});
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
// and a private key
xit('(using client id as array) should throw error with invalid jwt clientId', async () => {
try {
await facebook.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT FACEBOOK TOKEN HERE',
},
{ clientId: ['secret'] }
{ appIds: ['secret'] }
);
fail();
} catch (e) {
@@ -1540,7 +1443,7 @@ describe('facebook limited auth adapter', () => {
id: 'invalid user',
token: 'INSERT FACEBOOK TOKEN HERE',
},
{ clientId: 'INSERT CLIENT ID HERE' }
{ appIds: ['INSERT APP ID HERE'] }
);
fail();
} catch (e) {
@@ -1551,7 +1454,7 @@ describe('facebook limited auth adapter', () => {
it_id('c194d902-e697-46c9-a303-82c2d914473c')(it)('should throw error with with invalid user id (facebook.com)', async () => {
const fakeClaim = {
iss: 'https://www.facebook.com',
aud: 'invalid_client_id',
aud: 'invalid_app_id',
sub: 'a_different_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
@@ -1563,13 +1466,22 @@ describe('facebook limited auth adapter', () => {
try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
{ appIds: ['secret'] }
);
fail();
} catch (e) {
expect(e.message).toBe('auth data is invalid for this user.');
}
});
it('should throw error when appIds is not configured for Limited Login', async () => {
try {
await facebook.validateAuthData({ id: 'the_user_id', token: 'the_token' }, {});
fail('should have thrown');
} catch (e) {
expect(e.message).toBe('Facebook auth is not configured.');
}
});
});
describe('OTP TOTP auth adatper', () => {
+2 -2
View File
@@ -659,12 +659,12 @@ describe('server', () => {
});
it('should not fail when Google signin is introduced without the optional clientId', done => {
it('should not fail when Google signin is introduced with clientId', done => {
const jwt = require('jsonwebtoken');
const authUtils = require('../lib/Adapters/Auth/utils');
reconfigureServer({
auth: { google: {} },
auth: { google: { clientId: 'secret' } },
})
.then(() => {
const fakeClaim = {
+7
View File
@@ -73,6 +73,13 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
};
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
if (!clientId) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple auth is not configured.'
);
}
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
}
+9 -4
View File
@@ -52,8 +52,6 @@
* - `>= 6.5.6 < 7`
* - `>= 7.0.1`
*
* Secure authentication is recommended to ensure proper data protection and compliance with Facebook's guidelines.
*
* @see {@link https://developers.facebook.com/docs/facebook-login/limited-login/ Facebook Limited Login}
* @see {@link https://developers.facebook.com/docs/facebook-login/facebook-login-for-business/ Facebook Login for Business}
*/
@@ -131,7 +129,14 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
return key;
};
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
const verifyIdToken = async ({ token, id }, { appIds, cacheMaxEntries, cacheMaxAge }) => {
if (!Array.isArray(appIds) || !appIds.length) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Facebook auth is not configured.'
);
}
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
}
@@ -150,7 +155,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
jwtClaims = jwt.verify(token, signingKey, {
algorithms: ['RS256'],
// the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
audience: clientId,
audience: appIds,
});
} catch (exception) {
const message = exception.message;
+8 -8
View File
@@ -3,7 +3,7 @@
*
* @class GoogleAdapter
* @param {Object} options - The adapter configuration options.
* @param {string} options.clientId - Your Google application Client ID. Required for authentication.
* @param {string} options.clientId - Your Google application Client ID.
* @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).
*
@@ -74,6 +74,13 @@ const getGoogleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
};
async function verifyIdToken({ id_token: token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) {
if (!clientId) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Google auth is not configured.'
);
}
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
}
@@ -109,13 +116,6 @@ async function verifyIdToken({ id_token: token, id }, { clientId, cacheMaxEntrie
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `auth data is invalid for this user.`);
}
if (clientId && jwtClaims.aud !== clientId) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`id token not authorized for this clientId.`
);
}
return jwtClaims;
}