Skip to content

Commit 9f8d3f3

Browse files
authored
fix: JWT audience validation bypass in Google, Apple, and Facebook authentication adapters ([GHSA-x6fw-778m-wr9v](GHSA-x6fw-778m-wr9v)) (#10113)
1 parent 6b1b50c commit 9f8d3f3

File tree

5 files changed

+73
-149
lines changed

5 files changed

+73
-149
lines changed

spec/AuthenticationAdapters.spec.js

Lines changed: 47 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ describe('google auth adapter', () => {
484484

485485
it('should throw error with missing id_token', async () => {
486486
try {
487-
await google.validateAuthData({}, {});
487+
await google.validateAuthData({}, { clientId: 'secret' });
488488
fail();
489489
} catch (e) {
490490
expect(e.message).toBe('id token is invalid for this user.');
@@ -493,7 +493,7 @@ describe('google auth adapter', () => {
493493

494494
it('should not decode invalid id_token', async () => {
495495
try {
496-
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
496+
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, { clientId: 'secret' });
497497
fail();
498498
} catch (e) {
499499
expect(e.message).toBe('provided token does not decode as JWT');
@@ -646,6 +646,15 @@ describe('google auth adapter', () => {
646646
expect(e.message).toBe('auth data is invalid for this user.');
647647
}
648648
});
649+
650+
it('should throw error when clientId is not configured', async () => {
651+
try {
652+
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
653+
fail('should have thrown');
654+
} catch (e) {
655+
expect(e.message).toBe('Google auth is not configured.');
656+
}
657+
});
649658
});
650659

651660
describe('keycloak auth adapter', () => {
@@ -1202,6 +1211,15 @@ describe('apple signin auth adapter', () => {
12021211
expect(e.message).toBe('auth data is invalid for this user.');
12031212
}
12041213
});
1214+
1215+
it('should throw error when clientId is not configured', async () => {
1216+
try {
1217+
await apple.validateAuthData({ id: 'the_user_id', token: 'the_token' }, {});
1218+
fail('should have thrown');
1219+
} catch (e) {
1220+
expect(e.message).toBe('Apple auth is not configured.');
1221+
}
1222+
});
12051223
});
12061224

12071225
describe('phant auth adapter', () => {
@@ -1237,19 +1255,9 @@ describe('facebook limited auth adapter', () => {
12371255
const authUtils = require('../lib/Adapters/Auth/utils');
12381256

12391257
// TODO: figure out a way to run this test alongside facebook classic tests
1240-
xit('(using client id as string) should throw error with missing id_token', async () => {
1241-
try {
1242-
await facebook.validateAuthData({}, { clientId: 'secret' });
1243-
fail();
1244-
} catch (e) {
1245-
expect(e.message).toBe('Facebook auth is not configured.');
1246-
}
1247-
});
1248-
1249-
// TODO: figure out a way to run this test alongside facebook classic tests
1250-
xit('(using client id as array) should throw error with missing id_token', async () => {
1258+
xit('should throw error with missing id_token', async () => {
12511259
try {
1252-
await facebook.validateAuthData({}, { clientId: ['secret'] });
1260+
await facebook.validateAuthData({}, { appIds: ['secret'] });
12531261
fail();
12541262
} catch (e) {
12551263
expect(e.message).toBe('Facebook auth is not configured.');
@@ -1260,7 +1268,7 @@ describe('facebook limited auth adapter', () => {
12601268
try {
12611269
await facebook.validateAuthData(
12621270
{ id: 'the_user_id', token: 'the_token' },
1263-
{ clientId: 'secret' }
1271+
{ appIds: ['secret'] }
12641272
);
12651273
fail();
12661274
} catch (e) {
@@ -1277,7 +1285,7 @@ describe('facebook limited auth adapter', () => {
12771285

12781286
await facebook.validateAuthData(
12791287
{ id: 'the_user_id', token: 'the_token' },
1280-
{ clientId: 'secret' }
1288+
{ appIds: ['secret'] }
12811289
);
12821290
fail();
12831291
} catch (e) {
@@ -1302,7 +1310,7 @@ describe('facebook limited auth adapter', () => {
13021310

13031311
const result = await facebook.validateAuthData(
13041312
{ id: 'the_user_id', token: 'the_token' },
1305-
{ clientId: 'secret' }
1313+
{ appIds: ['secret'] }
13061314
);
13071315
expect(result).toEqual(fakeClaim);
13081316
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
@@ -1323,7 +1331,7 @@ describe('facebook limited auth adapter', () => {
13231331

13241332
await facebook.validateAuthData(
13251333
{ id: 'the_user_id', token: 'the_token' },
1326-
{ clientId: 'secret' }
1334+
{ appIds: ['secret'] }
13271335
);
13281336
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
13291337
});
@@ -1337,47 +1345,15 @@ describe('facebook limited auth adapter', () => {
13371345
try {
13381346
await facebook.validateAuthData(
13391347
{ id: 'the_user_id', token: 'the_token' },
1340-
{ clientId: 'secret' }
1348+
{ appIds: ['secret'] }
13411349
);
13421350
fail();
13431351
} catch (e) {
13441352
expect(e.message).toBe('jwt malformed');
13451353
}
13461354
});
13471355

1348-
it('(using client id as array) should not verify invalid id_token', async () => {
1349-
try {
1350-
await facebook.validateAuthData(
1351-
{ id: 'the_user_id', token: 'the_token' },
1352-
{ clientId: ['secret'] }
1353-
);
1354-
fail();
1355-
} catch (e) {
1356-
expect(e.message).toBe('provided token does not decode as JWT');
1357-
}
1358-
});
1359-
1360-
it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('using client id as string) should verify id_token (facebook.com)', async () => {
1361-
const fakeClaim = {
1362-
iss: 'https://www.facebook.com',
1363-
aud: 'secret',
1364-
exp: Date.now(),
1365-
sub: 'the_user_id',
1366-
};
1367-
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
1368-
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
1369-
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
1370-
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
1371-
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1372-
1373-
const result = await facebook.validateAuthData(
1374-
{ id: 'the_user_id', token: 'the_token' },
1375-
{ clientId: 'secret' }
1376-
);
1377-
expect(result).toEqual(fakeClaim);
1378-
});
1379-
1380-
it_id('c521a272-2ac2-4d8b-b5ed-ea250336d8b1')(it)('(using client id as array) should verify id_token (facebook.com)', async () => {
1356+
it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('should verify id_token (facebook.com)', async () => {
13811357
const fakeClaim = {
13821358
iss: 'https://www.facebook.com',
13831359
aud: 'secret',
@@ -1392,12 +1368,12 @@ describe('facebook limited auth adapter', () => {
13921368

13931369
const result = await facebook.validateAuthData(
13941370
{ id: 'the_user_id', token: 'the_token' },
1395-
{ clientId: ['secret'] }
1371+
{ appIds: ['secret'] }
13961372
);
13971373
expect(result).toEqual(fakeClaim);
13981374
});
13991375

1400-
it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using client id as array with multiple items) should verify id_token (facebook.com)', async () => {
1376+
it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using multiple appIds) should verify id_token (facebook.com)', async () => {
14011377
const fakeClaim = {
14021378
iss: 'https://www.facebook.com',
14031379
aud: 'secret',
@@ -1412,12 +1388,12 @@ describe('facebook limited auth adapter', () => {
14121388

14131389
const result = await facebook.validateAuthData(
14141390
{ id: 'the_user_id', token: 'the_token' },
1415-
{ clientId: ['secret', 'secret 123'] }
1391+
{ appIds: ['secret', 'secret 123'] }
14161392
);
14171393
expect(result).toEqual(fakeClaim);
14181394
});
14191395

1420-
it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('(using client id as string) should throw error with with invalid jwt issuer (facebook.com)', async () => {
1396+
it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('should throw error with with invalid jwt issuer (facebook.com)', async () => {
14211397
const fakeClaim = {
14221398
iss: 'https://not.facebook.com',
14231399
sub: 'the_user_id',
@@ -1431,7 +1407,7 @@ describe('facebook limited auth adapter', () => {
14311407
try {
14321408
await facebook.validateAuthData(
14331409
{ id: 'the_user_id', token: 'the_token' },
1434-
{ clientId: 'secret' }
1410+
{ appIds: ['secret'] }
14351411
);
14361412
fail();
14371413
} catch (e) {
@@ -1443,87 +1419,14 @@ describe('facebook limited auth adapter', () => {
14431419

14441420
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
14451421
// and a private key
1446-
xit('(using client id as array) should throw error with with invalid jwt issuer', async () => {
1447-
const fakeClaim = {
1448-
iss: 'https://not.facebook.com',
1449-
sub: 'the_user_id',
1450-
};
1451-
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
1452-
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
1453-
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
1454-
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
1455-
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1456-
1457-
try {
1458-
await facebook.validateAuthData(
1459-
{
1460-
id: 'INSERT ID HERE',
1461-
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
1462-
},
1463-
{ clientId: ['INSERT CLIENT ID HERE'] }
1464-
);
1465-
fail();
1466-
} catch (e) {
1467-
expect(e.message).toBe(
1468-
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
1469-
);
1470-
}
1471-
});
1472-
1473-
it('(using client id as string) with token', async () => {
1474-
const fakeClaim = {
1475-
iss: 'https://not.facebook.com',
1476-
sub: 'the_user_id',
1477-
};
1478-
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
1479-
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
1480-
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
1481-
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
1482-
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1483-
1484-
try {
1485-
await facebook.validateAuthData(
1486-
{
1487-
id: 'INSERT ID HERE',
1488-
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
1489-
},
1490-
{ clientId: 'INSERT CLIENT ID HERE' }
1491-
);
1492-
fail();
1493-
} catch (e) {
1494-
expect(e.message).toBe(
1495-
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
1496-
);
1497-
}
1498-
});
1499-
1500-
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
1501-
// and a private key
1502-
xit('(using client id as string) should throw error with invalid jwt clientId', async () => {
1422+
xit('should throw error with invalid jwt audience', async () => {
15031423
try {
15041424
await facebook.validateAuthData(
15051425
{
15061426
id: 'INSERT ID HERE',
15071427
token: 'INSERT FACEBOOK TOKEN HERE',
15081428
},
1509-
{ clientId: 'secret' }
1510-
);
1511-
fail();
1512-
} catch (e) {
1513-
expect(e.message).toBe('jwt audience invalid. expected: secret');
1514-
}
1515-
});
1516-
1517-
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
1518-
// and a private key
1519-
xit('(using client id as array) should throw error with invalid jwt clientId', async () => {
1520-
try {
1521-
await facebook.validateAuthData(
1522-
{
1523-
id: 'INSERT ID HERE',
1524-
token: 'INSERT FACEBOOK TOKEN HERE',
1525-
},
1526-
{ clientId: ['secret'] }
1429+
{ appIds: ['secret'] }
15271430
);
15281431
fail();
15291432
} catch (e) {
@@ -1540,7 +1443,7 @@ describe('facebook limited auth adapter', () => {
15401443
id: 'invalid user',
15411444
token: 'INSERT FACEBOOK TOKEN HERE',
15421445
},
1543-
{ clientId: 'INSERT CLIENT ID HERE' }
1446+
{ appIds: ['INSERT APP ID HERE'] }
15441447
);
15451448
fail();
15461449
} catch (e) {
@@ -1551,7 +1454,7 @@ describe('facebook limited auth adapter', () => {
15511454
it_id('c194d902-e697-46c9-a303-82c2d914473c')(it)('should throw error with with invalid user id (facebook.com)', async () => {
15521455
const fakeClaim = {
15531456
iss: 'https://www.facebook.com',
1554-
aud: 'invalid_client_id',
1457+
aud: 'invalid_app_id',
15551458
sub: 'a_different_user_id',
15561459
};
15571460
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
@@ -1563,13 +1466,22 @@ describe('facebook limited auth adapter', () => {
15631466
try {
15641467
await facebook.validateAuthData(
15651468
{ id: 'the_user_id', token: 'the_token' },
1566-
{ clientId: 'secret' }
1469+
{ appIds: ['secret'] }
15671470
);
15681471
fail();
15691472
} catch (e) {
15701473
expect(e.message).toBe('auth data is invalid for this user.');
15711474
}
15721475
});
1476+
1477+
it('should throw error when appIds is not configured for Limited Login', async () => {
1478+
try {
1479+
await facebook.validateAuthData({ id: 'the_user_id', token: 'the_token' }, {});
1480+
fail('should have thrown');
1481+
} catch (e) {
1482+
expect(e.message).toBe('Facebook auth is not configured.');
1483+
}
1484+
});
15731485
});
15741486

15751487
describe('OTP TOTP auth adatper', () => {

spec/index.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,12 +659,12 @@ describe('server', () => {
659659
});
660660

661661

662-
it('should not fail when Google signin is introduced without the optional clientId', done => {
662+
it('should not fail when Google signin is introduced with clientId', done => {
663663
const jwt = require('jsonwebtoken');
664664
const authUtils = require('../lib/Adapters/Auth/utils');
665665

666666
reconfigureServer({
667-
auth: { google: {} },
667+
auth: { google: { clientId: 'secret' } },
668668
})
669669
.then(() => {
670670
const fakeClaim = {

src/Adapters/Auth/apple.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
7373
};
7474

7575
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
76+
if (!clientId) {
77+
throw new Parse.Error(
78+
Parse.Error.OBJECT_NOT_FOUND,
79+
'Apple auth is not configured.'
80+
);
81+
}
82+
7683
if (!token) {
7784
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
7885
}

src/Adapters/Auth/facebook.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@
5252
* - `>= 6.5.6 < 7`
5353
* - `>= 7.0.1`
5454
*
55-
* Secure authentication is recommended to ensure proper data protection and compliance with Facebook's guidelines.
56-
*
5755
* @see {@link https://developers.facebook.com/docs/facebook-login/limited-login/ Facebook Limited Login}
5856
* @see {@link https://developers.facebook.com/docs/facebook-login/facebook-login-for-business/ Facebook Login for Business}
5957
*/
@@ -131,7 +129,14 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
131129
return key;
132130
};
133131

134-
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
132+
const verifyIdToken = async ({ token, id }, { appIds, cacheMaxEntries, cacheMaxAge }) => {
133+
if (!Array.isArray(appIds) || !appIds.length) {
134+
throw new Parse.Error(
135+
Parse.Error.OBJECT_NOT_FOUND,
136+
'Facebook auth is not configured.'
137+
);
138+
}
139+
135140
if (!token) {
136141
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
137142
}
@@ -150,7 +155,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
150155
jwtClaims = jwt.verify(token, signingKey, {
151156
algorithms: ['RS256'],
152157
// the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
153-
audience: clientId,
158+
audience: appIds,
154159
});
155160
} catch (exception) {
156161
const message = exception.message;

0 commit comments

Comments
 (0)