From 213801c4b1ff1ee5c93bb643d47e2413f4349620 Mon Sep 17 00:00:00 2001 From: 6thfdwp <6thfdwp@gmail.com> Date: Fri, 23 Feb 2018 22:37:55 +1000 Subject: [PATCH] auth: add adapter for Facebook accountkit login (#4434) * Integrate auth adapter for Facebook accountkit login * Also verify Facebook app id associated with account kit login * Add appsecret_proof as extra graph request parameter * Specific error message for Account kit and more test coverage * One more test to cover when AppIds for Facebook account kit not configured properly --- spec/AuthenticationAdapters.spec.js | 68 +++++++++++++++++++++- src/Adapters/Auth/facebookaccountkit.js | 75 +++++++++++++++++++++++++ src/Adapters/Auth/index.js | 3 +- 3 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 src/Adapters/Auth/facebookaccountkit.js diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index ad182ec4..ad77d0b7 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -5,7 +5,7 @@ const authenticationLoader = require('../src/Adapters/Auth'); const path = require('path'); describe('AuthenticationProviders', function() { - ["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].map(function(providerName){ + ["facebook", "facebookaccountkit", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].map(function(providerName){ it("Should validate structure of " + providerName, (done) => { const provider = require("../src/Adapters/Auth/" + providerName); jequal(typeof provider.validateAuthData, "function"); @@ -345,4 +345,70 @@ describe('AuthenticationProviders', function() { expect(appIds).toEqual(['a', 'b']); expect(providerOptions).toEqual(options.custom); }); + + it('properly loads Facebook accountkit adapter with options', () => { + const options = { + facebookaccountkit: { + appIds: ['a', 'b'], + appSecret: 'secret' + } + }; + const {adapter, appIds, providerOptions} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); + validateAuthenticationAdapter(adapter); + expect(appIds).toEqual(['a', 'b']); + expect(providerOptions.appSecret).toEqual('secret'); + }); + + it('should fail if Facebook appIds is not configured properly', (done) => { + const options = { + facebookaccountkit: { + appIds: [] + } + }; + const {adapter, appIds} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); + adapter.validateAppId(appIds) + .then(done.fail, err => { + expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + done(); + }) + }); + + it('should fail to validate Facebook accountkit auth with bad token', (done) => { + const options = { + facebookaccountkit: { + appIds: ['a', 'b'] + } + }; + const authData = { + id: 'fakeid', + access_token: 'badtoken' + }; + const {adapter} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); + adapter.validateAuthData(authData) + .then(done.fail, err => { + expect(err.code).toBe(190); + expect(err.type).toBe('OAuthException'); + done(); + }) + }); + + it('should fail to validate Facebook accountkit auth with bad token regardless of app secret proof', (done) => { + const options = { + facebookaccountkit: { + appIds: ['a', 'b'], + appSecret: 'badsecret' + } + }; + const authData = { + id: 'fakeid', + access_token: 'badtoken' + }; + const {adapter, providerOptions} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); + adapter.validateAuthData(authData, providerOptions) + .then(done.fail, err => { + expect(err.code).toBe(190); + expect(err.type).toBe('OAuthException'); + done(); + }) + }); }); diff --git a/src/Adapters/Auth/facebookaccountkit.js b/src/Adapters/Auth/facebookaccountkit.js new file mode 100644 index 00000000..ee26c3e5 --- /dev/null +++ b/src/Adapters/Auth/facebookaccountkit.js @@ -0,0 +1,75 @@ +const crypto = require('crypto'); +const https = require('https'); +const Parse = require('parse/node').Parse; + +const graphRequest = (path) => { + return new Promise((resolve, reject) => { + https.get(`https://graph.accountkit.com/v1.1/${path}`, (res) => { + var data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + try { + data = JSON.parse(data); + if (data.error) { + // when something wrong with fb graph request (token corrupted etc.) + // instead of network issue + reject(data.error); + } else { + resolve(data); + } + } catch (e) { + reject(e); + } + }); + }).on('error', function () { + reject('Failed to validate this access token with Facebook Account Kit.'); + }); + }); +}; + +function getRequestPath(authData, options) { + const access_token = authData.access_token, appSecret = options && options.appSecret; + if (appSecret) { + const appsecret_proof = crypto.createHmac("sha256", appSecret).update(access_token).digest('hex'); + return `me?access_token=${access_token}&appsecret_proof=${appsecret_proof}` + } + return `me?access_token=${access_token}`; +} + +function validateAppId(appIds, authData, options) { + if (!appIds.length) { + return Promise.reject( + new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook app id for Account Kit is not configured.') + ) + } + return graphRequest(getRequestPath(authData, options)) + .then(data => { + if (data && data.application && appIds.indexOf(data.application.id) != -1) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook app id for Account Kit is invalid for this user.'); + }) +} + +function validateAuthData(authData, options) { + return graphRequest(getRequestPath(authData, options)) + .then(data => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook Account Kit auth is invalid for this user.'); + }) +} + +module.exports = { + validateAppId, + validateAuthData +}; diff --git a/src/Adapters/Auth/index.js b/src/Adapters/Auth/index.js index aa99f09c..77101935 100755 --- a/src/Adapters/Auth/index.js +++ b/src/Adapters/Auth/index.js @@ -1,6 +1,7 @@ import loadAdapter from '../AdapterLoader'; const facebook = require('./facebook'); +const facebookaccountkit = require('./facebookaccountkit'); const instagram = require("./instagram"); const linkedin = require("./linkedin"); const meetup = require("./meetup"); @@ -27,6 +28,7 @@ const anonymous = { const providers = { facebook, + facebookaccountkit, instagram, linkedin, meetup, @@ -43,7 +45,6 @@ const providers = { wechat, weibo } - function authDataValidator(adapter, appIds, options) { return function(authData) { return adapter.validateAuthData(authData, options).then(() => {