Auth Adapters refactoring (#3177)

* Moves all authentication providers to Adapter/Auth

* refactors specs

* Deprecates oauth option in favor of auth option

- Deprecates facebookAppIds option (in favor of auth.facebook.appIds)
- Adds warnings about the deprecated options

* nits
This commit is contained in:
Florent Vilmart
2016-12-06 17:09:43 -05:00
committed by Arthur Cinader
parent a9067260fc
commit c1dcaf1271
28 changed files with 407 additions and 267 deletions

View File

@@ -1,145 +1,13 @@
var OAuth = require("../src/authDataManager/OAuth1Client");
var request = require('request');
var Config = require("../src/Config");
var defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
var authenticationLoader = require('../src/Adapters/Auth');
var path = require('path');
describe('OAuth', function() {
it("Nonce should have right length", (done) => {
jequal(OAuth.nonce().length, 30);
done();
});
it("Should properly build parameter string", (done) => {
var string = OAuth.buildParameterString({c:1, a:2, b:3})
jequal(string, "a=2&b=3&c=1");
done();
});
it("Should properly build empty parameter string", (done) => {
var string = OAuth.buildParameterString()
jequal(string, "");
done();
});
it("Should properly build signature string", (done) => {
var string = OAuth.buildSignatureString("get", "http://dummy.com", "");
jequal(string, "GET&http%3A%2F%2Fdummy.com&");
done();
});
it("Should properly generate request signature", (done) => {
var request = {
host: "dummy.com",
path: "path"
};
var oauth_params = {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA",
oauth_consumer_key: "hello",
oauth_token: "token"
};
var consumer_secret = "world";
var auth_token_secret = "secret";
request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret);
jequal(request.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"');
done();
});
it("Should properly build request", (done) => {
var options = {
host: "dummy.com",
consumer_key: "hello",
consumer_secret: "world",
auth_token: "token",
auth_token_secret: "secret",
// Custom oauth params for tests
oauth_params: {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA"
}
};
var path = "path";
var method = "get";
var oauthClient = new OAuth(options);
var req = oauthClient.buildRequest(method, path, {"query": "param"});
jequal(req.host, options.host);
jequal(req.path, "/"+path+"?query=param");
jequal(req.method, "GET");
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
done();
});
function validateCannotAuthenticateError(data, done) {
jequal(typeof data, "object");
jequal(typeof data.errors, "object");
var errors = data.errors;
jequal(typeof errors[0], "object");
// Cannot authenticate error
jequal(errors[0].code, 32);
done();
}
it("Should fail a GET request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var path = "/1.1/help/configuration.json";
var params = {"lang": "en"};
var oauthClient = new OAuth(options);
oauthClient.get(path, params).then(function(data){
validateCannotAuthenticateError(data, done);
})
});
it("Should fail a POST request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/1.1/account/settings.json";
var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(data){
validateCannotAuthenticateError(data, done);
})
});
it("Should fail a request", (done) => {
var options = {
host: "localhost",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/";
var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(){
jequal(false, true);
done();
}).catch(function(){
jequal(true, true);
done();
})
});
describe('AuthenticationProviers', function() {
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].map(function(providerName){
it("Should validate structure of "+providerName, (done) => {
var provider = require("../src/authDataManager/"+providerName);
var provider = require("../src/Adapters/Auth/"+providerName);
jequal(typeof provider.validateAuthData, "function");
jequal(typeof provider.validateAppId, "function");
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
@@ -325,5 +193,90 @@ describe('OAuth', function() {
});
});
function validateValidator(validator) {
expect(typeof validator).toBe('function');
}
})
function validateAuthenticationHandler(authenticatonHandler) {
expect(authenticatonHandler).not.toBeUndefined();
expect(typeof authenticatonHandler.getValidatorForProvider).toBe('function');
expect(typeof authenticatonHandler.getValidatorForProvider).toBe('function');
}
it('properly loads custom adapter', (done) => {
var validAuthData = {
id: 'hello',
token: 'world'
}
let adapter = {
validateAppId: function() {
return Promise.resolve();
},
validateAuthData: function(authData) {
if (authData.id == validAuthData.id && authData.token == validAuthData.token) {
return Promise.resolve();
}
return Promise.reject();
}
};
let authDataSpy = spyOn(adapter, 'validateAuthData').and.callThrough();
let appIdSpy = spyOn(adapter, 'validateAppId').and.callThrough();
let authenticationHandler = authenticationLoader({
customAuthentication: adapter
});
validateAuthenticationHandler(authenticationHandler);
let validator = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);
validator(validAuthData).then(() => {
expect(authDataSpy).toHaveBeenCalled();
// AppIds are not provided in the adapter, should not be called
expect(appIdSpy).not.toHaveBeenCalled();
done();
}, (err) => {
jfail(err);
done();
})
});
it('properly loads custom adapter module object', (done) => {
let authenticationHandler = authenticationLoader({
customAuthentication: path.resolve('./spec/support/CustomAuth.js')
});
validateAuthenticationHandler(authenticationHandler);
let validator = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);
validator({
token: 'my-token'
}).then(() => {
done();
}, (err) => {
jfail(err);
done();
})
});
it('properly loads custom adapter module object', (done) => {
let authenticationHandler = authenticationLoader({
customAuthentication: { module: path.resolve('./spec/support/CustomAuthFunction.js'), options: { token: 'valid-token' }}
});
validateAuthenticationHandler(authenticationHandler);
let validator = authenticationHandler.getValidatorForProvider('customAuthentication');
validateValidator(validator);
validator({
token: 'valid-token'
}).then(() => {
done();
}, (err) => {
jfail(err);
done();
})
});
});

136
spec/OAuth1.spec.js Normal file
View File

@@ -0,0 +1,136 @@
var OAuth = require("../src/Adapters/Auth/OAuth1Client");
describe('OAuth', function() {
it("Nonce should have right length", (done) => {
jequal(OAuth.nonce().length, 30);
done();
});
it("Should properly build parameter string", (done) => {
var string = OAuth.buildParameterString({c:1, a:2, b:3})
jequal(string, "a=2&b=3&c=1");
done();
});
it("Should properly build empty parameter string", (done) => {
var string = OAuth.buildParameterString()
jequal(string, "");
done();
});
it("Should properly build signature string", (done) => {
var string = OAuth.buildSignatureString("get", "http://dummy.com", "");
jequal(string, "GET&http%3A%2F%2Fdummy.com&");
done();
});
it("Should properly generate request signature", (done) => {
var request = {
host: "dummy.com",
path: "path"
};
var oauth_params = {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA",
oauth_consumer_key: "hello",
oauth_token: "token"
};
var consumer_secret = "world";
var auth_token_secret = "secret";
request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret);
jequal(request.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"');
done();
});
it("Should properly build request", (done) => {
var options = {
host: "dummy.com",
consumer_key: "hello",
consumer_secret: "world",
auth_token: "token",
auth_token_secret: "secret",
// Custom oauth params for tests
oauth_params: {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA"
}
};
var path = "path";
var method = "get";
var oauthClient = new OAuth(options);
var req = oauthClient.buildRequest(method, path, {"query": "param"});
jequal(req.host, options.host);
jequal(req.path, "/"+path+"?query=param");
jequal(req.method, "GET");
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
done();
});
function validateCannotAuthenticateError(data, done) {
jequal(typeof data, "object");
jequal(typeof data.errors, "object");
var errors = data.errors;
jequal(typeof errors[0], "object");
// Cannot authenticate error
jequal(errors[0].code, 32);
done();
}
it("Should fail a GET request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var path = "/1.1/help/configuration.json";
var params = {"lang": "en"};
var oauthClient = new OAuth(options);
oauthClient.get(path, params).then(function(data){
validateCannotAuthenticateError(data, done);
})
});
it("Should fail a POST request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/1.1/account/settings.json";
var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(data){
validateCannotAuthenticateError(data, done);
})
});
it("Should fail a request", (done) => {
var options = {
host: "localhost",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/";
var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(){
jequal(false, true);
done();
}).catch(function(){
jequal(true, true);
done();
})
});
});

View File

@@ -1,4 +1,4 @@
let twitter = require('../src/authDataManager/twitter');
let twitter = require('../src/Adapters/Auth/twitter');
describe('Twitter Auth', () => {
it('should use the proper configuration', () => {

View File

@@ -102,7 +102,7 @@ var defaultConfiguration = {
bundleId: 'bundleId',
}
},
oauth: { // Override the facebook provider
auth: { // Override the facebook provider
facebook: mockFacebook(),
myoauth: {
module: path.resolve(__dirname, "myoauth") // relative path as it's run from src

View File

@@ -0,0 +1,12 @@
module.exports = {
validateAppId: function() {
return Promise.resolve();
},
validateAuthData: function(authData) {
if (authData.token == 'my-token') {
return Promise.resolve();
}
return Promise.reject();
}
}

View File

@@ -0,0 +1,14 @@
module.exports = function(validAuthData) {
return {
validateAppId: function() {
return Promise.resolve();
},
validateAuthData: function(authData) {
if (authData.token == validAuthData.token) {
return Promise.resolve();
}
return Promise.reject();
}
}
}

View File

@@ -17,6 +17,7 @@ export function loadAdapter(adapter, defaultAdapter, options) {
}
}
} else if (typeof adapter === "string") {
/* eslint-disable */
adapter = require(adapter);
// If it's define as a module, get the default
if (adapter.default) {

View File

@@ -0,0 +1,22 @@
/*eslint no-unused-vars: "off"*/
export class AuthAdapter {
/*
@param appIds: the specified app ids in the configuration
@param authData: the client provided authData
@returns a promise that resolves if the applicationId is valid
*/
validateAppId(appIds, authData) {
return Promise.resolve({});
}
/*
@param authData: the client provided authData
@param options: additional options
*/
validateAuthData(authData, options) {
return Promise.resolve({});
}
}
export default AuthAdapter;

99
src/Adapters/Auth/index.js Executable file
View File

@@ -0,0 +1,99 @@
import loadAdapter from '../AdapterLoader';
const facebook = require('./facebook');
const instagram = require("./instagram");
const linkedin = require("./linkedin");
const meetup = require("./meetup");
const google = require("./google");
const github = require("./github");
const twitter = require("./twitter");
const spotify = require("./spotify");
const digits = require("./twitter"); // digits tokens are validated by twitter
const janrainengage = require("./janrainengage");
const janraincapture = require("./janraincapture");
const vkontakte = require("./vkontakte");
const qq = require("./qq");
const wechat = require("./wechat");
const weibo = require("./weibo");
const anonymous = {
validateAuthData: () => {
return Promise.resolve();
},
validateAppId: () => {
return Promise.resolve();
}
}
const providers = {
facebook,
instagram,
linkedin,
meetup,
google,
github,
twitter,
spotify,
anonymous,
digits,
janrainengage,
janraincapture,
vkontakte,
qq,
wechat,
weibo
}
function authDataValidator(adapter, appIds, options) {
return function(authData) {
return adapter.validateAuthData(authData, options).then(() => {
if (appIds) {
return adapter.validateAppId(appIds, authData, options);
}
return Promise.resolve();
});
}
}
module.exports = function(authOptions = {}, enableAnonymousUsers = true) {
let _enableAnonymousUsers = enableAnonymousUsers;
let setEnableAnonymousUsers = function(enable) {
_enableAnonymousUsers = enable;
}
// To handle the test cases on configuration
let getValidatorForProvider = function(provider) {
if (provider === 'anonymous' && !_enableAnonymousUsers) {
return;
}
const defaultAdapter = providers[provider];
let adapter = defaultAdapter;
const providerOptions = authOptions[provider];
if (!defaultAdapter && !providerOptions) {
return;
}
const appIds = providerOptions ? providerOptions.appIds : undefined;
// Try the configuration methods
if (providerOptions) {
const optionalAdapter = loadAdapter(providerOptions, undefined, providerOptions);
if (optionalAdapter) {
adapter = optionalAdapter;
}
}
if (!adapter.validateAuthData || !adapter.validateAppId) {
return;
}
return authDataValidator(adapter, appIds, providerOptions);
}
return Object.freeze({
getValidatorForProvider,
setEnableAnonymousUsers,
})
}

View File

@@ -1,7 +1,7 @@
// Helper functions for accessing the twitter API.
var OAuth = require('./OAuth1Client');
var Parse = require('parse/node').Parse;
var logger = require('../logger').default;
var logger = require('../../logger').default;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData, options) {

View File

@@ -4,7 +4,7 @@
var https = require('https');
var Parse = require('parse/node').Parse;
var logger = require('../logger').default;
var logger = require('../../logger').default;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData, params) {

View File

@@ -32,7 +32,6 @@ export class Config {
this.restAPIKey = cacheInfo.restAPIKey;
this.webhookKey = cacheInfo.webhookKey;
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
this.userSensitiveFields = cacheInfo.userSensitiveFields;

View File

@@ -7,7 +7,7 @@ var batch = require('./batch'),
Parse = require('parse/node').Parse,
path = require('path'),
url = require('url'),
authDataManager = require('./authDataManager');
authDataManager = require('./Adapters/Auth');
import defaults from './defaults';
import * as logging from './logger';
@@ -73,8 +73,6 @@ addParseCloud();
// to register your cloud code hooks and functions.
// "appId": the application id to host
// "masterKey": the master key for requests to this app
// "facebookAppIds": an array of valid Facebook Application IDs, required
// if using Facebook login
// "collectionPrefix": optional prefix for database collection names
// "fileKey": optional key from Parse dashboard for supporting older files
// hosted by Parse
@@ -112,11 +110,11 @@ class ParseServer {
restAPIKey,
webhookKey,
fileKey,
facebookAppIds = [],
userSensitiveFields = [],
enableAnonymousUsers = defaults.enableAnonymousUsers,
allowClientClassCreation = defaults.allowClientClassCreation,
oauth = {},
auth = {},
serverURL = requiredParameter('You must provide a serverURL!'),
maxUploadSize = defaults.maxUploadSize,
verifyUserEmails = defaults.verifyUserEmails,
@@ -191,6 +189,17 @@ class ParseServer {
const dbInitPromise = databaseController.performInitialization();
if (Object.keys(oauth).length > 0) {
/* eslint-disable no-console */
console.warn('oauth option is deprecated and will be removed in a future release, please use auth option instead');
if (Object.keys(auth).length > 0) {
console.warn('You should use only the auth option.');
}
/* eslint-enable */
}
auth = Object.assign({}, oauth, auth);
AppCache.put(appId, {
appId,
masterKey: masterKey,
@@ -202,7 +211,6 @@ class ParseServer {
restAPIKey: restAPIKey,
webhookKey: webhookKey,
fileKey: fileKey,
facebookAppIds: facebookAppIds,
analyticsController: analyticsController,
cacheController: cacheController,
filesController: filesController,
@@ -216,7 +224,7 @@ class ParseServer {
accountLockout: accountLockout,
passwordPolicy: passwordPolicy,
allowClientClassCreation: allowClientClassCreation,
authDataManager: authDataManager(oauth, enableAnonymousUsers),
authDataManager: authDataManager(auth, enableAnonymousUsers),
appName: appName,
publicServerURL: publicServerURL,
customPages: customPages,
@@ -232,11 +240,6 @@ class ParseServer {
userSensitiveFields
});
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatibility
if (process.env.FACEBOOK_APP_ID) {
AppCache.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
}
Config.validate(AppCache.get(appId));
this.config = AppCache.get(appId);
Config.setupPasswordValidator(this.config.passwordPolicy);

View File

@@ -1,110 +0,0 @@
let facebook = require('./facebook');
let instagram = require("./instagram");
let linkedin = require("./linkedin");
let meetup = require("./meetup");
let google = require("./google");
let github = require("./github");
let twitter = require("./twitter");
let spotify = require("./spotify");
let digits = require("./twitter"); // digits tokens are validated by twitter
let janrainengage = require("./janrainengage");
let janraincapture = require("./janraincapture");
let vkontakte = require("./vkontakte");
let qq = require("./qq");
let wechat = require("./wechat");
let weibo = require("./weibo");
let anonymous = {
validateAuthData: () => {
return Promise.resolve();
},
validateAppId: () => {
return Promise.resolve();
}
}
let providers = {
facebook,
instagram,
linkedin,
meetup,
google,
github,
twitter,
spotify,
anonymous,
digits,
janrainengage,
janraincapture,
vkontakte,
qq,
wechat,
weibo
}
module.exports = function(oauthOptions = {}, enableAnonymousUsers = true) {
let _enableAnonymousUsers = enableAnonymousUsers;
let setEnableAnonymousUsers = function(enable) {
_enableAnonymousUsers = enable;
}
// To handle the test cases on configuration
let getValidatorForProvider = function(provider) {
if (provider === 'anonymous' && !_enableAnonymousUsers) {
return;
}
let defaultProvider = providers[provider];
let optionalProvider = oauthOptions[provider];
if (!defaultProvider && !optionalProvider) {
return;
}
let appIds;
if (optionalProvider) {
appIds = optionalProvider.appIds;
}
var validateAuthData;
var validateAppId;
if (defaultProvider) {
validateAuthData = defaultProvider.validateAuthData;
validateAppId = defaultProvider.validateAppId;
}
// Try the configuration methods
if (optionalProvider) {
if (optionalProvider.module) {
validateAuthData = require(optionalProvider.module).validateAuthData;
validateAppId = require(optionalProvider.module).validateAppId;
}
if (optionalProvider.validateAuthData) {
validateAuthData = optionalProvider.validateAuthData;
}
if (optionalProvider.validateAppId) {
validateAppId = optionalProvider.validateAppId;
}
}
if (!validateAuthData || !validateAppId) {
return;
}
return function(authData) {
return validateAuthData(authData, optionalProvider).then(() => {
if (appIds) {
return validateAppId(appIds, authData, optionalProvider);
}
return Promise.resolve();
})
}
}
return Object.freeze({
getValidatorForProvider,
setEnableAnonymousUsers,
})
}

View File

@@ -83,7 +83,12 @@ export default {
},
"oauth": {
env: "PARSE_SERVER_OAUTH_PROVIDERS",
help: "Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth",
help: "[DEPRECATED (use auth option)] Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth",
action: objectParser
},
"auth": {
env: "PARSE_SERVER_AUTH_PROVIDERS",
help: "Configuration for your authentication providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth",
action: objectParser
},
"fileKey": {
@@ -92,9 +97,15 @@ export default {
},
"facebookAppIds": {
env: "PARSE_SERVER_FACEBOOK_APP_IDS",
help: "Comma separated list for your facebook app Ids",
type: "list",
action: arrayParser
help: "[DEPRECATED (use auth option)]",
action: function() {
throw 'facebookAppIds is deprecated, please use { auth: \
{facebook: \
{ appIds: [] } \
}\
}\
}';
}
},
"enableAnonymousUsers": {
env: "PARSE_SERVER_ENABLE_ANON_USERS",