Add support for Facebook Limited Login (#7219)
* Add support for Facebook Limited auth * Add tests * Fix tests * Fix tests * Add entry to changelog * Cleanup
This commit is contained in:
@@ -21,6 +21,7 @@ ___
|
|||||||
- NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis)
|
- NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis)
|
||||||
- NEW: Supporting patterns in LiveQuery server's config parameter `classNames` [#7131](https://github.com/parse-community/parse-server/pull/7131). Thanks to [Nes-si](https://github.com/Nes-si)
|
- NEW: Supporting patterns in LiveQuery server's config parameter `classNames` [#7131](https://github.com/parse-community/parse-server/pull/7131). Thanks to [Nes-si](https://github.com/Nes-si)
|
||||||
- NEW: `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator. [#7097](https://github.com/parse-community/parse-server/pull/7097). Thanks to [dblythy](https://github.com/dblythy)
|
- NEW: `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator. [#7097](https://github.com/parse-community/parse-server/pull/7097). Thanks to [dblythy](https://github.com/dblythy)
|
||||||
|
- NEW: Support Facebook Limited Login [#7219](https://github.com/parse-community/parse-server/pull/7219). Thanks to [miguel-s](https://github.com/miguel-s)
|
||||||
- IMPROVE: Retry transactions on MongoDB when it fails due to transient error [#7187](https://github.com/parse-community/parse-server/pull/7187). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo).
|
- IMPROVE: Retry transactions on MongoDB when it fails due to transient error [#7187](https://github.com/parse-community/parse-server/pull/7187). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo).
|
||||||
- IMPROVE: Bump tests to use Mongo 4.4.4 [#7184](https://github.com/parse-community/parse-server/pull/7184). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo).
|
- IMPROVE: Bump tests to use Mongo 4.4.4 [#7184](https://github.com/parse-community/parse-server/pull/7184). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo).
|
||||||
- IMPROVE: Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset. [#7146](https://github.com/parse-community/parse-server/pull/7146). Thanks to [Manuel Trezza](https://github.com/mtrezza).
|
- IMPROVE: Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset. [#7146](https://github.com/parse-community/parse-server/pull/7146). Thanks to [Manuel Trezza](https://github.com/mtrezza).
|
||||||
|
|||||||
@@ -1756,3 +1756,387 @@ describe('microsoft graph auth adapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('facebook limited auth adapter', () => {
|
||||||
|
const facebook = require('../lib/Adapters/Auth/facebook');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
// 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 () => {
|
||||||
|
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'] });
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe('Facebook auth is not configured.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not decode 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('should throw error if public key used to encode token is not available', async () => {
|
||||||
|
const fakeDecodedToken = {
|
||||||
|
header: { kid: '789', alg: 'RS256' },
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
|
||||||
|
await facebook.validateAuthData(
|
||||||
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe(
|
||||||
|
`Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use algorithm from key header to verify id_token', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://facebook.com',
|
||||||
|
aud: 'secret',
|
||||||
|
exp: Date.now(),
|
||||||
|
sub: 'the_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = {
|
||||||
|
header: { kid: '123', alg: 'RS256' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
|
||||||
|
const result = await facebook.validateAuthData(
|
||||||
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
expect(result).toEqual(fakeClaim);
|
||||||
|
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not verify invalid id_token', async () => {
|
||||||
|
const fakeDecodedToken = {
|
||||||
|
header: { kid: '123', alg: 'RS256' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await facebook.validateAuthData(
|
||||||
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe('jwt malformed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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('(using client id as string) should verify id_token', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://facebook.com',
|
||||||
|
aud: 'secret',
|
||||||
|
exp: Date.now(),
|
||||||
|
sub: 'the_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = {
|
||||||
|
header: { kid: '123', alg: 'RS256' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
|
const result = await facebook.validateAuthData(
|
||||||
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
expect(result).toEqual(fakeClaim);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('(using client id as array) should verify id_token', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://facebook.com',
|
||||||
|
aud: 'secret',
|
||||||
|
exp: Date.now(),
|
||||||
|
sub: 'the_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = {
|
||||||
|
header: { kid: '123', alg: 'RS256' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
|
const result = await facebook.validateAuthData(
|
||||||
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
|
{ clientId: ['secret'] }
|
||||||
|
);
|
||||||
|
expect(result).toEqual(fakeClaim);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('(using client id as array with multiple items) should verify id_token', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://facebook.com',
|
||||||
|
aud: 'secret',
|
||||||
|
exp: Date.now(),
|
||||||
|
sub: 'the_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = {
|
||||||
|
header: { kid: '123', alg: 'RS256' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
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('(using client id as string) 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' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await facebook.validateAuthData(
|
||||||
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe(
|
||||||
|
'id token not issued by correct OpenID provider - expected: https://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 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' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
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://facebook.com | from: https://not.facebook.com'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('(using client id as string) 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' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
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://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 () => {
|
||||||
|
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'] }
|
||||||
|
);
|
||||||
|
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('should throw error with invalid user id', async () => {
|
||||||
|
try {
|
||||||
|
await facebook.validateAuthData(
|
||||||
|
{
|
||||||
|
id: 'invalid user',
|
||||||
|
token: 'INSERT FACEBOOK TOKEN HERE',
|
||||||
|
},
|
||||||
|
{ clientId: 'INSERT CLIENT ID HERE' }
|
||||||
|
);
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe('auth data is invalid for this user.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error with with invalid user id', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://facebook.com',
|
||||||
|
aud: 'invalid_client_id',
|
||||||
|
sub: 'a_different_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = {
|
||||||
|
header: { kid: '123', alg: 'RS256' },
|
||||||
|
};
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
const fakeGetSigningKeyAsyncFunction = () => {
|
||||||
|
return {
|
||||||
|
kid: '123',
|
||||||
|
rsaPublicKey: 'the_rsa_public_key',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await facebook.validateAuthData(
|
||||||
|
{ id: 'the_user_id', token: 'the_token' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe('auth data is invalid for this user.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
// Helper functions for accessing the Facebook Graph API.
|
// Helper functions for accessing the Facebook Graph API.
|
||||||
const httpsRequest = require('./httpsRequest');
|
const Parse = require('parse/node').Parse;
|
||||||
var Parse = require('parse/node').Parse;
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const jwksClient = require('jwks-rsa');
|
||||||
|
const util = require('util');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const httpsRequest = require('./httpsRequest');
|
||||||
|
|
||||||
|
const TOKEN_ISSUER = 'https://facebook.com';
|
||||||
|
|
||||||
function getAppSecretPath(authData, options = {}) {
|
function getAppSecretPath(authData, options = {}) {
|
||||||
const appSecret = options.appSecret;
|
const appSecret = options.appSecret;
|
||||||
@@ -16,8 +21,7 @@ function getAppSecretPath(authData, options = {}) {
|
|||||||
return `&appsecret_proof=${appsecret_proof}`;
|
return `&appsecret_proof=${appsecret_proof}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this user id is valid.
|
function validateGraphToken(authData, options) {
|
||||||
function validateAuthData(authData, options) {
|
|
||||||
return graphRequest(
|
return graphRequest(
|
||||||
'me?fields=id&access_token=' + authData.access_token + getAppSecretPath(authData, options)
|
'me?fields=id&access_token=' + authData.access_token + getAppSecretPath(authData, options)
|
||||||
).then(data => {
|
).then(data => {
|
||||||
@@ -28,8 +32,7 @@ function validateAuthData(authData, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills iff this app id is valid.
|
function validateGraphAppId(appIds, authData, options) {
|
||||||
function validateAppId(appIds, authData, options) {
|
|
||||||
var access_token = authData.access_token;
|
var access_token = authData.access_token;
|
||||||
if (process.env.TESTING && access_token === 'test') {
|
if (process.env.TESTING && access_token === 'test') {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -47,6 +50,95 @@ function validateAppId(appIds, authData, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||||
|
const client = jwksClient({
|
||||||
|
jwksUri: `${TOKEN_ISSUER}/.well-known/oauth/openid/jwks/`,
|
||||||
|
cache: true,
|
||||||
|
cacheMaxEntries,
|
||||||
|
cacheMaxAge,
|
||||||
|
});
|
||||||
|
|
||||||
|
const asyncGetSigningKeyFunction = util.promisify(client.getSigningKey);
|
||||||
|
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
key = await asyncGetSigningKeyFunction(keyId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
`Unable to find matching key for Key ID: ${keyId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeaderFromToken = token => {
|
||||||
|
const decodedToken = jwt.decode(token, { complete: true });
|
||||||
|
if (!decodedToken) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'provided token does not decode as JWT');
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodedToken.header;
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
|
||||||
|
if (!token) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
|
||||||
|
const ONE_HOUR_IN_MS = 3600000;
|
||||||
|
let jwtClaims;
|
||||||
|
|
||||||
|
cacheMaxAge = cacheMaxAge || ONE_HOUR_IN_MS;
|
||||||
|
cacheMaxEntries = cacheMaxEntries || 5;
|
||||||
|
|
||||||
|
const facebookKey = await getFacebookKeyByKeyId(keyId, cacheMaxEntries, cacheMaxAge);
|
||||||
|
const signingKey = facebookKey.publicKey || facebookKey.rsaPublicKey;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jwtClaims = jwt.verify(token, signingKey, {
|
||||||
|
algorithms: algorithm,
|
||||||
|
// the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
|
||||||
|
audience: clientId,
|
||||||
|
});
|
||||||
|
} catch (exception) {
|
||||||
|
const message = exception.message;
|
||||||
|
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwtClaims.iss !== TOKEN_ISSUER) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
`id token not issued by correct OpenID provider - expected: ${TOKEN_ISSUER} | from: ${jwtClaims.iss}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwtClaims.sub !== id) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'auth data is invalid for this user.');
|
||||||
|
}
|
||||||
|
return jwtClaims;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateAuthData(authData, options) {
|
||||||
|
if (authData.token) {
|
||||||
|
return verifyIdToken(authData, options);
|
||||||
|
} else {
|
||||||
|
return validateGraphToken(authData, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId(appIds, authData, options) {
|
||||||
|
if (authData.token) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
return validateGraphAppId(appIds, authData, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A promisey wrapper for FB graph requests.
|
// A promisey wrapper for FB graph requests.
|
||||||
function graphRequest(path) {
|
function graphRequest(path) {
|
||||||
return httpsRequest.get('https://graph.facebook.com/' + path);
|
return httpsRequest.get('https://graph.facebook.com/' + path);
|
||||||
|
|||||||
Reference in New Issue
Block a user