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:
@@ -1,7 +1,12 @@
|
||||
// Helper functions for accessing the Facebook Graph API.
|
||||
const httpsRequest = require('./httpsRequest');
|
||||
var Parse = require('parse/node').Parse;
|
||||
const Parse = require('parse/node').Parse;
|
||||
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 = {}) {
|
||||
const appSecret = options.appSecret;
|
||||
@@ -16,8 +21,7 @@ function getAppSecretPath(authData, options = {}) {
|
||||
return `&appsecret_proof=${appsecret_proof}`;
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData, options) {
|
||||
function validateGraphToken(authData, options) {
|
||||
return graphRequest(
|
||||
'me?fields=id&access_token=' + authData.access_token + getAppSecretPath(authData, options)
|
||||
).then(data => {
|
||||
@@ -28,8 +32,7 @@ function validateAuthData(authData, options) {
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
function validateAppId(appIds, authData, options) {
|
||||
function validateGraphAppId(appIds, authData, options) {
|
||||
var access_token = authData.access_token;
|
||||
if (process.env.TESTING && access_token === 'test') {
|
||||
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.
|
||||
function graphRequest(path) {
|
||||
return httpsRequest.get('https://graph.facebook.com/' + path);
|
||||
|
||||
Reference in New Issue
Block a user