diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index 73154e4a..48230777 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -3,9 +3,19 @@ const Config = require("../lib/Config"); const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns; const authenticationLoader = require('../lib/Adapters/Auth'); const path = require('path'); +const responses = { + instagram: { data: { id: 'userId' } }, + janrainengage: { stat: 'ok', profile: { identifier: 'userId' }}, + janraincapture: { stat: 'ok', result: 'userId' }, + vkontakte: { response: { user_id: 'userId'}}, + google: { sub: 'userId' }, + wechat: { errcode: 0 }, + weibo: { uid: 'userId' }, + qq: 'callback( {"openid":"userId"} );' // yes it's like that, run eval in the client :P +} describe('AuthenticationProviders', function() { - ["facebook", "facebookaccountkit", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].map(function(providerName){ + ["facebook", "facebookaccountkit", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte", "qq", "spotify", "wechat", "weibo"].map(function(providerName){ it("Should validate structure of " + providerName, (done) => { const provider = require("../lib/Adapters/Auth/" + providerName); jequal(typeof provider.validateAuthData, "function"); @@ -18,6 +28,32 @@ describe('AuthenticationProviders', function() { validateAppIdPromise.then(()=>{}, ()=>{}); done(); }); + + it(`should provide the right responses for adapter ${providerName}`, async () => { + if (providerName === 'twitter') { + return; + } + spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake((options) => { + if (options === "https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.59&grant_type=client_credentials") { + return { + access_token: 'access_token' + } + } + return Promise.resolve(responses[providerName] || { id: 'userId' }); + }); + spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'request').and.callFake(() => { + return Promise.resolve(responses[providerName] || { id: 'userId' }); + }); + const provider = require("../lib/Adapters/Auth/" + providerName); + let params = {}; + if (providerName === 'vkontakte') { + params = { + appIds: 'appId', + appSecret: 'appSecret' + } + } + await provider.validateAuthData({ id: 'userId' }, params); + }); }); const getMockMyOauthProvider = function() { @@ -388,3 +424,60 @@ describe('AuthenticationProviders', function() { }) }); }); + +describe('google auth adapter', () => { + const google = require('../lib/Adapters/Auth/google'); + const httpsRequest = require('../lib/Adapters/Auth/httpsRequest'); + + it('should use id_token for validation is passed', async () => { + spyOn(httpsRequest, 'request').and.callFake(() => { + return Promise.resolve({ sub: 'userId' }); + }); + await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {}); + }); + + it('should use id_token for validation is passed and responds with user_id', async () => { + spyOn(httpsRequest, 'request').and.callFake(() => { + return Promise.resolve({ user_id: 'userId' }); + }); + await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {}); + }); + + it('should use access_token for validation is passed and responds with user_id', async () => { + spyOn(httpsRequest, 'request').and.callFake(() => { + return Promise.resolve({ user_id: 'userId' }); + }); + await google.validateAuthData({ id: 'userId', access_token: 'the_token' }, {}); + }); + + it('should use access_token for validation is passed with sub', async () => { + spyOn(httpsRequest, 'request').and.callFake(() => { + return Promise.resolve({ sub: 'userId' }); + }); + await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {}); + }); + + it('should fail when the id_token is invalid', async () => { + spyOn(httpsRequest, 'request').and.callFake(() => { + return Promise.resolve({ sub: 'badId' }); + }); + try { + await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {}); + fail() + } catch(e) { + expect(e.message).toBe('Google auth is invalid for this user.'); + } + }); + + it('should fail when the access_token is invalid', async () => { + spyOn(httpsRequest, 'request').and.callFake(() => { + return Promise.resolve({ sub: 'badId' }); + }); + try { + await google.validateAuthData({ id: 'userId', access_token: 'the_token' }, {}); + fail() + } catch(e) { + expect(e.message).toBe('Google auth is invalid for this user.'); + } + }); +}); diff --git a/src/Adapters/Auth/facebook.js b/src/Adapters/Auth/facebook.js index b36f5901..c2476e4a 100644 --- a/src/Adapters/Auth/facebook.js +++ b/src/Adapters/Auth/facebook.js @@ -1,5 +1,5 @@ // Helper functions for accessing the Facebook Graph API. -var https = require('https'); +const httpsRequest = require('./httpsRequest'); var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -36,24 +36,7 @@ function validateAppId(appIds, authData) { // A promisey wrapper for FB graph requests. function graphRequest(path) { - return new Promise(function(resolve, reject) { - https.get('https://graph.facebook.com/' + path, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Facebook.'); - }); - }); + return httpsRequest.get('https://graph.facebook.com/' + path); } module.exports = { diff --git a/src/Adapters/Auth/facebookaccountkit.js b/src/Adapters/Auth/facebookaccountkit.js index ee26c3e5..94a05e4b 100644 --- a/src/Adapters/Auth/facebookaccountkit.js +++ b/src/Adapters/Auth/facebookaccountkit.js @@ -1,32 +1,9 @@ const crypto = require('crypto'); -const https = require('https'); +const httpsRequest = require('./httpsRequest'); 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.'); - }); - }); + return httpsRequest.get(`https://graph.accountkit.com/v1.1/${path}`); }; function getRequestPath(authData, options) { @@ -60,6 +37,9 @@ function validateAppId(appIds, authData, options) { function validateAuthData(authData, options) { return graphRequest(getRequestPath(authData, options)) .then(data => { + if (data && data.error) { + throw data.error; + } if (data && data.id == authData.id) { return; } diff --git a/src/Adapters/Auth/github.js b/src/Adapters/Auth/github.js index 146fbdc6..2936af86 100644 --- a/src/Adapters/Auth/github.js +++ b/src/Adapters/Auth/github.js @@ -1,6 +1,6 @@ // Helper functions for accessing the github API. -var https = require('https'); var Parse = require('parse/node').Parse; +const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { @@ -22,30 +22,13 @@ function validateAppId() { // A promisey wrapper for api requests function request(path, access_token) { - return new Promise(function(resolve, reject) { - https.get({ - host: 'api.github.com', - path: '/' + path, - headers: { - 'Authorization': 'bearer ' + access_token, - 'User-Agent': 'parse-server' - } - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Github.'); - }); + return httpsRequest.get({ + host: 'api.github.com', + path: '/' + path, + headers: { + 'Authorization': 'bearer ' + access_token, + 'User-Agent': 'parse-server' + } }); } diff --git a/src/Adapters/Auth/google.js b/src/Adapters/Auth/google.js index 7cc41492..a042aeee 100644 --- a/src/Adapters/Auth/google.js +++ b/src/Adapters/Auth/google.js @@ -1,9 +1,9 @@ // Helper functions for accessing the google API. -var https = require('https'); var Parse = require('parse/node').Parse; +const httpsRequest = require('./httpsRequest'); function validateIdToken(id, token) { - return request("tokeninfo?id_token=" + token) + return googleRequest("tokeninfo?id_token=" + token) .then((response) => { if (response && (response.sub == id || response.user_id == id)) { return; @@ -15,7 +15,7 @@ function validateIdToken(id, token) { } function validateAuthToken(id, token) { - return request("tokeninfo?access_token=" + token) + return googleRequest("tokeninfo?access_token=" + token) .then((response) => { if (response && (response.sub == id || response.user_id == id)) { return; @@ -47,25 +47,8 @@ function validateAppId() { } // A promisey wrapper for api requests -function request(path) { - return new Promise(function(resolve, reject) { - https.get("https://www.googleapis.com/oauth2/v3/" + path, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Google.'); - }); - }); +function googleRequest(path) { + return httpsRequest.request("https://www.googleapis.com/oauth2/v3/" + path); } module.exports = { diff --git a/src/Adapters/Auth/httpsRequest.js b/src/Adapters/Auth/httpsRequest.js new file mode 100644 index 00000000..193d57e3 --- /dev/null +++ b/src/Adapters/Auth/httpsRequest.js @@ -0,0 +1,41 @@ +const https = require('https'); + +function makeCallback(resolve, reject, noJSON) { + return function(res) { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + if (noJSON) { + return resolve(data); + } + try { + data = JSON.parse(data); + } catch(e) { + return reject(e); + } + resolve(data); + }); + res.on('error', reject); + }; +} + +function get(options, noJSON = false) { + return new Promise((resolve, reject) => { + https + .get(options, makeCallback(resolve, reject, noJSON)) + .on('error', reject); + }); +} + +function request(options, postData) { + return new Promise((resolve, reject) => { + const req = https.request(options, makeCallback(resolve, reject)); + req.on('error', reject); + req.write(postData); + req.end(); + }); +} + +module.exports = { get, request }; diff --git a/src/Adapters/Auth/instagram.js b/src/Adapters/Auth/instagram.js index 1c6c0f73..2bf6ffaa 100644 --- a/src/Adapters/Auth/instagram.js +++ b/src/Adapters/Auth/instagram.js @@ -1,6 +1,6 @@ // Helper functions for accessing the instagram API. -var https = require('https'); var Parse = require('parse/node').Parse; +const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { @@ -22,20 +22,7 @@ function validateAppId() { // A promisey wrapper for api requests function request(path) { - return new Promise(function(resolve, reject) { - https.get("https://api.instagram.com/v1/" + path, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - data = JSON.parse(data); - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Instagram.'); - }); - }); + return httpsRequest.get("https://api.instagram.com/v1/" + path); } module.exports = { diff --git a/src/Adapters/Auth/janraincapture.js b/src/Adapters/Auth/janraincapture.js index 85ae98da..05b33b71 100644 --- a/src/Adapters/Auth/janraincapture.js +++ b/src/Adapters/Auth/janraincapture.js @@ -1,7 +1,7 @@ // Helper functions for accessing the Janrain Capture API. -var https = require('https'); var Parse = require('parse/node').Parse; var querystring = require('querystring'); +const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData, options) { @@ -30,22 +30,7 @@ function request(host, access_token) { 'attribute_name': 'uuid' // we only need to pull the uuid for this access token to make sure it matches }); - return new Promise(function(resolve, reject) { - https.get({ - host: host, - path: '/entity?' + query_string_data - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function () { - resolve(JSON.parse(data)); - }); - }).on('error', function() { - reject('Failed to validate this access token with Janrain capture.'); - }); - }); + return httpsRequest.get({ host: host, path: '/entity?' + query_string_data }); } module.exports = { diff --git a/src/Adapters/Auth/janrainengage.js b/src/Adapters/Auth/janrainengage.js index 7de682e7..0c0bcde3 100644 --- a/src/Adapters/Auth/janrainengage.js +++ b/src/Adapters/Auth/janrainengage.js @@ -1,11 +1,11 @@ // Helper functions for accessing the Janrain Engage API. -var https = require('https'); +var httpsRequest = require('./httpsRequest'); var Parse = require('parse/node').Parse; var querystring = require('querystring'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData, options) { - return request(options.api_key, authData.auth_token) + return apiRequest(options.api_key, authData.auth_token) .then((data) => { //successful response will have a "stat" (status) of 'ok' and a profile node with an identifier //see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data @@ -23,7 +23,7 @@ function validateAppId() { } // A promisey wrapper for api requests -function request(api_key, auth_token) { +function apiRequest(api_key, auth_token) { var post_data = querystring.stringify({ 'token': auth_token, @@ -41,29 +41,7 @@ function request(api_key, auth_token) { } }; - return new Promise(function (resolve, reject) { - // Create the post request. - var post_req = https.request(post_options, function (res) { - var data = ''; - res.setEncoding('utf8'); - // Append data as we receive it from the Janrain engage server. - res.on('data', function (d) { - data += d; - }); - // Once we have all the data, we can parse it and return the data we want. - res.on('end', function () { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }); - - post_req.write(post_data); - post_req.end(); - }); + return httpsRequest.request(post_options, post_data); } module.exports = { diff --git a/src/Adapters/Auth/linkedin.js b/src/Adapters/Auth/linkedin.js index de5fc66c..8f6a8377 100644 --- a/src/Adapters/Auth/linkedin.js +++ b/src/Adapters/Auth/linkedin.js @@ -1,6 +1,6 @@ // Helper functions for accessing the linkedin API. -var https = require('https'); var Parse = require('parse/node').Parse; +const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { @@ -30,28 +30,10 @@ function request(path, access_token, is_mobile_sdk) { if(is_mobile_sdk) { headers['x-li-src'] = 'msdk'; } - - return new Promise(function(resolve, reject) { - https.get({ - host: 'api.linkedin.com', - path: '/v1/' + path, - headers: headers - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Linkedin.'); - }); + return httpsRequest.get({ + host: 'api.linkedin.com', + path: '/v1/' + path, + headers: headers }); } diff --git a/src/Adapters/Auth/meetup.js b/src/Adapters/Auth/meetup.js index bb14dc54..a484e68c 100644 --- a/src/Adapters/Auth/meetup.js +++ b/src/Adapters/Auth/meetup.js @@ -1,6 +1,6 @@ // Helper functions for accessing the meetup API. -var https = require('https'); var Parse = require('parse/node').Parse; +const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { @@ -22,29 +22,12 @@ function validateAppId() { // A promisey wrapper for api requests function request(path, access_token) { - return new Promise(function(resolve, reject) { - https.get({ - host: 'api.meetup.com', - path: '/2/' + path, - headers: { - 'Authorization': 'bearer ' + access_token - } - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Meetup.'); - }); + return httpsRequest.get({ + host: 'api.meetup.com', + path: '/2/' + path, + headers: { + 'Authorization': 'bearer ' + access_token + } }); } diff --git a/src/Adapters/Auth/qq.js b/src/Adapters/Auth/qq.js index 6f4dfdc0..a64193cf 100644 --- a/src/Adapters/Auth/qq.js +++ b/src/Adapters/Auth/qq.js @@ -1,5 +1,5 @@ // Helper functions for accessing the qq Graph API. -var https = require('https'); +const httpsRequest = require('./httpsRequest'); var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -19,33 +19,23 @@ function validateAppId() { // A promisey wrapper for qq graph requests. function graphRequest(path) { - return new Promise(function (resolve, reject) { - https.get('https://graph.qq.com/oauth2.0/' + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - var starPos = data.indexOf("("); - var endPos = data.indexOf(")"); - if(starPos == -1 || endPos == -1){ - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); - } - data = data.substring(starPos + 1,endPos - 1); - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with qq.'); - }); + return httpsRequest.get('https://graph.qq.com/oauth2.0/' + path, true).then((data) => { + return parseResponseData(data); }); } +function parseResponseData(data) { + const starPos = data.indexOf("("); + const endPos = data.indexOf(")"); + if(starPos == -1 || endPos == -1){ + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); + } + data = data.substring(starPos + 1,endPos - 1); + return JSON.parse(data); +} + module.exports = { validateAppId, - validateAuthData + validateAuthData, + parseResponseData, }; diff --git a/src/Adapters/Auth/spotify.js b/src/Adapters/Auth/spotify.js index 701422c5..f30bea09 100644 --- a/src/Adapters/Auth/spotify.js +++ b/src/Adapters/Auth/spotify.js @@ -1,5 +1,5 @@ // Helper functions for accessing the Spotify API. -var https = require('https'); +const httpsRequest = require('./httpsRequest'); var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -36,29 +36,12 @@ function validateAppId(appIds, authData) { // A promisey wrapper for Spotify API requests. function request(path, access_token) { - return new Promise(function(resolve, reject) { - https.get({ - host: 'api.spotify.com', - path: '/v1/' + path, - headers: { - 'Authorization': 'Bearer ' + access_token - } - }, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function() { - reject('Failed to validate this access token with Spotify.'); - }); + return httpsRequest.get({ + host: 'api.spotify.com', + path: '/v1/' + path, + headers: { + 'Authorization': 'Bearer ' + access_token + } }); } diff --git a/src/Adapters/Auth/vkontakte.js b/src/Adapters/Auth/vkontakte.js index b43b60f1..e687573e 100644 --- a/src/Adapters/Auth/vkontakte.js +++ b/src/Adapters/Auth/vkontakte.js @@ -2,7 +2,7 @@ // Helper functions for accessing the vkontakte API. -var https = require('https'); +const httpsRequest = require('./httpsRequest'); var Parse = require('parse/node').Parse; var logger = require('../../logger').default; @@ -41,24 +41,7 @@ function validateAppId() { // A promisey wrapper for api requests function request(host, path) { - return new Promise(function (resolve, reject) { - https.get("https://" + host + "/" + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Vk.'); - }); - }); + return httpsRequest.get("https://" + host + "/" + path); } module.exports = { diff --git a/src/Adapters/Auth/wechat.js b/src/Adapters/Auth/wechat.js index e42d5c36..d192df25 100644 --- a/src/Adapters/Auth/wechat.js +++ b/src/Adapters/Auth/wechat.js @@ -1,5 +1,5 @@ // Helper functions for accessing the WeChat Graph API. -var https = require('https'); +const httpsRequest = require('./httpsRequest'); var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. @@ -19,24 +19,7 @@ function validateAppId() { // A promisey wrapper for WeChat graph requests. function graphRequest(path) { - return new Promise(function (resolve, reject) { - https.get('https://api.weixin.qq.com/sns/' + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with wechat.'); - }); - }); + return httpsRequest.get('https://api.weixin.qq.com/sns/' + path); } module.exports = { diff --git a/src/Adapters/Auth/weibo.js b/src/Adapters/Auth/weibo.js index 64efada2..8d4a3a10 100644 --- a/src/Adapters/Auth/weibo.js +++ b/src/Adapters/Auth/weibo.js @@ -1,5 +1,5 @@ // Helper functions for accessing the weibo Graph API. -var https = require('https'); +var httpsRequest = require('./httpsRequest'); var Parse = require('parse/node').Parse; var querystring = require('querystring'); @@ -20,42 +20,19 @@ function validateAppId() { // A promisey wrapper for weibo graph requests. function graphRequest(access_token) { - return new Promise(function (resolve, reject) { - var postData = querystring.stringify({ - "access_token":access_token - }); - var options = { - hostname: 'api.weibo.com', - path: '/oauth2/get_token_info', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(postData) - } - }; - var req = https.request(options, function(res){ - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch(e) { - return reject(e); - } - resolve(data); - }); - res.on('error', function () { - reject('Failed to validate this access token with weibo.'); - }); - }); - req.on('error', function () { - reject('Failed to validate this access token with weibo.'); - }); - req.write(postData); - req.end(); + var postData = querystring.stringify({ + "access_token": access_token }); + var options = { + hostname: 'api.weibo.com', + path: '/oauth2/get_token_info', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } + }; + return httpsRequest.request(options, postData); } module.exports = {