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
This commit is contained in:
6thfdwp
2018-02-23 22:37:55 +10:00
committed by Florent Vilmart
parent 55f4b0f7c5
commit 213801c4b1
3 changed files with 144 additions and 2 deletions

View File

@@ -5,7 +5,7 @@ const authenticationLoader = require('../src/Adapters/Auth');
const path = require('path'); const path = require('path');
describe('AuthenticationProviders', function() { 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) => { it("Should validate structure of " + providerName, (done) => {
const provider = require("../src/Adapters/Auth/" + providerName); const provider = require("../src/Adapters/Auth/" + providerName);
jequal(typeof provider.validateAuthData, "function"); jequal(typeof provider.validateAuthData, "function");
@@ -345,4 +345,70 @@ describe('AuthenticationProviders', function() {
expect(appIds).toEqual(['a', 'b']); expect(appIds).toEqual(['a', 'b']);
expect(providerOptions).toEqual(options.custom); 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();
})
});
}); });

View File

@@ -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
};

View File

@@ -1,6 +1,7 @@
import loadAdapter from '../AdapterLoader'; import loadAdapter from '../AdapterLoader';
const facebook = require('./facebook'); const facebook = require('./facebook');
const facebookaccountkit = require('./facebookaccountkit');
const instagram = require("./instagram"); const instagram = require("./instagram");
const linkedin = require("./linkedin"); const linkedin = require("./linkedin");
const meetup = require("./meetup"); const meetup = require("./meetup");
@@ -27,6 +28,7 @@ const anonymous = {
const providers = { const providers = {
facebook, facebook,
facebookaccountkit,
instagram, instagram,
linkedin, linkedin,
meetup, meetup,
@@ -43,7 +45,6 @@ const providers = {
wechat, wechat,
weibo weibo
} }
function authDataValidator(adapter, appIds, options) { function authDataValidator(adapter, appIds, options) {
return function(authData) { return function(authData) {
return adapter.validateAuthData(authData, options).then(() => { return adapter.validateAuthData(authData, options).then(() => {