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:
@@ -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();
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
75
src/Adapters/Auth/facebookaccountkit.js
Normal file
75
src/Adapters/Auth/facebookaccountkit.js
Normal 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
|
||||||
|
};
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user