Skip to content

Commit b9673da

Browse files
authored
Refactor all auth adapters to reduce duplications (#4954)
* Refactor all auth adapters to reduce duplications * Adds mocking and proper testing for all auth adapters * Proper testing of the google auth adapter * noit
1 parent f1b0083 commit b9673da

16 files changed

+214
-320
lines changed

spec/AuthenticationAdapters.spec.js

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@ const Config = require("../lib/Config");
33
const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns;
44
const authenticationLoader = require('../lib/Adapters/Auth');
55
const path = require('path');
6+
const responses = {
7+
instagram: { data: { id: 'userId' } },
8+
janrainengage: { stat: 'ok', profile: { identifier: 'userId' }},
9+
janraincapture: { stat: 'ok', result: 'userId' },
10+
vkontakte: { response: { user_id: 'userId'}},
11+
google: { sub: 'userId' },
12+
wechat: { errcode: 0 },
13+
weibo: { uid: 'userId' },
14+
qq: 'callback( {"openid":"userId"} );' // yes it's like that, run eval in the client :P
15+
}
616

717
describe('AuthenticationProviders', function() {
8-
["facebook", "facebookaccountkit", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].map(function(providerName){
18+
["facebook", "facebookaccountkit", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte", "qq", "spotify", "wechat", "weibo"].map(function(providerName){
919
it("Should validate structure of " + providerName, (done) => {
1020
const provider = require("../lib/Adapters/Auth/" + providerName);
1121
jequal(typeof provider.validateAuthData, "function");
@@ -18,6 +28,32 @@ describe('AuthenticationProviders', function() {
1828
validateAppIdPromise.then(()=>{}, ()=>{});
1929
done();
2030
});
31+
32+
it(`should provide the right responses for adapter ${providerName}`, async () => {
33+
if (providerName === 'twitter') {
34+
return;
35+
}
36+
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake((options) => {
37+
if (options === "https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.59&grant_type=client_credentials") {
38+
return {
39+
access_token: 'access_token'
40+
}
41+
}
42+
return Promise.resolve(responses[providerName] || { id: 'userId' });
43+
});
44+
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'request').and.callFake(() => {
45+
return Promise.resolve(responses[providerName] || { id: 'userId' });
46+
});
47+
const provider = require("../lib/Adapters/Auth/" + providerName);
48+
let params = {};
49+
if (providerName === 'vkontakte') {
50+
params = {
51+
appIds: 'appId',
52+
appSecret: 'appSecret'
53+
}
54+
}
55+
await provider.validateAuthData({ id: 'userId' }, params);
56+
});
2157
});
2258

2359
const getMockMyOauthProvider = function() {
@@ -388,3 +424,60 @@ describe('AuthenticationProviders', function() {
388424
})
389425
});
390426
});
427+
428+
describe('google auth adapter', () => {
429+
const google = require('../lib/Adapters/Auth/google');
430+
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
431+
432+
it('should use id_token for validation is passed', async () => {
433+
spyOn(httpsRequest, 'request').and.callFake(() => {
434+
return Promise.resolve({ sub: 'userId' });
435+
});
436+
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
437+
});
438+
439+
it('should use id_token for validation is passed and responds with user_id', async () => {
440+
spyOn(httpsRequest, 'request').and.callFake(() => {
441+
return Promise.resolve({ user_id: 'userId' });
442+
});
443+
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
444+
});
445+
446+
it('should use access_token for validation is passed and responds with user_id', async () => {
447+
spyOn(httpsRequest, 'request').and.callFake(() => {
448+
return Promise.resolve({ user_id: 'userId' });
449+
});
450+
await google.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
451+
});
452+
453+
it('should use access_token for validation is passed with sub', async () => {
454+
spyOn(httpsRequest, 'request').and.callFake(() => {
455+
return Promise.resolve({ sub: 'userId' });
456+
});
457+
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
458+
});
459+
460+
it('should fail when the id_token is invalid', async () => {
461+
spyOn(httpsRequest, 'request').and.callFake(() => {
462+
return Promise.resolve({ sub: 'badId' });
463+
});
464+
try {
465+
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
466+
fail()
467+
} catch(e) {
468+
expect(e.message).toBe('Google auth is invalid for this user.');
469+
}
470+
});
471+
472+
it('should fail when the access_token is invalid', async () => {
473+
spyOn(httpsRequest, 'request').and.callFake(() => {
474+
return Promise.resolve({ sub: 'badId' });
475+
});
476+
try {
477+
await google.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
478+
fail()
479+
} catch(e) {
480+
expect(e.message).toBe('Google auth is invalid for this user.');
481+
}
482+
});
483+
});

src/Adapters/Auth/facebook.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Helper functions for accessing the Facebook Graph API.
2-
var https = require('https');
2+
const httpsRequest = require('./httpsRequest');
33
var Parse = require('parse/node').Parse;
44

55
// Returns a promise that fulfills iff this user id is valid.
@@ -36,24 +36,7 @@ function validateAppId(appIds, authData) {
3636

3737
// A promisey wrapper for FB graph requests.
3838
function graphRequest(path) {
39-
return new Promise(function(resolve, reject) {
40-
https.get('https://graph.facebook.com/' + path, function(res) {
41-
var data = '';
42-
res.on('data', function(chunk) {
43-
data += chunk;
44-
});
45-
res.on('end', function() {
46-
try {
47-
data = JSON.parse(data);
48-
} catch(e) {
49-
return reject(e);
50-
}
51-
resolve(data);
52-
});
53-
}).on('error', function() {
54-
reject('Failed to validate this access token with Facebook.');
55-
});
56-
});
39+
return httpsRequest.get('https://graph.facebook.com/' + path);
5740
}
5841

5942
module.exports = {

src/Adapters/Auth/facebookaccountkit.js

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,9 @@
11
const crypto = require('crypto');
2-
const https = require('https');
2+
const httpsRequest = require('./httpsRequest');
33
const Parse = require('parse/node').Parse;
44

55
const graphRequest = (path) => {
6-
return new Promise((resolve, reject) => {
7-
https.get(`https://graph.accountkit.com/v1.1/${path}`, (res) => {
8-
var data = '';
9-
res.on('data', (chunk) => {
10-
data += chunk;
11-
});
12-
res.on('end', () => {
13-
try {
14-
data = JSON.parse(data);
15-
if (data.error) {
16-
// when something wrong with fb graph request (token corrupted etc.)
17-
// instead of network issue
18-
reject(data.error);
19-
} else {
20-
resolve(data);
21-
}
22-
} catch (e) {
23-
reject(e);
24-
}
25-
});
26-
}).on('error', function () {
27-
reject('Failed to validate this access token with Facebook Account Kit.');
28-
});
29-
});
6+
return httpsRequest.get(`https://graph.accountkit.com/v1.1/${path}`);
307
};
318

329
function getRequestPath(authData, options) {
@@ -60,6 +37,9 @@ function validateAppId(appIds, authData, options) {
6037
function validateAuthData(authData, options) {
6138
return graphRequest(getRequestPath(authData, options))
6239
.then(data => {
40+
if (data && data.error) {
41+
throw data.error;
42+
}
6343
if (data && data.id == authData.id) {
6444
return;
6545
}

src/Adapters/Auth/github.js

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Helper functions for accessing the github API.
2-
var https = require('https');
32
var Parse = require('parse/node').Parse;
3+
const httpsRequest = require('./httpsRequest');
44

55
// Returns a promise that fulfills iff this user id is valid.
66
function validateAuthData(authData) {
@@ -22,30 +22,13 @@ function validateAppId() {
2222

2323
// A promisey wrapper for api requests
2424
function request(path, access_token) {
25-
return new Promise(function(resolve, reject) {
26-
https.get({
27-
host: 'api.github.com',
28-
path: '/' + path,
29-
headers: {
30-
'Authorization': 'bearer ' + access_token,
31-
'User-Agent': 'parse-server'
32-
}
33-
}, function(res) {
34-
var data = '';
35-
res.on('data', function(chunk) {
36-
data += chunk;
37-
});
38-
res.on('end', function() {
39-
try {
40-
data = JSON.parse(data);
41-
} catch(e) {
42-
return reject(e);
43-
}
44-
resolve(data);
45-
});
46-
}).on('error', function() {
47-
reject('Failed to validate this access token with Github.');
48-
});
25+
return httpsRequest.get({
26+
host: 'api.github.com',
27+
path: '/' + path,
28+
headers: {
29+
'Authorization': 'bearer ' + access_token,
30+
'User-Agent': 'parse-server'
31+
}
4932
});
5033
}
5134

src/Adapters/Auth/google.js

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// Helper functions for accessing the google API.
2-
var https = require('https');
32
var Parse = require('parse/node').Parse;
3+
const httpsRequest = require('./httpsRequest');
44

55
function validateIdToken(id, token) {
6-
return request("tokeninfo?id_token=" + token)
6+
return googleRequest("tokeninfo?id_token=" + token)
77
.then((response) => {
88
if (response && (response.sub == id || response.user_id == id)) {
99
return;
@@ -15,7 +15,7 @@ function validateIdToken(id, token) {
1515
}
1616

1717
function validateAuthToken(id, token) {
18-
return request("tokeninfo?access_token=" + token)
18+
return googleRequest("tokeninfo?access_token=" + token)
1919
.then((response) => {
2020
if (response && (response.sub == id || response.user_id == id)) {
2121
return;
@@ -47,25 +47,8 @@ function validateAppId() {
4747
}
4848

4949
// A promisey wrapper for api requests
50-
function request(path) {
51-
return new Promise(function(resolve, reject) {
52-
https.get("https://www.googleapis.com/oauth2/v3/" + path, function(res) {
53-
var data = '';
54-
res.on('data', function(chunk) {
55-
data += chunk;
56-
});
57-
res.on('end', function() {
58-
try {
59-
data = JSON.parse(data);
60-
} catch(e) {
61-
return reject(e);
62-
}
63-
resolve(data);
64-
});
65-
}).on('error', function() {
66-
reject('Failed to validate this access token with Google.');
67-
});
68-
});
50+
function googleRequest(path) {
51+
return httpsRequest.request("https://www.googleapis.com/oauth2/v3/" + path);
6952
}
7053

7154
module.exports = {

src/Adapters/Auth/httpsRequest.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const https = require('https');
2+
3+
function makeCallback(resolve, reject, noJSON) {
4+
return function(res) {
5+
let data = '';
6+
res.on('data', (chunk) => {
7+
data += chunk;
8+
});
9+
res.on('end', () => {
10+
if (noJSON) {
11+
return resolve(data);
12+
}
13+
try {
14+
data = JSON.parse(data);
15+
} catch(e) {
16+
return reject(e);
17+
}
18+
resolve(data);
19+
});
20+
res.on('error', reject);
21+
};
22+
}
23+
24+
function get(options, noJSON = false) {
25+
return new Promise((resolve, reject) => {
26+
https
27+
.get(options, makeCallback(resolve, reject, noJSON))
28+
.on('error', reject);
29+
});
30+
}
31+
32+
function request(options, postData) {
33+
return new Promise((resolve, reject) => {
34+
const req = https.request(options, makeCallback(resolve, reject));
35+
req.on('error', reject);
36+
req.write(postData);
37+
req.end();
38+
});
39+
}
40+
41+
module.exports = { get, request };

src/Adapters/Auth/instagram.js

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Helper functions for accessing the instagram API.
2-
var https = require('https');
32
var Parse = require('parse/node').Parse;
3+
const httpsRequest = require('./httpsRequest');
44

55
// Returns a promise that fulfills iff this user id is valid.
66
function validateAuthData(authData) {
@@ -22,20 +22,7 @@ function validateAppId() {
2222

2323
// A promisey wrapper for api requests
2424
function request(path) {
25-
return new Promise(function(resolve, reject) {
26-
https.get("https://api.instagram.com/v1/" + path, function(res) {
27-
var data = '';
28-
res.on('data', function(chunk) {
29-
data += chunk;
30-
});
31-
res.on('end', function() {
32-
data = JSON.parse(data);
33-
resolve(data);
34-
});
35-
}).on('error', function() {
36-
reject('Failed to validate this access token with Instagram.');
37-
});
38-
});
25+
return httpsRequest.get("https://api.instagram.com/v1/" + path);
3926
}
4027

4128
module.exports = {

src/Adapters/Auth/janraincapture.js

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Helper functions for accessing the Janrain Capture API.
2-
var https = require('https');
32
var Parse = require('parse/node').Parse;
43
var querystring = require('querystring');
4+
const httpsRequest = require('./httpsRequest');
55

66
// Returns a promise that fulfills iff this user id is valid.
77
function validateAuthData(authData, options) {
@@ -30,22 +30,7 @@ function request(host, access_token) {
3030
'attribute_name': 'uuid' // we only need to pull the uuid for this access token to make sure it matches
3131
});
3232

33-
return new Promise(function(resolve, reject) {
34-
https.get({
35-
host: host,
36-
path: '/entity?' + query_string_data
37-
}, function(res) {
38-
var data = '';
39-
res.on('data', function(chunk) {
40-
data += chunk;
41-
});
42-
res.on('end', function () {
43-
resolve(JSON.parse(data));
44-
});
45-
}).on('error', function() {
46-
reject('Failed to validate this access token with Janrain capture.');
47-
});
48-
});
33+
return httpsRequest.get({ host: host, path: '/entity?' + query_string_data });
4934
}
5035

5136
module.exports = {

0 commit comments

Comments
 (0)