From ee475bfa4fab83a8adfa4bb7190b913cfe2de35f Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Wed, 5 Jun 2013 16:24:15 -0700 Subject: [PATCH 01/24] Fix to README docs - missing code section closing characters --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c7eca911..9d0f2f58 100644 --- a/README.md +++ b/README.md @@ -2425,6 +2425,7 @@ object whose parameter name keys map to description values: ```javascript everyauth.stripe.configurable(); +``` ### Salesforce @@ -2488,6 +2489,7 @@ object whose parameter name keys map to description values: ```javascript everyauth.salesforce.configurable(); +``` ## Configuring a Module From 6d7b6add4bf1d2c477290a2997335738e24e79d2 Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Tue, 25 Jun 2013 01:31:11 -0700 Subject: [PATCH 02/24] Implement SurveyMonkey authorization --- lib/modules/surveymonkey.js | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 lib/modules/surveymonkey.js diff --git a/lib/modules/surveymonkey.js b/lib/modules/surveymonkey.js new file mode 100644 index 00000000..b57e6641 --- /dev/null +++ b/lib/modules/surveymonkey.js @@ -0,0 +1,79 @@ +var oauthModule = require('./oauth2'), + url = require('url'), + request = require('request'); + +var surveymonkey = module.exports = +oauthModule.submodule('surveymonkey') + + .configurable({ + client_id: "Set this to your client_id. Defaults to none" + }) + + .oauthHost('https://api.surveymonkey.net') + .apiHost('https://api.surveymonkey.net') + + //Set up Auth Path - requires client_id + .authPath('/oauth/authorize') + .authQueryParam('response_type', 'code') + .authQueryParam('client_id', function () { + return this._client_id && this.client_id(); + }) + + //Set up access Token path - requires client_id and client_secret (which is really your api key) + .accessTokenPath('/oauth/token') + .accessTokenParam('grant_type', 'authorization_code') + .accessTokenParam('client_id', function() { + return this._client_id && this.client_id(); + }) + .accessTokenParam('client_secret', function() { + return this._appId && this.appId(); + }) + .accessTokenHttpMethod('post') + .postAccessTokenParamsVia('data') + + .entryPath('/auth/surveymonkey') + .callbackPath('/auth/surveymonkey/callback') + + .authCallbackDidErr( function (req) { + var parsedUrl = url.parse(req.url, true); + return parsedUrl.query && !!parsedUrl.query.error; + }) + + .handleAuthCallbackError( function (req, res) { + var parsedUrl = url.parse(req.url, true), + errorDesc = parsedUrl.query.error + "; " + parsedUrl.query.error_description; + if (res.render) { + res.render(__dirname + '/../views/auth-fail.jade', { + errorDescription: errorDesc + }); + } else { + // TODO Replace this with a nice fallback + throw new Error("You must configure handleAuthCallbackError if you are not using express"); + } + }) + + //With SurveyMonkey - there isn't an api call to fetch info on the user - all we get is the access token + .fetchOAuthUser( function (accessToken, authResponse) { + return authResponse.code; + }) + + .moduleErrback( function (err, seqValues) { + if (err instanceof Error) { + var next = seqValues.next; + return next(err); + } else if (err.extra) { + var surveymonkeyResponse = err.extra.res, + serverResponse = seqValues.res; + serverResponse.writeHead( + surveymonkeyResponse.statusCode, + surveymonkeyResponse.headers); + serverResponse.end(err.extra.data); + } else if (err.statusCode) { + var serverResponse = seqValues.res; + serverResponse.writeHead(err.statusCode); + serverResponse.end(err.data); + } else { + console.error(err); + throw new Error('Unsupported error type'); + } + }); From 4665e64cb78e3715b7b4208ff3dae7368c916793 Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Tue, 25 Jun 2013 01:31:56 -0700 Subject: [PATCH 03/24] Fixes endless callback loop by passing back res in non 200 error failure cases. --- lib/modules/oauth2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index 9a4592a9..c15a76a7 100644 --- a/lib/modules/oauth2.js +++ b/lib/modules/oauth2.js @@ -186,7 +186,7 @@ everyModule.submodule('oauth2') err.extra = {data: body, res: res}; return p.fail(err); } - if (parseInt(res.statusCode / 100) != 2) return p.fail({extra: {res: res, data: body}}); + if (parseInt(res.statusCode / 100) != 2) return p.fail({statusCode: res.statusCode, data: body}); var resType = res.headers['content-type'] , data; if (resType.substring(0, 10) === 'text/plain') { From 88d05a08cf45a0907ece9d10ff7517fba67490e6 Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Fri, 9 Aug 2013 16:27:00 -0700 Subject: [PATCH 04/24] Add ability in survey monkey to get details about the user now that this is part of their api. --- lib/modules/surveymonkey.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/modules/surveymonkey.js b/lib/modules/surveymonkey.js index b57e6641..b2b16e56 100644 --- a/lib/modules/surveymonkey.js +++ b/lib/modules/surveymonkey.js @@ -54,7 +54,33 @@ oauthModule.submodule('surveymonkey') //With SurveyMonkey - there isn't an api call to fetch info on the user - all we get is the access token .fetchOAuthUser( function (accessToken, authResponse) { - return authResponse.code; + var p = this.Promise(); + + request.post({ + url: this.apiHost() + '/v2/user/get_user_details?api_key=' + this.appId(), + headers: {'Authorization': 'bearer ' + accessToken} + }, function(err, res, body) { + if(err) { + return p.fail(err); + } else { + + //Suverymonkey sends back errors in the status code - not as non 200 responses + if(body) { + body = JSON.parse(body); + } else { + body.status = 1; + } + + if(parseInt(res.statusCode/100, 10) !== 2 || body.status !== 0) { + return p.fail({extra:{data:body, res: res}}); + } + var oAuthUser = body; + oAuthUser.code = authResponse.code; + p.fulfill(oAuthUser); + } + }); + //return authResponse.code; + return p; }) .moduleErrback( function (err, seqValues) { From 44b3ca8b1d3ad76773a578e8740795bd6e5a837b Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Mon, 12 Aug 2013 18:16:23 -0700 Subject: [PATCH 05/24] Add first implmentation of desk.com module --- lib/modules/desk.js | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/modules/desk.js diff --git a/lib/modules/desk.js b/lib/modules/desk.js new file mode 100644 index 00000000..e3129a3a --- /dev/null +++ b/lib/modules/desk.js @@ -0,0 +1,62 @@ +var oauthModule = require('./oauth'), + url = require('url'); + +var desk = module.exports = +oauthModule.submodule('desk') + .apiHost('https://datahero.desk.com/api/v2') + .oauthHost('https://datahero.desk.com') + .requestTokenPath('/oauth/request_token') + .accessTokenPath('/oauth/access_token') + .authorizePath('/oauth/authorize') + .entryPath('/auth/desk') + .callbackPath('/auth/desk/callback') + + .authCallbackDidErr( function (req) { + var parsedUrl = url.parse(req.url, true); + return parsedUrl.query && !!parsedUrl.query.not_approved; + }) + + .handleAuthCallbackError( function (req, res) { + var parsedUrl = url.parse(req.url, true), + errorDesc = parsedUrl.query.error + "; " + parsedUrl.query.error_description; + if (res.render) { + res.render(__dirname + '/../views/auth-fail.jade', { + errorDescription: errorDesc + }); + } else { + // TODO Replace this with a nice fallback + throw new Error("You must configure handleAuthCallbackError if you are not using express"); + } + }) + + .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { + var p = this.Promise(); + this.oauth.get(this.apiHost() + '/account', accessToken, accessTokenSecret, function (err, data) { + if (err) return p.fail(err); + var oauthUser = JSON.parse(data); + oauthUser.id = oauthUser.uid; + p.fulfill(oauthUser); + }); + return p; + }) + + .moduleErrback( function (err, seqValues) { + if (err instanceof Error) { + var next = seqValues.next; + return next(err); + } else if (err.extra) { + var deskResponse = err.extra.res, + serverResponse = seqValues.res; + serverResponse.writeHead( + deskResponse.statusCode, + deskResponse.headers); + serverResponse.end(err.extra.data); + } else if (err.statusCode) { + var serverResponse = seqValues.res; + serverResponse.writeHead(err.statusCode); + serverResponse.end(err.data); + } else { + console.error(err); + throw new Error('Unsupported error type'); + } + }); \ No newline at end of file From 426a8e1665cad27140ef15ff64152bb45c71e5c1 Mon Sep 17 00:00:00 2001 From: Chris Neumann Date: Sat, 17 Aug 2013 18:46:25 -0700 Subject: [PATCH 06/24] Change oauthHost and apiHost to be customizable. --- lib/modules/desk.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/modules/desk.js b/lib/modules/desk.js index e3129a3a..9fa795c9 100644 --- a/lib/modules/desk.js +++ b/lib/modules/desk.js @@ -3,8 +3,12 @@ var oauthModule = require('./oauth'), var desk = module.exports = oauthModule.submodule('desk') - .apiHost('https://datahero.desk.com/api/v2') - .oauthHost('https://datahero.desk.com') + // oauthHost & apiHost set at runtime, since they depend on the site prefix endpoint + .configurable({ + apiHost: 'https://SITE_PREFIX.desk.com/', + oauthHost: 'https://SITE_PREFIX.desk.com/' + }) + .requestTokenPath('/oauth/request_token') .accessTokenPath('/oauth/access_token') .authorizePath('/oauth/authorize') @@ -59,4 +63,4 @@ oauthModule.submodule('desk') console.error(err); throw new Error('Unsupported error type'); } - }); \ No newline at end of file + }); From 2d102a9415060453a5b17c822576a452e056cc1d Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Tue, 27 Aug 2013 23:44:13 -0700 Subject: [PATCH 07/24] Change oAuth initialization to allow for dynamic oauthHost names to be created. Introduce new steps to set the oAuthHostDomain. --- lib/modules/oauth.js | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/modules/oauth.js b/lib/modules/oauth.js index 65021a14..49c08d7e 100644 --- a/lib/modules/oauth.js +++ b/lib/modules/oauth.js @@ -20,17 +20,18 @@ everyModule.submodule('oauth') , convertErr: '(DEPRECATED) a function (data) that extracts an error message from data arg, where `data` is what is returned from a failed OAuth request' , authCallbackDidErr: 'Define the condition for the auth module determining if the auth callback url denotes a failure. Returns true/false.' }) - .definit( function () { - this.oauth = new OAuth( - this.oauthHost() + this.requestTokenPath() - , this.oauthHost() + this.accessTokenPath() - , this.consumerKey() - , this.consumerSecret() - , '1.0', null, 'HMAC-SHA1'); - }) .get('entryPath', 'the link a user follows, whereupon you redirect them to the 3rd party OAuth provider dialog - e.g., "/auth/twitter"') + .step('setDomain') + .description('sets the domain for dymamic domains - do not override to take oauthHost') + .accepts('req res next') + .promises(null) + .canBreakTo('authCallbackErrorSteps') + .step('initializeOAuth') + .description('This step initializes the oauth module') + .accepts('req res next') + .promises(null) .step('getRequestToken') .description('asks OAuth Provider for a request token') .accepts('req res next') @@ -92,6 +93,28 @@ everyModule.submodule('oauth') .accepts('req res next') .promises(null) + .setDomain ( function(req, res, next) { + //This is only to be overriden to dynamically set the oAuthHost + //To override, copy and uncomment the line below + //var p = this.Promise(); + //this._oauthHost = 'yourvalue'; + //p.fulfill(); + return; + }) + + .initializeOAuth (function (req, res, next) { + //If we had an initialization - delete it + if (this.oauth) + delete this.oauth; + + this.oauth = new OAuth( + this.oauthHost() + this.requestTokenPath() + , this.oauthHost() + this.accessTokenPath() + , this.consumerKey() + , this.consumerSecret() + , '1.0', null, 'HMAC-SHA1'); + }) + .getRequestToken( function (req, res, next) { // Automatic hostname detection + assignment @@ -265,4 +288,4 @@ oauth.requestTokenQueryParam = function (key, val) { if (val) this.moreRequestTokenQueryParams[key] = val; return this; -}; +}; \ No newline at end of file From 14a88a5c97d3be93472ddcaee7cad839b34d1af7 Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Tue, 27 Aug 2013 23:44:34 -0700 Subject: [PATCH 08/24] Modify desk module to use new dynamic host names --- lib/modules/desk.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/modules/desk.js b/lib/modules/desk.js index 9fa795c9..2651699f 100644 --- a/lib/modules/desk.js +++ b/lib/modules/desk.js @@ -3,12 +3,11 @@ var oauthModule = require('./oauth'), var desk = module.exports = oauthModule.submodule('desk') - // oauthHost & apiHost set at runtime, since they depend on the site prefix endpoint .configurable({ - apiHost: 'https://SITE_PREFIX.desk.com/', - oauthHost: 'https://SITE_PREFIX.desk.com/' + domain: "URL identifying domain for the api" }) - + .apiHost('https://something.desk.com/api/v2') + .oauthHost('https://somthing.desk.com') .requestTokenPath('/oauth/request_token') .accessTokenPath('/oauth/access_token') .authorizePath('/oauth/authorize') @@ -20,7 +19,7 @@ oauthModule.submodule('desk') return parsedUrl.query && !!parsedUrl.query.not_approved; }) - .handleAuthCallbackError( function (req, res) { + .handleAuthCallbackError( function (req, res, next) { var parsedUrl = url.parse(req.url, true), errorDesc = parsedUrl.query.error + "; " + parsedUrl.query.error_description; if (res.render) { @@ -35,7 +34,7 @@ oauthModule.submodule('desk') .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { var p = this.Promise(); - this.oauth.get(this.apiHost() + '/account', accessToken, accessTokenSecret, function (err, data) { + this.oauth.get(this.apiHost().call(this) + '/account', accessToken, accessTokenSecret, function (err, data) { if (err) return p.fail(err); var oauthUser = JSON.parse(data); oauthUser.id = oauthUser.uid; From c242903e96c4786d83c73f70faba4adfd7f5681e Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Mon, 14 Oct 2013 21:19:30 -0700 Subject: [PATCH 09/24] Add Microsoft Live oAuth module --- lib/modules/microsoft.js | 141 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 lib/modules/microsoft.js diff --git a/lib/modules/microsoft.js b/lib/modules/microsoft.js new file mode 100644 index 00000000..f824b01b --- /dev/null +++ b/lib/modules/microsoft.js @@ -0,0 +1,141 @@ +var oauthModule = require('./oauth2') + , url = require('url') + , request = require('request'); + +var microsoft = module.exports = +oauthModule.submodule('microsoft') + .configurable({ + scope: "URL identifying the Microsoft service to be accessed. See the documentation for the API you'd like to use for what scope to specify. To specify more than one scope, list each one separated with a space.", + display: "The display type used for the authentication page. Valid values are: 'popup', 'touch', 'page', 'none'", + locale: "Optional - A market string that determines how the consent UI is localized. Defaults to autodetect" + }) + + .oauthHost('https://login.live.com') + .apiHost('https://apis.live.net') + + .authPath('/oauth20_authorize.srf') + .authQueryParam('response_type', 'code') + + .accessTokenPath('/oauth20_token.srf') + .accessTokenParam('grant_type', 'authorization_code') + .accessTokenHttpMethod('post') + .postAccessTokenParamsVia('data') + + .entryPath('/auth/microsoft') + .callbackPath('/auth/microsoft/callback') + + .authQueryParam({ + display: function() { + return this._display && this.display(); + }, + locale: function () { + return this._locale && this.locale(); + }, + scope: function () { + return this._scope && this.scope(); + } + }) + + .addToSession( function (sess, auth) { + this._super(sess, auth); + if (auth.refresh_token) { + sess.auth[this.name].refreshToken = auth.refresh_token; + sess.auth[this.name].expiresInSeconds = parseInt(auth.expires_in, 10); + } + }) + + .authCallbackDidErr( function (req) { + var parsedUrl = url.parse(req.url, true); + return parsedUrl.query && !!parsedUrl.query.error; + }) + + .handleAuthCallbackError( function (req, res) { + var parsedUrl = url.parse(req.url, true) + , errorDesc = parsedUrl.query.error + "; " + parsedUrl.query.error_description; + if (res.render) { + res.render(__dirname + '/../views/auth-fail.jade', { + errorDescription: errorDesc + }); + } else { + // TODO Replace this with a nice fallback + throw new Error("You must configure handleAuthCallbackError if you are not using express"); + } + }) + .moduleErrback( function (err, seqValues) { + if (err instanceof Error) { + var next = seqValues.next; + return next(err); + } else if (err.extra) { + var microsoftResponse = err.extra.res + , serverResponse = seqValues.res; + serverResponse.writeHead( + microsoftResponse.statusCode + , microsoftResponse.headers); + serverResponse.end(err.extra.data); + } else if (err.statusCode) { + var serverResponse = seqValues.res; + serverResponse.writeHead(err.statusCode); + serverResponse.end(err.data); + } else { + console.error(err); + throw new Error('Unsupported error type'); + } + }) + + .fetchOAuthUser( function (accessToken, authResponse) { + var p = this.Promise(); + + request.get({ + url: this.apiHost() + '/v5.0/me', + qs: {access_token: accessToken} + }, function(err, res, body) { + if(err){ + return p.fail(err); + } else { + if(parseInt(res.statusCode/100,10) !== 2) { + return p.fail({extra:{data:body, res: res}}); + } + var oAuthUser = JSON.parse(body); + p.fulfill(oAuthUser); + } + }); + return p; + }); + +/** + * @param {Object} params in an object that includes the keys: + * - refreshToken: The refresh token returned from the authorization code + * exchange + * - clientId: The client_id obtained during application registration + * - clientSecret: The client secret obtained during the application registration + * @param {Function} cb + */ +microsoft.refreshToken = function (params, cb) { + request.post('https://login.live.com/oauth20_token.srf', { + form: { + refresh_token: params.refreshToken + , client_id: params.clientId + , client_secret: params.clientSecret + , grant_type: 'refresh_token' + } + }, function (err, res, body) { + // `body` should look like: + // { + // "access_token":"1/fFBGRNJru1FQd44AzqT3Zg", + // "expires_in":3920, + // "token_type":"Bearer", + // } + if (err) return cb(err); + if (parseInt(res.statusCode / 100, 10) !== 2) { + cb(null, {}, res); + } else { + body = JSON.parse(body); + cb(null, { + accessToken: body.access_token + , expiresIn: body.expires_in + , idToken: body.id_token + }, res); + } + }); + return this; +}; From 3155bb92ae946c390037033131c62ec67097e7f3 Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Fri, 15 Nov 2013 11:08:33 -0800 Subject: [PATCH 10/24] Desk.com changed their user information route from /accounts to /users/current. Fix to pull that info --- lib/modules/desk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/desk.js b/lib/modules/desk.js index 2651699f..7666104a 100644 --- a/lib/modules/desk.js +++ b/lib/modules/desk.js @@ -34,7 +34,7 @@ oauthModule.submodule('desk') .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { var p = this.Promise(); - this.oauth.get(this.apiHost().call(this) + '/account', accessToken, accessTokenSecret, function (err, data) { + this.oauth.get(this.apiHost().call(this) + '/users/current', accessToken, accessTokenSecret, function (err, data) { if (err) return p.fail(err); var oauthUser = JSON.parse(data); oauthUser.id = oauthUser.uid; From 5c6e1db4ad6624c328dd5cf809eeee28224a5c3c Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Mon, 25 Nov 2013 12:48:11 -0800 Subject: [PATCH 11/24] Add ability for accessToken requests to put data in the query string as well as in the data params for oauth2 requests. --- lib/modules/oauth2.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index c15a76a7..f43f4912 100644 --- a/lib/modules/oauth2.js +++ b/lib/modules/oauth2.js @@ -145,6 +145,7 @@ everyModule.submodule('oauth2') } , url = this._oauthHost + this._accessTokenPath , additionalParams = this.moreAccessTokenParams + , additionalQueryParams = this.moreAccessTokenQueryParams , param; if (this._accessTokenPath.indexOf("://") != -1) { @@ -154,6 +155,20 @@ everyModule.submodule('oauth2') url = this._accessTokenPath; } + //Some auths take params specific to query string, but still expect posted data + if(additionalQueryParams) for (var j in additionalQueryParams) { + param = additionalQueryParams[j]; + if ('function' === typeof param) { + additionalQueryParams[j] = // cache the fn call + param = param.call(this, data.req, data.res); + } + if ('function' === typeof param) { + param = param.call(this, data.req, data.res); + } + //Add this to the query of the post url - j is the key and param the data + url += '?' + j + '=' + param; + } + if (additionalParams) for (var k in additionalParams) { param = additionalParams[k]; if ('function' === typeof param) { @@ -258,6 +273,7 @@ everyModule.submodule('oauth2') oauth2.moreAuthQueryParams = {}; oauth2.moreAccessTokenParams = {}; +oauth2.moreAccessTokenQueryParams = {}; oauth2.cloneOnSubmodule.push('moreAuthQueryParams', 'moreAccessTokenParams'); oauth2 @@ -297,6 +313,18 @@ oauth2.accessTokenParam = function (key, val) { return this; }; +oauth2.accessTokenQueryParam = function (key, val) { + if (arguments.length === 1 && key.constructor == Object) { + for (var k in key) { + this.accessTokenQueryParam(k, key[k]); + } + return this; + } + if (val) + this.moreAccessTokenQueryParams[key] = val; + return this; +}; + /** * Where to redirect to after a failed or successful OAuth authorization */ From edab96a63a24642bbef42792ed3834d0357234d0 Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Mon, 25 Nov 2013 12:49:13 -0800 Subject: [PATCH 12/24] Adjust survey monkey everyauth module to allow for new oAuth params. https://github.com/Datahero/datahero-node/issues/3627 --- lib/modules/surveymonkey.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/modules/surveymonkey.js b/lib/modules/surveymonkey.js index b2b16e56..71f0e15b 100644 --- a/lib/modules/surveymonkey.js +++ b/lib/modules/surveymonkey.js @@ -18,15 +18,21 @@ oauthModule.submodule('surveymonkey') .authQueryParam('client_id', function () { return this._client_id && this.client_id(); }) + .authQueryParam('api_key', function() { + return this._appId && this.appId(); + }) - //Set up access Token path - requires client_id and client_secret (which is really your api key) + //Set up access Token path - requires client_id and client_secret .accessTokenPath('/oauth/token') + .accessTokenQueryParam('api_key', function() { + return this._appId && this.appId(); + }) .accessTokenParam('grant_type', 'authorization_code') .accessTokenParam('client_id', function() { return this._client_id && this.client_id(); }) .accessTokenParam('client_secret', function() { - return this._appId && this.appId(); + return this._appSecret && this.appSecret(); }) .accessTokenHttpMethod('post') .postAccessTokenParamsVia('data') @@ -88,10 +94,10 @@ oauthModule.submodule('surveymonkey') var next = seqValues.next; return next(err); } else if (err.extra) { - var surveymonkeyResponse = err.extra.res, + var surveymonkeyResponse = err.extra.res, serverResponse = seqValues.res; serverResponse.writeHead( - surveymonkeyResponse.statusCode, + surveymonkeyResponse.statusCode, surveymonkeyResponse.headers); serverResponse.end(err.extra.data); } else if (err.statusCode) { From 1b45e6daccee59ba37c7c508a19442b463dea568 Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Mon, 9 Dec 2013 17:30:40 -0800 Subject: [PATCH 13/24] Small hopeful fix for what seems to be a reserved variable url --- lib/modules/oauth2.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index f43f4912..70934a5d 100644 --- a/lib/modules/oauth2.js +++ b/lib/modules/oauth2.js @@ -143,7 +143,7 @@ everyModule.submodule('oauth2') , code: code , client_secret: this._appSecret } - , url = this._oauthHost + this._accessTokenPath + , specialUrl = this._oauthHost + this._accessTokenPath , additionalParams = this.moreAccessTokenParams , additionalQueryParams = this.moreAccessTokenQueryParams , param; @@ -152,7 +152,7 @@ everyModule.submodule('oauth2') // Just in case the access token url uses a different subdomain // than than the other urls involved in the oauth2 process. // * cough * ... gowalla - url = this._accessTokenPath; + specialUrl = this._accessTokenPath; } //Some auths take params specific to query string, but still expect posted data @@ -166,7 +166,7 @@ everyModule.submodule('oauth2') param = param.call(this, data.req, data.res); } //Add this to the query of the post url - j is the key and param the data - url += '?' + j + '=' + param; + specialUrl += '?' + j + '=' + param; } if (additionalParams) for (var k in additionalParams) { @@ -181,7 +181,7 @@ everyModule.submodule('oauth2') params[k] = param; } - var opts = { url: url } + var opts = { url: specialUrl } , paramsVia = this._postAccessTokenParamsVia; switch (paramsVia) { case 'query': // Submit as a querystring From c75622bf620f7c554bf135065f82bbaa3072400e Mon Sep 17 00:00:00 2001 From: Islam Sharabash Date: Mon, 30 Dec 2013 11:47:45 -0800 Subject: [PATCH 14/24] Don't follow redirectPath if headers sent. Everyauth lacks sane error handling. This allows us to redirect to a different path on error and not have problems with trying to send a response twice. Related https://github.com/Datahero/datahero-node/issues/419 --- lib/modules/oauth2.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index 70934a5d..1692d957 100644 --- a/lib/modules/oauth2.js +++ b/lib/modules/oauth2.js @@ -261,6 +261,9 @@ everyModule.submodule('oauth2') return this.redirect(res, continueTo); } + if (res.headerSent) + return; + var redirectTo = this._redirectPath; if (!redirectTo) throw new Error('You must configure a redirectPath'); From 0d40e5b71d38553838c09e2548b5284df9782577 Mon Sep 17 00:00:00 2001 From: Islam Sharabash Date: Sun, 5 Jan 2014 17:36:57 -0800 Subject: [PATCH 15/24] Adding mandrill authentication --- lib/modules/mandrill.js | 134 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 lib/modules/mandrill.js diff --git a/lib/modules/mandrill.js b/lib/modules/mandrill.js new file mode 100644 index 00000000..8646cf61 --- /dev/null +++ b/lib/modules/mandrill.js @@ -0,0 +1,134 @@ +var everyModule = require('./everymodule'), + url = require('url'), + querystring = require('querystring'), + request = require('request'), + extractHostname = require('../utils').extractHostname; + + +var mandrill = module.exports = +everyModule.submodule('mandrill') + .configurable({ + apiHost: 'e.g. https://mandrillapp.com/api/1.0/', + apiAuthUrl: 'e.g. https://mandrillapp.com/api-auth/', + authenticationId: 'The app authentication id generated from mandrill', + authCallbackDidErr: 'Define the condition for the auth module determining if the auth callback url denotes a failure. Returns true/false.', + myHostname: 'e.g., http://local.host:3000 . Notice no trailing slash', + redirectPath: 'the path to redirect once the user is authenticated' + }) + + // Declares a GET route that is aliased + // as 'entryPath'. The handler for this route + // triggers the series of steps that you see + // indented below it. + .get('entryPath', + 'the link a user follows, whereupon you redirect them to authentication url- e.g., "/auth/mandrill"') + .step('redirectToMandrill') + .accepts('req res next') + .promises(null) + + // post to callbackPath is aliased below. Mandrill redirects to callbackPath using both methods. + .get('callbackPath', + 'the callback path to redirect to after an authorization - e.g., "/auth/mandrill/callback"') + .step('getApiKey') + .description('retrieves a verifier code from the url query') + .accepts('req res next') + .promises('apiKey') + .canBreakTo('authCallbackErrorSteps') + .step('getSession') + .accepts('req') + .promises('session') + .step('fetchUser') + .accepts('apiKey') + .promises('mandrillUser') + .step('findOrCreateUser') + .accepts('session apiKey mandrillUser') + .promises('user') + .step('sendResponse') + .accepts('res') + .promises(null) + + .stepseq('authCallbackErrorSteps') + .step('handleAuthCallbackError', + 'a request handler that intercepts a failed authorization message sent from mandrill') + .accepts('req res next') + .promises(null) + + .apiAuthUrl('http://mandrillapp.com/api-auth/') + .apiHost('https://mandrillapp.com/api/1.0/') + .entryPath('/auth/mandrill') + .callbackPath('/auth/mandrill/callback') + + .redirectToMandrill(function(req, res) { + if (!this._myHostname) { + this.myHostname(extractHostname(req)); + } + + var authUrl, + params; + + params = { + id: this.authenticationId(), + redirect_url: this.myHostname() + this.callbackPath() + } + authUrl = this.apiAuthUrl() + '?' + querystring.stringify(params); + + this.redirect(res, authUrl); + }) + + .getApiKey(function (req, res, next) { + var data, + apiKey; + + if (this._authCallbackDidErr(req)) { + return this.breakTo('authCallbackErrorSteps', req, res, next); + } + + // Note: This assumes that you're using connect.bodyParser + // TODO(ibash) handle both cases where bodyParser is / is not used + apiKey = req.body.key; + return apiKey; + }) + + .getSession(function(req) { + return req.session; + }) + + .fetchUser(function(apiKey) { + var promise = this.Promise(), + userUrl = this.apiHost() + '/users/info.json'; + + request.post({url: userUrl, json:{key: apiKey}}, function(error, res, body) { + if (error) { + error.extra = {res: res, data: body}; + return promise.fail(error); + } + + if (body && body.status && body.status === 'error') { + // error from mandrill + var errorMsg = body.name + ': ' + body.message; + return promise.fail(new Error(errorMsg)); + } + + // body is an object representing the user + promise.fulfill(body); + }); + + return promise; + }) + + .sendResponse( function (res) { + var redirectTo = this.redirectPath(); + if (!redirectTo) + throw new Error('You must configure a redirectPath'); + this.redirect(res, redirectTo); + }) + + .authCallbackDidErr(function(req) { + return req.query && !!req.query.error; + }) + .handleAuthCallbackError(function(req, res, next) { + next(new Error("Authorization Error")); + }); + +// alias post callbackPath to get callbackPath +mandrill._stepSequences['post:callbackPath'] = mandrill._stepSequences['get:callbackPath']; From 76cfc461dc9ef6941f85285eaf07af06ffe76b92 Mon Sep 17 00:00:00 2001 From: Islam Sharabash Date: Wed, 22 Jan 2014 14:40:42 -0800 Subject: [PATCH 16/24] adding additonal parameters to err.extra and fixing issue with additonalQueryParams in oauth2 related https://github.com/Datahero/datahero-node/issues/4328 --- lib/modules/oauth2.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index 1692d957..41eb3c6b 100644 --- a/lib/modules/oauth2.js +++ b/lib/modules/oauth2.js @@ -156,17 +156,21 @@ everyModule.submodule('oauth2') } //Some auths take params specific to query string, but still expect posted data - if(additionalQueryParams) for (var j in additionalQueryParams) { - param = additionalQueryParams[j]; - if ('function' === typeof param) { - additionalQueryParams[j] = // cache the fn call + if (additionalQueryParams) { + var queryParams = {}; + for (var j in additionalQueryParams) { + param = additionalQueryParams[j]; + if ('function' === typeof param) { + additionalQueryParams[j] = // cache the fn call + param = param.call(this, data.req, data.res); + } + if ('function' === typeof param) { param = param.call(this, data.req, data.res); + } + queryParams[j] = param; } - if ('function' === typeof param) { - param = param.call(this, data.req, data.res); - } - //Add this to the query of the post url - j is the key and param the data - specialUrl += '?' + j + '=' + param; + + specialUrl += '?' + querystring.stringify(queryParams); } if (additionalParams) for (var k in additionalParams) { @@ -198,10 +202,14 @@ everyModule.submodule('oauth2') opts[paramsVia] = params; request[this._accessTokenHttpMethod](opts, function (err, res, body) { if (err) { - err.extra = {data: body, res: res}; + err.extra = {data: body, res: res, url: specialUrl, + additionalParams: additionalParams, additionalQueryParams: additionalQueryParams}; return p.fail(err); } - if (parseInt(res.statusCode / 100) != 2) return p.fail({statusCode: res.statusCode, data: body}); + if (parseInt(res.statusCode / 100) != 2) { + return p.fail({statusCode: res.statusCode, data: body, url: specialUrl, + additionalParams: additionalParams, additionalQueryParams: additionalQueryParams}); + } var resType = res.headers['content-type'] , data; if (resType.substring(0, 10) === 'text/plain') { From dd89a37fa604b628e3e53a62f7bc1374f3e39f9a Mon Sep 17 00:00:00 2001 From: Jeff Zabel Date: Fri, 31 Jan 2014 15:51:32 -0700 Subject: [PATCH 17/24] Add issue that moreAccessTokenQueryParams was not getting cloned correctly resulting in leaks. Fixes https://github.com/Datahero/datahero-node/issues/4602 --- lib/modules/oauth2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index 41eb3c6b..3d0d42e8 100644 --- a/lib/modules/oauth2.js +++ b/lib/modules/oauth2.js @@ -285,7 +285,7 @@ everyModule.submodule('oauth2') oauth2.moreAuthQueryParams = {}; oauth2.moreAccessTokenParams = {}; oauth2.moreAccessTokenQueryParams = {}; -oauth2.cloneOnSubmodule.push('moreAuthQueryParams', 'moreAccessTokenParams'); +oauth2.cloneOnSubmodule.push('moreAuthQueryParams', 'moreAccessTokenParams', 'moreAccessTokenQueryParams'); oauth2 .authPath('/oauth/authorize') From 7f1b5c1021720f96738e5a4b9f0736d2d92c30a1 Mon Sep 17 00:00:00 2001 From: Islam Sharabash Date: Wed, 9 Apr 2014 11:37:32 -0700 Subject: [PATCH 18/24] Bumping version of connect because of vulnerability ref: https://nodesecurity.io/advisories/methodOverride_Middleware_Reflected_Cross-Site_Scripting --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cf6b838..f7518057 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dependencies": { "oauth": "https://github.com/ciaranj/node-oauth/tarball/master", "request": "2.9.x", - "connect": "2.3.x", + "connect": ">=2.8.1", "openid": ">=0.2.0", "xml2js": ">=0.1.7", "node-swt": ">=0.1.1", From 4173fc6ad78ee6f258efac79c104adb7c1def277 Mon Sep 17 00:00:00 2001 From: Blair Anderson Date: Tue, 22 Apr 2014 13:49:47 -0600 Subject: [PATCH 19/24] start zendesk strategy --- lib/modules/zendesk.js | 81 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 lib/modules/zendesk.js diff --git a/lib/modules/zendesk.js b/lib/modules/zendesk.js new file mode 100644 index 00000000..a8d62cae --- /dev/null +++ b/lib/modules/zendesk.js @@ -0,0 +1,81 @@ +var oauthModule = require('./oauth'), + url = require('url'); + +var zendesk = module.exports = +oauthModule.submodule('zendesk') + .configurable({ + domain: "URL identifying domain for the api", + scope: "Zendesk scope values, either read or write or both." + }) + .apiHost('https://something.zendesk.com/api/v2') + .oauthHost('https://somthing.zendesk.com') + + .authorizePath('/oauth/authorizations/new') + .authorizeQueryParam('response_type', 'code') + .authorizeQueryParam('scope', 'read') + + .requestTokenPath('/oauth/tokens') + .accessTokenPath('/oauth/access_token') + + .entryPath('/auth/zendesk') + .callbackPath('/auth/zendesk/callback') + + .authCallbackDidErr( function (req) { + var parsedUrl = url.parse(req.url, true); + return parsedUrl.query && !!parsedUrl.query.not_approved; + }) + + .handleAuthCallbackError( function (req, res, next) { + var parsedUrl = url.parse(req.url, true), + errorDesc = parsedUrl.query.error + "; " + parsedUrl.query.error_description; + if (res.render) { + res.render(__dirname + '/../views/auth-fail.jade', { + errorDescription: errorDesc + }); + } else { + // TODO Replace this with a nice fallback + throw new Error("You must configure handleAuthCallbackError if you are not using express"); + } + }) + + .fetchOAuthUser( function (accessToken, authResponse) { + var p = this.Promise(); + + request.get({ + url: this.apiHost() + '/users/me', + headers: {'Authorization': 'Bearer ' + accessToken} + }, function(err, res, body) { + if(err){ + return p.fail(err); + } else { + if(parseInt(res.statusCode/100,10) !=2) { + return p.fail({extra:{data:body, res: res}}); + } + var oAuthUser = JSON.parse(body); + p.fulfill(oAuthUser); + } + }); + return p; + }) + + + .moduleErrback( function (err, seqValues) { + if (err instanceof Error) { + var next = seqValues.next; + return next(err); + } else if (err.extra) { + var zendeskResponse = err.extra.res, + serverResponse = seqValues.res; + serverResponse.writeHead( + zendeskResponse.statusCode, + zendeskResponse.headers); + serverResponse.end(err.extra.data); + } else if (err.statusCode) { + var serverResponse = seqValues.res; + serverResponse.writeHead(err.statusCode); + serverResponse.end(err.data); + } else { + console.error(err); + throw new Error('Unsupported error type'); + } + }); From 50d8a7fcc9c70ab355a7d319e25861d3807146bb Mon Sep 17 00:00:00 2001 From: Blair Anderson Date: Wed, 23 Apr 2014 14:45:45 -0600 Subject: [PATCH 20/24] add setDomain to oauth2 --- lib/modules/oauth2.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index 3d0d42e8..01546459 100644 --- a/lib/modules/oauth2.js +++ b/lib/modules/oauth2.js @@ -40,6 +40,9 @@ everyModule.submodule('oauth2') // indented below it. .get('entryPath', 'the link a user follows, whereupon you redirect them to the 3rd party OAuth provider dialog - e.g., "/auth/facebook"') + .step('setDomain') + .accepts('req res next') + .promises(null) .step('getAuthUri') .accepts('req res next') .promises('authUri') @@ -83,6 +86,15 @@ everyModule.submodule('oauth2') .accepts('req res next') .promises(null) + .setDomain ( function(req, res, next) { + //This is only to be overriden to dynamically set the oAuthHost + //To override, copy and uncomment the line below + //var p = this.Promise(); + //this._oauthHost = 'yourvalue'; + //p.fulfill(); + return; + }) + .getAuthUri( function (req, res, next) { // Automatic hostname detection + assignment From cb59d460ba148d0f07256b37a84cc694ed858196 Mon Sep 17 00:00:00 2001 From: Blair Anderson Date: Wed, 23 Apr 2014 14:45:53 -0600 Subject: [PATCH 21/24] migrate zendesk to oauth2 --- lib/modules/zendesk.js | 45 +++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/lib/modules/zendesk.js b/lib/modules/zendesk.js index a8d62cae..8a97faca 100644 --- a/lib/modules/zendesk.js +++ b/lib/modules/zendesk.js @@ -1,4 +1,4 @@ -var oauthModule = require('./oauth'), +var oauthModule = require('./oauth2'), url = require('url'); var zendesk = module.exports = @@ -10,34 +10,12 @@ oauthModule.submodule('zendesk') .apiHost('https://something.zendesk.com/api/v2') .oauthHost('https://somthing.zendesk.com') - .authorizePath('/oauth/authorizations/new') - .authorizeQueryParam('response_type', 'code') - .authorizeQueryParam('scope', 'read') - - .requestTokenPath('/oauth/tokens') - .accessTokenPath('/oauth/access_token') + .authPath('/oauth/authorizations/new') + .accessTokenPath('/oauth/tokens') .entryPath('/auth/zendesk') .callbackPath('/auth/zendesk/callback') - .authCallbackDidErr( function (req) { - var parsedUrl = url.parse(req.url, true); - return parsedUrl.query && !!parsedUrl.query.not_approved; - }) - - .handleAuthCallbackError( function (req, res, next) { - var parsedUrl = url.parse(req.url, true), - errorDesc = parsedUrl.query.error + "; " + parsedUrl.query.error_description; - if (res.render) { - res.render(__dirname + '/../views/auth-fail.jade', { - errorDescription: errorDesc - }); - } else { - // TODO Replace this with a nice fallback - throw new Error("You must configure handleAuthCallbackError if you are not using express"); - } - }) - .fetchOAuthUser( function (accessToken, authResponse) { var p = this.Promise(); @@ -58,6 +36,23 @@ oauthModule.submodule('zendesk') return p; }) + .authCallbackDidErr( function (req) { + var parsedUrl = url.parse(req.url, true); + return parsedUrl.query && !!parsedUrl.query.not_approved; + }) + + .handleAuthCallbackError( function (req, res, next) { + var parsedUrl = url.parse(req.url, true), + errorDesc = parsedUrl.query.error + "; " + parsedUrl.query.error_description; + if (res.render) { + res.render(__dirname + '/../views/auth-fail.jade', { + errorDescription: errorDesc + }); + } else { + // TODO Replace this with a nice fallback + throw new Error("You must configure handleAuthCallbackError if you are not using express"); + } + }) .moduleErrback( function (err, seqValues) { if (err instanceof Error) { From a5c3469bbff3229052d2c2c295fd6786bc27444b Mon Sep 17 00:00:00 2001 From: Blair Anderson Date: Wed, 23 Apr 2014 18:16:46 -0600 Subject: [PATCH 22/24] implement fetchOAuthUser --- lib/modules/zendesk.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/modules/zendesk.js b/lib/modules/zendesk.js index 8a97faca..17e3e6f8 100644 --- a/lib/modules/zendesk.js +++ b/lib/modules/zendesk.js @@ -1,4 +1,5 @@ var oauthModule = require('./oauth2'), + request = require('request'), url = require('url'); var zendesk = module.exports = @@ -12,27 +13,25 @@ oauthModule.submodule('zendesk') .authPath('/oauth/authorizations/new') .accessTokenPath('/oauth/tokens') + .accessTokenParam('grant_type', 'authorization_code') .entryPath('/auth/zendesk') .callbackPath('/auth/zendesk/callback') - .fetchOAuthUser( function (accessToken, authResponse) { - var p = this.Promise(); + .fetchOAuthUser( function (accessToken) { + var p = this.Promise(), + url = this._apiHost() + '/users/me.json', + headers = {'Authorization': 'Bearer ' + accessToken}; request.get({ - url: this.apiHost() + '/users/me', - headers: {'Authorization': 'Bearer ' + accessToken} - }, function(err, res, body) { - if(err){ - return p.fail(err); - } else { - if(parseInt(res.statusCode/100,10) !=2) { - return p.fail({extra:{data:body, res: res}}); - } - var oAuthUser = JSON.parse(body); - p.fulfill(oAuthUser); - } + url: url, + headers: headers + }, function(err, data, body){ + if (err) return p.fail(err); + var oauthUser = JSON.parse(body).user; + p.fulfill(oauthUser); }); + return p; }) From da44b762f4a2e08af31c86307335cccfb17f4291 Mon Sep 17 00:00:00 2001 From: Doug Farre Date: Tue, 27 May 2014 12:44:15 -0600 Subject: [PATCH 23/24] adding paypal module --- lib/modules/paypal.js | 104 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 lib/modules/paypal.js diff --git a/lib/modules/paypal.js b/lib/modules/paypal.js new file mode 100644 index 00000000..d94960a5 --- /dev/null +++ b/lib/modules/paypal.js @@ -0,0 +1,104 @@ +var oauthModule = require('./oauth2') + , url = require('url'); + +var paypal = module.exports = +oauthModule.submodule('paypal') + .configurable({ + scope: 'specify types of access: See https://developer.paypal.com/docs/integration/direct/identity/attributes/' + }) + + .oauthHost('https://www.paypal.com') + .apiHost('https://api.paypal.com') + + .authPath('/webapps/auth/protocol/openidconnect/v1/authorize') + .authQueryParam('response_type', 'code') + + // See line 158 in oauth2.js for reason to include the entire url here + + // Use oAuth request to retrive an acces toekn for use with api calls + .accessTokenPath(this._apiHost() + 'v1/oauth2/token') + + // (Identity) Grant token from authorization code + .accessTokenPath(this._apiHost() + 'v1/identity/openidconnect/tokenservice') + + .accessTokenParam('grant_type', 'client_credentials') + .accessTokenHttpMethod('post') + .postAccessTokenParamsVia('data') + + .entryPath('/auth/paypal') + .callbackPath('/auth/paypal/callback') + + .authQueryParam('scope', function () { + return this._scope && this.scope(); + }) + + .authCallbackDidErr( function (req) { + var parsedUrl = url.parse(req.url, true); + return parsedUrl.query && !!parsedUrl.query.error; + }) + .handleAuthCallbackError( function (req, res) { + var parsedUrl = url.parse(req.url, true) + , errorDesc = parsedUrl.query.error_description; + if (res.render) { + res.render(__dirname + '/../views/auth-fail.jade', { + errorDescription: errorDesc + }); + } else { + // TODO Replace this with a nice fallback + throw new Error("You must configure handleAuthCallbackError if you are not using express"); + } + }) + + .fetchOAuthUser( function (accessToken) { + var p = this.Promise(), + url = this._apiHost() + '/v1/identity/openidconnect/userinfo/?schema=openid', + headers = {'Authorization': 'Bearer ' + accessToken}, + queryParams = '?schema=openid'; + + request.get({ + url: url + queryParams, + headers: headers + }, function(err, data, body) { + if (err) return p.fail(err); + var oauthUser = JSON.parse(body).user; + p.fulfill(oauthUser); + }); + + return p; + }) + + + .moduleErrback( function (err, seqValues) { + if (err instanceof Error) { + var next = seqValues.next; + return next(err); + } else if (err.extra) { + var facebookResponse = err.extra.res + , serverResponse = seqValues.res; + serverResponse.writeHead( + facebookResponse.statusCode + , facebookResponse.headers); + serverResponse.end(err.extra.data); + } else if (err.statusCode) { + var serverResponse = seqValues.res; + serverResponse.writeHead(err.statusCode); + serverResponse.end(err.data); + } else { + console.error(err); + throw new Error('Unsupported error type'); + } + }); + +fb.mobile = function (isMobile) { + if (isMobile) { + this.authPath('https://m.facebook.com/dialog/oauth'); + } + return this; +}; + +fb.popup = function (isPopup) { + if (isPopup) { + this.authQueryParam('display', 'popup'); + } + return this; +}; From 35fb39fb62455e9d583cb8c63a608045a9e7e2b2 Mon Sep 17 00:00:00 2001 From: Doug Farre Date: Wed, 28 May 2014 16:12:08 -0600 Subject: [PATCH 24/24] unverified first attempt at everyauth paypal module; comments exist describing what functionality should be tested further --- lib/modules/paypal.js | 147 +++++++++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 23 deletions(-) diff --git a/lib/modules/paypal.js b/lib/modules/paypal.js index d94960a5..7d87d8b7 100644 --- a/lib/modules/paypal.js +++ b/lib/modules/paypal.js @@ -1,4 +1,10 @@ var oauthModule = require('./oauth2') + , querystring = require('querystring') + , request = require('request') + , everyModule = require('./everymodule') + , OAuth = require('oauth').OAuth2 + , url = require('url') + , extractHostname = require('../utils').extractHostname , url = require('url'); var paypal = module.exports = @@ -7,19 +13,20 @@ oauthModule.submodule('paypal') scope: 'specify types of access: See https://developer.paypal.com/docs/integration/direct/identity/attributes/' }) + // Override these in implementation to hit sandbox during development .oauthHost('https://www.paypal.com') .apiHost('https://api.paypal.com') .authPath('/webapps/auth/protocol/openidconnect/v1/authorize') .authQueryParam('response_type', 'code') - // See line 158 in oauth2.js for reason to include the entire url here - // Use oAuth request to retrive an acces toekn for use with api calls - .accessTokenPath(this._apiHost() + 'v1/oauth2/token') + // Use oAuth request to retrive an acces toekn for use with api calls + // See line 158 in oauth2.js for reason to include the entire url (and not the typically use relative path) + .accessTokenPath('https://api.sandbox.paypal.com/v1/oauth2/token') - // (Identity) Grant token from authorization code - .accessTokenPath(this._apiHost() + 'v1/identity/openidconnect/tokenservice') + // (Identity) Grant token from authorization code: third party site sign-in + //.accessTokenPath('https://api.paypal.com/v1/identity/openidconnect/tokenservice') .accessTokenParam('grant_type', 'client_credentials') .accessTokenHttpMethod('post') @@ -34,8 +41,10 @@ oauthModule.submodule('paypal') .authCallbackDidErr( function (req) { var parsedUrl = url.parse(req.url, true); + console.log("error happends here:", parsedUrl.query); return parsedUrl.query && !!parsedUrl.query.error; }) + .handleAuthCallbackError( function (req, res) { var parsedUrl = url.parse(req.url, true) , errorDesc = parsedUrl.query.error_description; @@ -51,7 +60,7 @@ oauthModule.submodule('paypal') .fetchOAuthUser( function (accessToken) { var p = this.Promise(), - url = this._apiHost() + '/v1/identity/openidconnect/userinfo/?schema=openid', + url = this._apiHost() + '/v1/identity/openidconnect/userinfo/', headers = {'Authorization': 'Bearer ' + accessToken}, queryParams = '?schema=openid'; @@ -67,17 +76,123 @@ oauthModule.submodule('paypal') return p; }) + .getAccessToken( function (code, data) { + console.log("oauth2 getAccessToken data: ", data); + + var p = this.Promise() + , params = { + // PayPal expects incomming requests to treat thses as Basic Http Auth credentials + // https://developer.paypal.com/docs/integration/direct/make-your-first-call/ + //client_id: this._appId + //client_secret: this._appSecret + redirect_uri: this._myHostname + this._callbackPath, + code: code + } + , specialUrl = this._oauthHost + this._accessTokenPath + , additionalParams = this.moreAccessTokenParams + , additionalQueryParams = this.moreAccessTokenQueryParams + , param; + + if (this._accessTokenPath.indexOf("://") != -1) { + // Just in case the access token url uses a different subdomain + // than than the other urls involved in the oauth2 process. + // * cough * ... gowalla + specialUrl = this._accessTokenPath; + } + + //Some auths take params specific to query string, but still expect posted data + if (additionalQueryParams) { + var queryParams = {}; + for (var j in additionalQueryParams) { + param = additionalQueryParams[j]; + if ('function' === typeof param) { + additionalQueryParams[j] = // cache the fn call + param = param.call(this, data.req, data.res); + } + if ('function' === typeof param) { + param = param.call(this, data.req, data.res); + } + queryParams[j] = param; + } + + specialUrl += '?' + querystring.stringify(queryParams); + } + + if (additionalParams) for (var k in additionalParams) { + param = additionalParams[k]; + if ('function' === typeof param) { + additionalParams[k] = // cache the fn call + param = param.call(this, data.req, data.res); + } + if ('function' === typeof param) { + param = param.call(this, data.req, data.res); + } + params[k] = param; + } + + var opts = { url: specialUrl } + , paramsVia = this._postAccessTokenParamsVia; + switch (paramsVia) { + case 'query': // Submit as a querystring + opts.headers || (opts.headers = {}); + opts.headers['Content-Length'] = 0; + paramsVia = 'qs'; + break; + case 'data': // Submit via application/x-www-form-urlencoded + paramsVia = 'form'; + break; + default: + throw new Error('postAccessTokenParamsVia must be either "query" or "data"'); + } + + opts[paramsVia] = params; + + // To client_id & client_secret as http basic auth creds, add the "auth" block to the request's options + // TODO: the previouse statment needs to be verified; perhaps including creds in custom header would be better + // https://github.com/mikeal/request + opts['auth'] = { user: this._appId, pass: this._appSecret, sendImmediately: true}; + + request[this._accessTokenHttpMethod](opts, function (err, res, body) { + if (err) { + err.extra = {data: body, res: res, url: specialUrl, + additionalParams: additionalParams, additionalQueryParams: additionalQueryParams}; + return p.fail(err); + } + + if (parseInt(res.statusCode / 100) != 2) { + return p.fail({statusCode: res.statusCode, data: body, url: specialUrl, + additionalParams: additionalParams, additionalQueryParams: additionalQueryParams}); + } + var resType = res.headers['content-type'] + , data; + if (resType.substring(0, 10) === 'text/plain') { + data = querystring.parse(body); + } else if (resType.substring(0, 33) === 'application/x-www-form-urlencoded') { + data = querystring.parse(body); + } else if (resType.substring(0, 16) === 'application/json') { + data = JSON.parse(body); + } else { + throw new Error('Unsupported content-type ' + resType); + } + var aToken = data.access_token; + + delete data.access_token; + p.fulfill(aToken, data); + }); + + return p; + }) .moduleErrback( function (err, seqValues) { if (err instanceof Error) { var next = seqValues.next; return next(err); } else if (err.extra) { - var facebookResponse = err.extra.res + var ghResponse = err.extra.res , serverResponse = seqValues.res; serverResponse.writeHead( - facebookResponse.statusCode - , facebookResponse.headers); + ghResponse.statusCode + , ghResponse.headers); serverResponse.end(err.extra.data); } else if (err.statusCode) { var serverResponse = seqValues.res; @@ -88,17 +203,3 @@ oauthModule.submodule('paypal') throw new Error('Unsupported error type'); } }); - -fb.mobile = function (isMobile) { - if (isMobile) { - this.authPath('https://m.facebook.com/dialog/oauth'); - } - return this; -}; - -fb.popup = function (isPopup) { - if (isPopup) { - this.authQueryParam('display', 'popup'); - } - return this; -};