From e010fd82f2b4d32c1ded9f494b29508c9dcf36c1 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 4 Feb 2016 14:03:39 -0500 Subject: [PATCH 1/3] Generic OAuth provider support Refactors facebook login into oauth generic login Adds additional oauth2 providers adds ability to pass an oAuth validator in the config Adds Twitter validation support + OAuth 1 client Support auth_token instead of access_token for twitter Improves code coverage of OAuth Adds validation of oauth provider structures Better coverage of the OAuth spec 100% coverage of OAuth1.js Adds passing auth_token_secret for Twitter auth. Refactors auth validation methods to include authData parameter - Adds ability to extens oauth validator through configuration - Adds ability to extend oauth validator through external module (file or package) - Adds more tests - Adds tests to login with custom auth provider Adds more tests for REST API fixes twitter auth_token f --- bin/parse-server | 5 + spec/OAuth.spec.js | 307 ++++++++++++++++++++++++++++++++++++++ spec/ParseUser.spec.js | 57 +++---- spec/helper.js | 21 ++- spec/myoauth.js | 17 +++ src/Config.js | 1 + src/RestWrite.js | 97 +++++++++--- src/facebook.js | 11 +- src/index.js | 3 +- src/oauth/OAuth1Client.js | 226 ++++++++++++++++++++++++++++ src/oauth/facebook.js | 57 +++++++ src/oauth/github.js | 51 +++++++ src/oauth/google.js | 44 ++++++ src/oauth/index.js | 17 +++ src/oauth/instagram.js | 44 ++++++ src/oauth/linkedin.js | 51 +++++++ src/oauth/meetup.js | 50 +++++++ src/oauth/twitter.js | 30 ++++ src/transform.js | 59 +++++--- 19 files changed, 1061 insertions(+), 87 deletions(-) create mode 100644 spec/OAuth.spec.js create mode 100644 spec/myoauth.js create mode 100644 src/oauth/OAuth1Client.js create mode 100644 src/oauth/facebook.js create mode 100644 src/oauth/github.js create mode 100644 src/oauth/google.js create mode 100644 src/oauth/index.js create mode 100644 src/oauth/instagram.js create mode 100644 src/oauth/linkedin.js create mode 100644 src/oauth/meetup.js create mode 100644 src/oauth/twitter.js diff --git a/bin/parse-server b/bin/parse-server index 8d0104e470..0785b7f0de 100755 --- a/bin/parse-server +++ b/bin/parse-server @@ -30,6 +30,11 @@ if (process.env.PARSE_SERVER_OPTIONS) { facebookAppIds = facebookAppIds.split(","); options.facebookAppIds = facebookAppIds; } + + var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS; + if (oauth) { + options.oauth = JSON.parse(oauth); + }; } var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/"; diff --git a/spec/OAuth.spec.js b/spec/OAuth.spec.js new file mode 100644 index 0000000000..47e4349d93 --- /dev/null +++ b/spec/OAuth.spec.js @@ -0,0 +1,307 @@ +var OAuth = require("../src/oauth/OAuth1Client"); +var request = require('request'); + +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(data){ + jequal(false, true); + done(); + }).catch(function(){ + jequal(true, true); + done(); + }) + }); + + ["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){ + it("Should validate structure of "+providerName, (done) => { + var provider = require("../src/oauth/"+providerName); + jequal(typeof provider.validateAuthData, "function"); + jequal(typeof provider.validateAppId, "function"); + jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor); + jequal(provider.validateAppId("app", "key", {}).constructor, Promise.prototype.constructor); + done(); + }); + }); + + var getMockMyOauthProvider = function() { + return { + authData: { + id: "12345", + access_token: "12345", + expiration_date: new Date().toJSON(), + }, + shouldError: false, + loggedOut: false, + synchronizedUserId: null, + synchronizedAuthToken: null, + synchronizedExpiration: null, + + authenticate: function(options) { + if (this.shouldError) { + options.error(this, "An error occurred"); + } else if (this.shouldCancel) { + options.error(this, null); + } else { + options.success(this, this.authData); + } + }, + restoreAuthentication: function(authData) { + if (!authData) { + this.synchronizedUserId = null; + this.synchronizedAuthToken = null; + this.synchronizedExpiration = null; + return true; + } + this.synchronizedUserId = authData.id; + this.synchronizedAuthToken = authData.access_token; + this.synchronizedExpiration = authData.expiration_date; + return true; + }, + getAuthType: function() { + return "myoauth"; + }, + deauthenticate: function() { + this.loggedOut = true; + this.restoreAuthentication(null); + } + }; + }; + + var ExtendedUser = Parse.User.extend({ + extended: function() { + return true; + } + }); + + var createOAuthUser = function(callback) { + var jsonBody = { + authData: { + myoauth: getMockMyOauthProvider().authData + } + }; + var headers = {'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json' } + + var options = { + headers: {'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json' }, + url: 'http://localhost:8378/1/users', + body: JSON.stringify(jsonBody) + }; + + return request.post(options, callback); + } + + it("should create user with REST API", (done) => { + + createOAuthUser((error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.objectId).not.toBeNull(); + expect(b.objectId).not.toBeUndefined(); + done(); + }); + + }); + + it("should only create a single user with REST API", (done) => { + var objectId; + createOAuthUser((error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.objectId).not.toBeNull(); + expect(b.objectId).not.toBeUndefined(); + objectId = b.objectId; + + createOAuthUser((error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.objectId).not.toBeNull(); + expect(b.objectId).not.toBeUndefined(); + expect(b.objectId).toBe(objectId); + done(); + }); + }); + + }); + + it("unlink and link with custom provider", (done) => { + var provider = getMockMyOauthProvider(); + Parse.User._registerAuthenticationProvider(provider); + Parse.User._logInWith("myoauth", { + success: function(model) { + ok(model instanceof Parse.User, "Model should be a Parse.User"); + strictEqual(Parse.User.current(), model); + ok(model.extended(), "Should have used the subclass."); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); + ok(model._isLinked("myoauth"), "User should be linked to myoauth"); + + model._unlinkFrom("myoauth", { + success: function(model) { + ok(!model._isLinked("myoauth"), + "User should not be linked to myoauth"); + ok(!provider.synchronizedUserId, "User id should be cleared"); + ok(!provider.synchronizedAuthToken, "Auth token should be cleared"); + ok(!provider.synchronizedExpiration, + "Expiration should be cleared"); + + model._linkWith("myoauth", { + success: function(model) { + ok(provider.synchronizedUserId, "User id should have a value"); + ok(provider.synchronizedAuthToken, + "Auth token should have a value"); + ok(provider.synchronizedExpiration, + "Expiration should have a value"); + ok(model._isLinked("myoauth"), + "User should be linked to myoauth"); + done(); + }, + error: function(model, error) { + ok(false, "linking again should succeed"); + done(); + } + }); + }, + error: function(model, error) { + ok(false, "unlinking should succeed"); + done(); + } + }); + }, + error: function(model, error) { + ok(false, "linking should have worked"); + done(); + } + }); + }); + + +}) \ No newline at end of file diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 1d5049a883..33da62e824 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -831,9 +831,11 @@ describe('Parse.User testing', () => { // server-side. var getMockFacebookProvider = function() { return { - userId: "8675309", - authToken: "jenny", - expiration: new Date().toJSON(), + authData: { + id: "8675309", + access_token: "jenny", + expiration_date: new Date().toJSON(), + }, shouldError: false, loggedOut: false, synchronizedUserId: null, @@ -846,11 +848,7 @@ describe('Parse.User testing', () => { } else if (this.shouldCancel) { options.error(this, null); } else { - options.success(this, { - id: this.userId, - access_token: this.authToken, - expiration_date: this.expiration - }); + options.success(this, this.authData); } }, restoreAuthentication: function(authData) { @@ -889,13 +887,14 @@ describe('Parse.User testing', () => { ok(model instanceof Parse.User, "Model should be a Parse.User"); strictEqual(Parse.User.current(), model); ok(model.extended(), "Should have used subclass."); - strictEqual(provider.userId, provider.synchronizedUserId); - strictEqual(provider.authToken, provider.synchronizedAuthToken); - strictEqual(provider.expiration, provider.synchronizedExpiration); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); ok(model._isLinked("facebook"), "User should be linked to facebook"); done(); }, error: function(model, error) { + console.error(model, error); ok(false, "linking should have worked"); done(); } @@ -910,9 +909,9 @@ describe('Parse.User testing', () => { ok(model instanceof Parse.User, "Model should be a Parse.User"); strictEqual(Parse.User.current(), model); ok(model.extended(), "Should have used the subclass."); - strictEqual(provider.userId, provider.synchronizedUserId); - strictEqual(provider.authToken, provider.synchronizedAuthToken); - strictEqual(provider.expiration, provider.synchronizedExpiration); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); ok(model._isLinked("facebook"), "User should be linked to facebook"); Parse.User.logOut(); @@ -925,20 +924,22 @@ describe('Parse.User testing', () => { "Model should be a Parse.User"); ok(innerModel === Parse.User.current(), "Returned model should be the current user"); - ok(provider.userId === provider.synchronizedUserId); - ok(provider.authToken === provider.synchronizedAuthToken); + ok(provider.authData.id === provider.synchronizedUserId); + ok(provider.authData.access_token === provider.synchronizedAuthToken); ok(innerModel._isLinked("facebook"), "User should be linked to facebook"); ok(innerModel.existed(), "User should not be newly-created"); done(); }, error: function(model, error) { + fail(error); ok(false, "LogIn should have worked"); done(); } }); }, error: function(model, error) { + console.error(model, error); ok(false, "LogIn should have worked"); done(); } @@ -987,9 +988,9 @@ describe('Parse.User testing', () => { success: function(model) { ok(model instanceof Parse.User, "Model should be a Parse.User"); strictEqual(Parse.User.current(), model); - strictEqual(provider.userId, provider.synchronizedUserId); - strictEqual(provider.authToken, provider.synchronizedAuthToken); - strictEqual(provider.expiration, provider.synchronizedExpiration); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); ok(model._isLinked("facebook"), "User should be linked"); done(); }, @@ -1020,9 +1021,9 @@ describe('Parse.User testing', () => { success: function(model) { ok(model instanceof Parse.User, "Model should be a Parse.User"); strictEqual(Parse.User.current(), model); - strictEqual(provider.userId, provider.synchronizedUserId); - strictEqual(provider.authToken, provider.synchronizedAuthToken); - strictEqual(provider.expiration, provider.synchronizedExpiration); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); ok(model._isLinked("facebook"), "User should be linked."); var user2 = new Parse.User(); user2.set("username", "testLinkWithProviderToAlreadyLinkedUser2"); @@ -1123,9 +1124,9 @@ describe('Parse.User testing', () => { ok(model instanceof Parse.User, "Model should be a Parse.User."); strictEqual(Parse.User.current(), model); ok(model.extended(), "Should have used the subclass."); - strictEqual(provider.userId, provider.synchronizedUserId); - strictEqual(provider.authToken, provider.synchronizedAuthToken); - strictEqual(provider.expiration, provider.synchronizedExpiration); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); ok(model._isLinked("facebook"), "User should be linked to facebook."); model._unlinkFrom("facebook", { @@ -1159,9 +1160,9 @@ describe('Parse.User testing', () => { ok(model instanceof Parse.User, "Model should be a Parse.User"); strictEqual(Parse.User.current(), model); ok(model.extended(), "Should have used the subclass."); - strictEqual(provider.userId, provider.synchronizedUserId); - strictEqual(provider.authToken, provider.synchronizedAuthToken); - strictEqual(provider.expiration, provider.synchronizedExpiration); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); + strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); ok(model._isLinked("facebook"), "User should be linked to facebook"); model._unlinkFrom("facebook", { diff --git a/spec/helper.js b/spec/helper.js index 3e6c6d9853..8b587f7d5d 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -5,7 +5,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; var cache = require('../src/cache'); var DatabaseAdapter = require('../src/DatabaseAdapter'); var express = require('express'); -var facebook = require('../src/facebook'); +var facebook = require('../src/oauth/facebook'); var ParseServer = require('../src/index').ParseServer; var databaseURI = process.env.DATABASE_URI; @@ -22,7 +22,13 @@ var api = new ParseServer({ restAPIKey: 'rest', masterKey: 'test', collectionPrefix: 'test_', - fileKey: 'test' + fileKey: 'test', + oauth: { // Override the facebook provider + facebook: mockFacebook(), + myoauth: { + module: "../spec/myoauth" // relative path as it's run from src + } + } }); var app = express(); @@ -40,7 +46,6 @@ Parse.Promise.disableAPlusCompliant(); beforeEach(function(done) { Parse.initialize('test', 'test', 'test'); - mockFacebook(); Parse.User.enableUnsafeCurrentUser(); done(); }); @@ -175,18 +180,20 @@ function range(n) { } function mockFacebook() { - facebook.validateUserId = function(userId, accessToken) { - if (userId === '8675309' && accessToken === 'jenny') { + var facebook = {}; + facebook.validateAuthData = function(authData) { + if (authData.id === '8675309' && authData.access_token === 'jenny') { return Promise.resolve(); } return Promise.reject(); }; - facebook.validateAppId = function(appId, accessToken) { - if (accessToken === 'jenny') { + facebook.validateAppId = function(appId, authData) { + if (authData.access_token === 'jenny') { return Promise.resolve(); } return Promise.reject(); }; + return facebook; } function clearData() { diff --git a/spec/myoauth.js b/spec/myoauth.js new file mode 100644 index 0000000000..d28f9e8130 --- /dev/null +++ b/spec/myoauth.js @@ -0,0 +1,17 @@ +// Custom oauth provider by module + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + if (authData.id == "12345" && authData.access_token == "12345") { + return Promise.resolve(); + } + return Promise.reject(); +} +function validateAppId() { + return Promise.resolve(); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; diff --git a/src/Config.js b/src/Config.js index cb047b679c..aeb25a6173 100644 --- a/src/Config.js +++ b/src/Config.js @@ -25,6 +25,7 @@ function Config(applicationId, mount) { this.database = DatabaseAdapter.getDatabaseConnection(applicationId); this.filesController = cacheInfo.filesController; + this.oauth = cacheInfo.oauth; this.mount = mount; } diff --git a/src/RestWrite.js b/src/RestWrite.js index 781bc655bb..54f5cfc996 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -9,7 +9,7 @@ var cache = require('./cache'); var Config = require('./Config'); var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); -var facebook = require('./facebook'); +var oauth = require("./oauth"); var Parse = require('parse/node'); var triggers = require('./triggers'); @@ -147,19 +147,26 @@ RestWrite.prototype.validateAuthData = function() { return; } - var facebookData = this.data.authData.facebook; + var authData = this.data.authData; var anonData = this.data.authData.anonymous; - + if (this.config.enableAnonymousUsers === true && (anonData === null || (anonData && anonData.id))) { return this.handleAnonymousAuthData(); - } else if (facebookData === null || - (facebookData && facebookData.id && facebookData.access_token)) { - return this.handleFacebookAuthData(); - } else { - throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, - 'This authentication method is unsupported.'); + } + + // Not anon, try other providers + var providers = Object.keys(authData); + if (!anonData && providers.length == 1) { + var provider = providers[0]; + var providerAuthData = authData[provider]; + var hasToken = (providerAuthData && providerAuthData.id); + if (providerAuthData === null || hasToken) { + return this.handleOAuthAuthData(provider); + } } + throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, + 'This authentication method is unsupported.'); }; RestWrite.prototype.handleAnonymousAuthData = function() { @@ -208,27 +215,71 @@ RestWrite.prototype.handleAnonymousAuthData = function() { }; -RestWrite.prototype.handleFacebookAuthData = function() { - var facebookData = this.data.authData.facebook; - if (facebookData === null && this.query) { - // We are unlinking from Facebook. - this.data._auth_data_facebook = null; +RestWrite.prototype.handleOAuthAuthData = function(provider) { + var authData = this.data.authData[provider]; + + if (authData === null && this.query) { + // We are unlinking from the provider. + this.data["_auth_data_" + provider ] = null; return; } - return facebook.validateUserId(facebookData.id, - facebookData.access_token) + var appIds; + var oauthOptions = this.config.oauth[provider]; + if (oauthOptions) { + appIds = oauthOptions.appIds; + } else if (provider == "facebook") { + appIds = this.config.facebookAppIds; + } + + var validateAuthData; + var validateAppId; + + + if (oauth[provider]) { + validateAuthData = oauth[provider].validateAuthData; + validateAppId = oauth[provider].validateAppId; + } + + // Try the configuration methods + if (oauthOptions) { + if (oauthOptions.module) { + validateAuthData = require(oauthOptions.module).validateAuthData; + validateAppId = require(oauthOptions.module).validateAppId; + }; + + if (oauthOptions.validateAuthData) { + validateAuthData = oauthOptions.validateAuthData; + } + if (oauthOptions.validateAppId) { + validateAppId = oauthOptions.validateAppId; + } + } + // try the custom provider first, fallback on the oauth implementation + + if (!validateAuthData || !validateAppId) { + return false; + }; + + return validateAuthData(authData, oauthOptions) .then(() => { - return facebook.validateAppId(this.config.facebookAppIds, - facebookData.access_token); + if (appIds && typeof validateAppId === "function") { + return validateAppId(appIds, authData, oauthOptions); + } + + // No validation required by the developer + return Promise.resolve(); + }).then(() => { // Check if this user already exists // TODO: does this handle re-linking correctly? + var query = {}; + query['authData.' + provider + '.id'] = authData.id; return this.config.database.find( this.className, - {'authData.facebook.id': facebookData.id}, {}); + query, {}); }).then((results) => { - this.storage['authProvider'] = "facebook"; + this.storage['authProvider'] = provider; if (results.length > 0) { if (!this.query) { // We're signing up, but this user already exists. Short-circuit @@ -247,7 +298,7 @@ RestWrite.prototype.handleFacebookAuthData = function() { delete this.data.authData; return; } - // We're trying to create a duplicate FB auth. Forbid it + // We're trying to create a duplicate oauth auth. Forbid it throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); } else { @@ -256,12 +307,12 @@ RestWrite.prototype.handleFacebookAuthData = function() { // This FB auth does not already exist, so transform it to a // saveable format - this.data._auth_data_facebook = facebookData; + this.data["_auth_data_" + provider ] = authData; // Delete the rest format key before saving delete this.data.authData; }); -}; +} // The non-third-party parts of User transformation RestWrite.prototype.transformUser = function() { diff --git a/src/facebook.js b/src/facebook.js index 5f9bbee85e..77e0e2134f 100644 --- a/src/facebook.js +++ b/src/facebook.js @@ -3,10 +3,10 @@ var https = require('https'); var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. -function validateUserId(userId, access_token) { - return graphRequest('me?fields=id&access_token=' + access_token) +function validateAuthData(authData) { + return graphRequest('me?fields=id&access_token=' + authData.access_token) .then((data) => { - if (data && data.id == userId) { + if (data && data.id == authData.id) { return; } throw new Parse.Error( @@ -16,7 +16,8 @@ function validateUserId(userId, access_token) { } // Returns a promise that fulfills iff this app id is valid. -function validateAppId(appIds, access_token) { +function validateAppId(appIds, authData) { + var access_token = authData.access_token; if (!appIds.length) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, @@ -53,5 +54,5 @@ function graphRequest(path) { module.exports = { validateAppId: validateAppId, - validateUserId: validateUserId + validateAuthData: validateAuthData }; diff --git a/src/index.js b/src/index.js index a5355f6caf..47b639f423 100644 --- a/src/index.js +++ b/src/index.js @@ -105,7 +105,8 @@ function ParseServer(args) { fileKey: args.fileKey || 'invalid-file-key', facebookAppIds: args.facebookAppIds || [], filesController: filesController, - enableAnonymousUsers: args.enableAnonymousUsers || true + enableAnonymousUsers: args.enableAnonymousUsers || true, + oauth: args.oauth || {}, }; // To maintain compatibility. TODO: Remove in v2.1 diff --git a/src/oauth/OAuth1Client.js b/src/oauth/OAuth1Client.js new file mode 100644 index 0000000000..2c70be0cd5 --- /dev/null +++ b/src/oauth/OAuth1Client.js @@ -0,0 +1,226 @@ +var https = require('https'), + crypto = require('crypto'); + +var OAuth = function(options) { + this.consumer_key = options.consumer_key; + this.consumer_secret = options.consumer_secret; + this.auth_token = options.auth_token; + this.auth_token_secret = options.auth_token_secret; + this.host = options.host; + this.oauth_params = options.oauth_params || {}; +}; + +OAuth.prototype.send = function(method, path, params, body){ + + var request = this.buildRequest(method, path, params, body); + // Encode the body properly, the current Parse Implementation don't do it properly + return new Promise(function(resolve, reject) { + var httpRequest = https.request(request, function(res) { + var data = ''; + res.on('data', function(chunk) { + data += chunk; + }); + res.on('end', function() { + data = JSON.parse(data); + resolve(data); + }); + }).on('error', function(e) { + reject('Failed to make an OAuth request'); + }); + if (request.body) { + httpRequest.write(request.body); + } + httpRequest.end(); + }); +}; + +OAuth.prototype.buildRequest = function(method, path, params, body) { + if (path.indexOf("/") != 0) { + path = "/"+path; + } + if (params && Object.keys(params).length > 0) { + path += "?" + OAuth.buildParameterString(params); + } + + var request = { + host: this.host, + path: path, + method: method.toUpperCase() + }; + + var oauth_params = this.oauth_params || {}; + oauth_params.oauth_consumer_key = this.consumer_key; + if(this.auth_token){ + oauth_params["oauth_token"] = this.auth_token; + } + + request = OAuth.signRequest(request, oauth_params, this.consumer_secret, this.auth_token_secret); + + if (body && Object.keys(body).length > 0) { + request.body = OAuth.buildParameterString(body); + } + return request; +} + +OAuth.prototype.get = function(path, params) { + return this.send("GET", path, params); +} + +OAuth.prototype.post = function(path, params, body) { + return this.send("POST", path, params, body); +} + +/* + Proper string %escape encoding +*/ +OAuth.encode = function(str) { + // discuss at: http://phpjs.org/functions/rawurlencode/ + // original by: Brett Zamir (http://brett-zamir.me) + // input by: travc + // input by: Brett Zamir (http://brett-zamir.me) + // input by: Michael Grier + // input by: Ratheous + // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // bugfixed by: Brett Zamir (http://brett-zamir.me) + // bugfixed by: Joris + // reimplemented by: Brett Zamir (http://brett-zamir.me) + // reimplemented by: Brett Zamir (http://brett-zamir.me) + // note: This reflects PHP 5.3/6.0+ behavior + // note: Please be aware that this function expects to encode into UTF-8 encoded strings, as found on + // note: pages served as UTF-8 + // example 1: rawurlencode('Kevin van Zonneveld!'); + // returns 1: 'Kevin%20van%20Zonneveld%21' + // example 2: rawurlencode('http://kevin.vanzonneveld.net/'); + // returns 2: 'http%3A%2F%2Fkevin.vanzonneveld.net%2F' + // example 3: rawurlencode('http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a'); + // returns 3: 'http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a' + + str = (str + '') + .toString(); + + // Tilde should be allowed unescaped in future versions of PHP (as reflected below), but if you want to reflect current + // PHP behavior, you would need to add ".replace(/~/g, '%7E');" to the following. + return encodeURIComponent(str) + .replace(/!/g, '%21') + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29') + .replace(/\*/g, '%2A'); +} + +OAuth.signatureMethod = "HMAC-SHA1"; +OAuth.version = "1.0"; + +/* + Generate a nonce +*/ +OAuth.nonce = function(){ + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for( var i=0; i < 30; i++ ) + text += possible.charAt(Math.floor(Math.random() * possible.length)); + + return text; +} + +OAuth.buildParameterString = function(obj){ + var result = {}; + + // Sort keys and encode values + if (obj) { + var keys = Object.keys(obj).sort(); + + // Map key=value, join them by & + return keys.map(function(key){ + return key + "=" + OAuth.encode(obj[key]); + }).join("&"); + } + + return ""; +} + +/* + Build the signature string from the object +*/ + +OAuth.buildSignatureString = function(method, url, parameters){ + return [method.toUpperCase(), OAuth.encode(url), OAuth.encode(parameters)].join("&"); +} + +/* + Retuns encoded HMAC-SHA1 from key and text +*/ +OAuth.signature = function(text, key){ + crypto = require("crypto"); + return OAuth.encode(crypto.createHmac('sha1', key).update(text).digest('base64')); +} + +OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_token_secret){ + oauth_parameters = oauth_parameters || {}; + + // Set default values + if (!oauth_parameters.oauth_nonce) { + oauth_parameters.oauth_nonce = OAuth.nonce(); + } + if (!oauth_parameters.oauth_timestamp) { + oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime()/1000); + } + if (!oauth_parameters.oauth_signature_method) { + oauth_parameters.oauth_signature_method = OAuth.signatureMethod; + } + if (!oauth_parameters.oauth_version) { + oauth_parameters.oauth_version = OAuth.version; + } + + if(!auth_token_secret){ + auth_token_secret=""; + } + // Force GET method if unset + if (!request.method) { + request.method = "GET" + } + + // Collect all the parameters in one signatureParameters object + var signatureParams = {}; + var parametersToMerge = [request.params, request.body, oauth_parameters]; + for(var i in parametersToMerge) { + var parameters = parametersToMerge[i]; + for(var k in parameters) { + signatureParams[k] = parameters[k]; + } + } + + // Create a string based on the parameters + var parameterString = OAuth.buildParameterString(signatureParams); + + // Build the signature string + var url = "https://"+request.host+""+request.path; + + var signatureString = OAuth.buildSignatureString(request.method, url, parameterString); + // Hash the signature string + var signatureKey = [OAuth.encode(consumer_secret), OAuth.encode(auth_token_secret)].join("&"); + + var signature = OAuth.signature(signatureString, signatureKey); + + // Set the signature in the params + oauth_parameters.oauth_signature = signature; + if(!request.headers){ + request.headers = {}; + } + + // Set the authorization header + var signature = Object.keys(oauth_parameters).sort().map(function(key){ + var value = oauth_parameters[key]; + return key+'="'+value+'"'; + }).join(", ") + + request.headers.Authorization = 'OAuth ' + signature; + + // Set the content type header + request.headers["Content-Type"] = "application/x-www-form-urlencoded"; + return request; + +} + +module.exports = OAuth; \ No newline at end of file diff --git a/src/oauth/facebook.js b/src/oauth/facebook.js new file mode 100644 index 0000000000..822df7117a --- /dev/null +++ b/src/oauth/facebook.js @@ -0,0 +1,57 @@ +// Helper functions for accessing the Facebook Graph API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return graphRequest('me?fields=id&access_token=' + authData.access_token) + .then((data) => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId(appIds, access_token) { + if (!appIds.length) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook auth is not configured.'); + } + return graphRequest('app?access_token=' + access_token) + .then((data) => { + if (data && appIds.indexOf(data.id) != -1) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook auth is invalid for this user.'); + }); +} + +// A promisey wrapper for FB graph requests. +function graphRequest(path) { + return new Promise(function(resolve, reject) { + https.get('https://graph.facebook.com/v2.5/' + 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(e) { + reject('Failed to validate this access token with Facebook.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; diff --git a/src/oauth/github.js b/src/oauth/github.js new file mode 100644 index 0000000000..ab6715b185 --- /dev/null +++ b/src/oauth/github.js @@ -0,0 +1,51 @@ +// Helper functions for accessing the github API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return request('user', authData.access_token) + .then((data) => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Github auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// 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() { + data = JSON.parse(data); + resolve(data); + }); + }).on('error', function(e) { + reject('Failed to validate this access token with Github.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; diff --git a/src/oauth/google.js b/src/oauth/google.js new file mode 100644 index 0000000000..c339eae904 --- /dev/null +++ b/src/oauth/google.js @@ -0,0 +1,44 @@ +// Helper functions for accessing the google API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return request("tokeninfo?access_token="+authData.access_token) + .then((response) => { + if (response && response.user_id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Google auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(path) { + return new Promise(function(resolve, reject) { + https.get("https://www.googleapis.com/oauth2/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(e) { + reject('Failed to validate this access token with Google.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; diff --git a/src/oauth/index.js b/src/oauth/index.js new file mode 100644 index 0000000000..f39aea07cf --- /dev/null +++ b/src/oauth/index.js @@ -0,0 +1,17 @@ +var facebook = require('./facebook'); +var instagram = require("./instagram"); +var linkedin = require("./linkedin"); +var meetup = require("./meetup"); +var google = require("./google"); +var github = require("./github"); +var twitter = require("./twitter"); + +module.exports = { + facebook: facebook, + github: github, + google: google, + instagram: instagram, + linkedin: linkedin, + meetup: meetup, + twitter: twitter +} \ No newline at end of file diff --git a/src/oauth/instagram.js b/src/oauth/instagram.js new file mode 100644 index 0000000000..03971695ff --- /dev/null +++ b/src/oauth/instagram.js @@ -0,0 +1,44 @@ +// Helper functions for accessing the instagram API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return request("users/self/?access_token="+authData.access_token) + .then((response) => { + if (response && response.data && response.data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Instagram auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// 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(e) { + reject('Failed to validate this access token with Instagram.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; diff --git a/src/oauth/linkedin.js b/src/oauth/linkedin.js new file mode 100644 index 0000000000..efcd13cd9f --- /dev/null +++ b/src/oauth/linkedin.js @@ -0,0 +1,51 @@ +// Helper functions for accessing the linkedin API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return request('people/~:(id)', authData.access_token) + .then((data) => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Meetup auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(path, access_token) { + return new Promise(function(resolve, reject) { + https.get({ + host: 'api.linkedin.com', + path: '/v1/' + path, + headers: { + 'Authorization': 'Bearer '+access_token, + 'x-li-format': 'json' + } + }, function(res) { + var data = ''; + res.on('data', function(chunk) { + data += chunk; + }); + res.on('end', function() { + data = JSON.parse(data); + resolve(data); + }); + }).on('error', function(e) { + reject('Failed to validate this access token with Linkedin.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; diff --git a/src/oauth/meetup.js b/src/oauth/meetup.js new file mode 100644 index 0000000000..04d16c5acd --- /dev/null +++ b/src/oauth/meetup.js @@ -0,0 +1,50 @@ +// Helper functions for accessing the meetup API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return request('member/self', authData.access_token) + .then((data) => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Meetup auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// 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() { + data = JSON.parse(data); + resolve(data); + }); + }).on('error', function(e) { + reject('Failed to validate this access token with Meetup.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; diff --git a/src/oauth/twitter.js b/src/oauth/twitter.js new file mode 100644 index 0000000000..b53ce333b1 --- /dev/null +++ b/src/oauth/twitter.js @@ -0,0 +1,30 @@ +// Helper functions for accessing the meetup API. +var OAuth = require('./OAuth1Client'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData, options) { + var client = new OAuth(options); + client.host = "api.twitter.com"; + client.auth_token = authData.auth_token; + client.auth_token_secret = authData.auth_token_secret; + + return client.get("/1.1/account/verify_credentials.json").then((data) => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Twitter auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; diff --git a/src/transform.js b/src/transform.js index 802bf0754a..83a8017d55 100644 --- a/src/transform.js +++ b/src/transform.js @@ -55,21 +55,21 @@ export function transformKeyValue(schema, className, restKey, restValue, options case '_wperm': return {key: key, value: restValue}; break; - case 'authData.anonymous.id': - if (options.query) { - return {key: '_auth_data_anonymous.id', value: restValue}; - } - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, - 'can only query on ' + key); - break; - case 'authData.facebook.id': - if (options.query) { - // Special-case auth data. - return {key: '_auth_data_facebook.id', value: restValue}; - } - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, - 'can only query on ' + key); - break; + // case 'authData.anonymous.id': + // if (options.query) { + // return {key: '_auth_data_anonymous.id', value: restValue}; + // } + // throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, + // 'can only query on ' + key); + // break; + // case 'authData.facebook.id': + // if (options.query) { + // // Special-case auth data. + // return {key: '_auth_data_facebook.id', value: restValue}; + // } + // throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, + // 'can only query on ' + key); + // break; case '$or': if (!options.query) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, @@ -97,6 +97,18 @@ export function transformKeyValue(schema, className, restKey, restValue, options }); return {key: '$and', value: mongoSubqueries}; default: + // Other auth data + var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); + if (authDataMatch) { + if (options.query) { + var provider = authDataMatch[1]; + // Special-case auth data. + return {key: '_auth_data_'+provider+'.id', value: restValue}; + } + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, + 'can only query on ' + key); + break; + }; if (options.validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key); @@ -646,15 +658,16 @@ function untransformObject(schema, className, mongoObject) { case '_expiresAt': restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key])).iso; break; - case '_auth_data_anonymous': - restObject['authData'] = restObject['authData'] || {}; - restObject['authData']['anonymous'] = mongoObject[key]; - break; - case '_auth_data_facebook': - restObject['authData'] = restObject['authData'] || {}; - restObject['authData']['facebook'] = mongoObject[key]; - break; default: + // Check other auth data keys + var authDataMatch = key.match(/^_auth_data_([a-zA-Z0-9_]+)$/); + if (authDataMatch) { + var provider = authDataMatch[1]; + restObject['authData'] = restObject['authData'] || {}; + restObject['authData'][provider] = mongoObject[key]; + break; + } + if (key.indexOf('_p_') == 0) { var newKey = key.substring(3); var expected; From 45bf8ffadb60929bee64d5d58372e2b226238246 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Tue, 16 Feb 2016 08:00:00 -0500 Subject: [PATCH 2/3] removes commented out code --- src/transform.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/transform.js b/src/transform.js index 83a8017d55..0e99b48836 100644 --- a/src/transform.js +++ b/src/transform.js @@ -55,21 +55,6 @@ export function transformKeyValue(schema, className, restKey, restValue, options case '_wperm': return {key: key, value: restValue}; break; - // case 'authData.anonymous.id': - // if (options.query) { - // return {key: '_auth_data_anonymous.id', value: restValue}; - // } - // throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, - // 'can only query on ' + key); - // break; - // case 'authData.facebook.id': - // if (options.query) { - // // Special-case auth data. - // return {key: '_auth_data_facebook.id', value: restValue}; - // } - // throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, - // 'can only query on ' + key); - // break; case '$or': if (!options.query) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, From 077b977efad63f741f9ebc1b442753b5f049d9b1 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Tue, 16 Feb 2016 08:28:14 -0500 Subject: [PATCH 3/3] Updates README --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/README.md b/README.md index 10b44d5413..0a7365dd2e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,61 @@ The client keys used with Parse are no longer necessary with parse-server. If y * restAPIKey * dotNetKey +#### OAuth Support + +parse-server supports 3rd party authentication with + +* Twitter +* Meetup +* Linkedin +* Google +* Instagram +* Facebook + + +Configuration options for these 3rd-party modules is done with the oauth option passed to ParseServer: + +``` +{ + oauth: { + twitter: { + consumer_key: "", // REQUIRED + consumer_secret: "" // REQUIRED + }, + facebook: { + appIds: "FACEBOOK APP ID" + } + } + +} +``` + +#### Custom Authentication + +It is possible to leverage the OAuth support with any 3rd party authentication that you bring in. + +``` +{ + + oauth: { + my_custom_auth: { + module: "PATH_TO_MODULE" // OR object, + option1: "", + option2: "", + } + } +} +``` + +On this module, you need to implement and export those two functions `validateAuthData(authData, options) {} ` and `validateAppId(appIds, authData) {}`. + +For more informations about custom auth please see the examples: + +- [facebook OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/facebook.js) +- [twitter OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/twitter.js) +- [instagram OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/instagram.js) + + #### Advanced options: * filesAdapter - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js))