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 diff --git a/lib/modules/desk.js b/lib/modules/desk.js new file mode 100644 index 00000000..7666104a --- /dev/null +++ b/lib/modules/desk.js @@ -0,0 +1,65 @@ +var oauthModule = require('./oauth'), + url = require('url'); + +var desk = module.exports = +oauthModule.submodule('desk') + .configurable({ + 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') + .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, 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, accessTokenSecret, params) { + var p = this.Promise(); + 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; + 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'); + } + }); 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']; 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; +}; 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 diff --git a/lib/modules/oauth2.js b/lib/modules/oauth2.js index 9a4592a9..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 @@ -143,15 +155,34 @@ 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; 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 - url = this._accessTokenPath; + 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) { @@ -166,7 +197,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 @@ -183,10 +214,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({extra: {res: res, 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') { @@ -246,6 +281,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'); @@ -258,7 +296,8 @@ everyModule.submodule('oauth2') oauth2.moreAuthQueryParams = {}; oauth2.moreAccessTokenParams = {}; -oauth2.cloneOnSubmodule.push('moreAuthQueryParams', 'moreAccessTokenParams'); +oauth2.moreAccessTokenQueryParams = {}; +oauth2.cloneOnSubmodule.push('moreAuthQueryParams', 'moreAccessTokenParams', 'moreAccessTokenQueryParams'); oauth2 .authPath('/oauth/authorize') @@ -297,6 +336,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 */ diff --git a/lib/modules/paypal.js b/lib/modules/paypal.js new file mode 100644 index 00000000..7d87d8b7 --- /dev/null +++ b/lib/modules/paypal.js @@ -0,0 +1,205 @@ +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 = +oauthModule.submodule('paypal') + .configurable({ + 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') + + + // 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: third party site sign-in + //.accessTokenPath('https://api.paypal.com/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); + 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; + 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/', + 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; + }) + + .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 ghResponse = err.extra.res + , serverResponse = seqValues.res; + serverResponse.writeHead( + ghResponse.statusCode + , ghResponse.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'); + } + }); diff --git a/lib/modules/surveymonkey.js b/lib/modules/surveymonkey.js new file mode 100644 index 00000000..71f0e15b --- /dev/null +++ b/lib/modules/surveymonkey.js @@ -0,0 +1,111 @@ +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(); + }) + .authQueryParam('api_key', function() { + return this._appId && this.appId(); + }) + + //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._appSecret && this.appSecret(); + }) + .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) { + 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) { + 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'); + } + }); diff --git a/lib/modules/zendesk.js b/lib/modules/zendesk.js new file mode 100644 index 00000000..17e3e6f8 --- /dev/null +++ b/lib/modules/zendesk.js @@ -0,0 +1,75 @@ +var oauthModule = require('./oauth2'), + request = require('request'), + 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') + + .authPath('/oauth/authorizations/new') + .accessTokenPath('/oauth/tokens') + .accessTokenParam('grant_type', 'authorization_code') + + .entryPath('/auth/zendesk') + .callbackPath('/auth/zendesk/callback') + + .fetchOAuthUser( function (accessToken) { + var p = this.Promise(), + url = this._apiHost() + '/users/me.json', + headers = {'Authorization': 'Bearer ' + accessToken}; + + request.get({ + 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; + }) + + .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) { + 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'); + } + }); 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",