diff --git a/examples/client_auth/README.md b/examples/client_auth/README.md index a0b01e4..e7a905c 100644 --- a/examples/client_auth/README.md +++ b/examples/client_auth/README.md @@ -1,19 +1,26 @@ -# Podio Password Auth +# Podio Client Auth A simple example app for client side authentication against the Platform API. # Installation ``` -npm install +$ npm install ``` +# Configuration + Make sure to add your clientId in the [config file](https://github.com/podio/podio-js/blob/master/examples/client_auth/public/javascript/config.js). -# Server +Also, go to the root of this repository and compile PodioJS by running +``` +$ npm run bundle +``` + +# Server ``` -npm start +$ npm start ``` The app is accessible through http://localhost:3000 diff --git a/examples/client_auth/public/javascript/index.js b/examples/client_auth/public/javascript/index.js index 0e5b71d..dc10a0d 100644 --- a/examples/client_auth/public/javascript/index.js +++ b/examples/client_auth/public/javascript/index.js @@ -46,8 +46,8 @@ function onStartAuthClick(e) { var elmBody = document.body; - if (!platform.isAuthenticated()) { - // methods are registered globally for the popup to call on this main window + platform.isAuthenticated().catch(function (err) { + // methods are registered globally for the popup to call on this main window window.onAuthCompleted = function() { // the platform SDK instance from the popup has // received a new auth token and saved it to the store. @@ -61,7 +61,7 @@ }; openPopup(); - } + }); } /*** @@ -72,14 +72,14 @@ function onRequestUserClick(e) { var elmBody = document.body; - if (platform.isAuthenticated()) { + platform.isAuthenticated().then(function () { platform.request('get', '/user/status') - .then(function(responseData) { - elmBody.innerHTML = compiledUser({ profile: responseData.profile }); - }); - } else { + .then(function(responseData) { + elmBody.innerHTML = compiledUser({ profile: responseData.profile }); + }); + }).catch(function () { elmBody.innerHTML = compiledError(); - } + }); } // Use event delegation @@ -98,8 +98,8 @@ // replace the content with a success template // if we had authenticated previously and auth tokens // are available in the store - if (platform.isAuthenticated()) { + platform.isAuthenticated().then(function () { document.body.innerHTML = compiledSuccess(); - } + }); })(PodioJS, SessionStore, PlatformConfig, _); diff --git a/examples/client_auth/public/javascript/sessionStore.js b/examples/client_auth/public/javascript/sessionStore.js index 29884c3..4f1a582 100644 --- a/examples/client_auth/public/javascript/sessionStore.js +++ b/examples/client_auth/public/javascript/sessionStore.js @@ -4,8 +4,8 @@ var podioOAuth = localStorage.getItem('podioOAuth'); if (podioOAuth) { podioOAuth = JSON.parse(podioOAuth); - callback(podioOAuth); } + callback(podioOAuth || {}); }, set: function(podioOAuth, authType) { localStorage.setItem('podioOAuth', JSON.stringify(podioOAuth)); diff --git a/examples/password_auth/.gitignore b/examples/password_auth/.gitignore index de29dd0..1ee3b53 100644 --- a/examples/password_auth/.gitignore +++ b/examples/password_auth/.gitignore @@ -1 +1,2 @@ -node_modules/**/* \ No newline at end of file +node_modules/**/* +config.json \ No newline at end of file diff --git a/examples/password_auth/README.md b/examples/password_auth/README.md index 03ed09b..f9222da 100644 --- a/examples/password_auth/README.md +++ b/examples/password_auth/README.md @@ -8,7 +8,15 @@ A simple example app for password authentication against the Platform API. npm install ``` -Make sure to add your credentials to the [routes file](https://github.com/podio/podio-js/blob/master/examples/password_auth/routes/index.js#L6-L9). +Make sure to add your client ID, client secret, username and password in a new file ```./config.json```. It should have the following format: +``` +{ + "clientId": "", + "clientSecret": "", + "username": "", + "password": " 0) { callback(JSON.parse(data)); + } else { + callback(); } }); } diff --git a/examples/server_auth/.gitignore b/examples/server_auth/.gitignore index de29dd0..1ee3b53 100644 --- a/examples/server_auth/.gitignore +++ b/examples/server_auth/.gitignore @@ -1 +1,2 @@ -node_modules/**/* \ No newline at end of file +node_modules/**/* +config.json \ No newline at end of file diff --git a/examples/server_auth/README.md b/examples/server_auth/README.md index cddf046..e3f2a8f 100644 --- a/examples/server_auth/README.md +++ b/examples/server_auth/README.md @@ -8,7 +8,13 @@ A simple example app for server-side authentication against the Platform API. npm install ``` -Make sure to add your clientId and clientSecret to the [routes file](https://github.com/podio/podio-js/blob/master/examples/server_auth/routes/index.js#L9-L10). +Make sure to add your client ID and client secret in a new file ```./config.json```. It should have the following format: +``` +{ + "clientId": "", + "clientSecret": "" +} +``` # Server diff --git a/examples/server_auth/routes/index.js b/examples/server_auth/routes/index.js index a483442..70e9dfe 100644 --- a/examples/server_auth/routes/index.js +++ b/examples/server_auth/routes/index.js @@ -6,8 +6,17 @@ var Busboy = require("busboy"); var temp = require('temp'); var fs = require('fs'); -var clientId = ''; // your clientId here -var clientSecret = '' // your clientSecret here; +// Remember to place a file in this folder called 'config.json', +// with the contents formatted like so: +// { +// "clientId": "", +// "clientSecret": """ +// } +var config = JSON.parse(fs.readFileSync('./config.json')); + +var clientId = config.clientId; +var clientSecret = config.clientSecret; + var podio = new PodioJS({ authType: 'server', clientId: clientId, clientSecret: clientSecret }, { sessionStore: sessionStore }); function getFullURL(req) { @@ -20,12 +29,14 @@ router.get('/', function(req, res) { var errorCode = req.query.error; var redirectURL = getFullURL(req); - if (podio.isAuthenticated()) { + podio.isAuthenticated() + .then(function () { // ready to make API calls res.render('success'); - } else { + }).catch(function () { + if (typeof authCode !== 'undefined') { - podio.getAccessToken(authCode, redirectURL, function () { + podio.getAccessToken(authCode, redirectURL, function (err) { // we are ready to make API calls res.render('success'); }); @@ -36,17 +47,21 @@ router.get('/', function(req, res) { // we have neither an authCode nor have we authenticated before res.render('index', { authUrl: podio.getAuthorizationURL(redirectURL) }); } - } + }); }); router.get('/user', function(req, res) { - if (podio.isAuthenticated()) { - podio.request('get', '/user/status', null, function(responseData) { - res.render('user', { profile: responseData.profile }); - }); - } else { + + podio.isAuthenticated() + .then(function() { + return podio.request('get', '/user/status'); + }) + .then(function(responseData) { + res.render('user', { profile: responseData.profile }); + }) + .catch(function(err) { res.send(401); - } + }); }); router.get('/upload', function(req, res) { @@ -56,29 +71,35 @@ router.get('/upload', function(req, res) { router.post('/upload', function(req, res) { var busboy = new Busboy({ headers: req.headers }); - if (!podio.isAuthenticated()) { - res.send(401); - return; - } + podio.isAuthenticated() + .then(function() { - busboy.on('file', function(fieldname, file, filename, encoding, mimetype) { - var dir = temp.mkdirSync(); - var filePath = dir + '/' + filename; + busboy.on('file', function(fieldname, file, filename, encoding, mimetype) { - fs.writeFileSync(filePath, ''); + var dir = temp.mkdirSync(); + var filePath = dir + '/' + filename; - file.on('data', function(data) { - fs.appendFileSync(filePath, data); - }); + fs.writeFileSync(filePath, ''); + + file.on('data', function(data) { + fs.appendFileSync(filePath, data); + }); - file.on('end', function() { - podio.uploadFile(filePath, filename, function(body, response) { - res.render('upload_success', { fileId: body.file_id }) + file.on('end', function() { + podio.uploadFile(filePath, filename) + .then(function(body, response) { + res.render('upload_success', { fileId: body.file_id }) + }) + .catch(function (err) { + res.end(String(err)); + }); }); }); + req.pipe(busboy); + }) + .catch(function () { + res.send(401); }); - - req.pipe(busboy); }); module.exports = router; diff --git a/examples/server_auth/sessionStore.js b/examples/server_auth/sessionStore.js index dc9b865..a3b2587 100644 --- a/examples/server_auth/sessionStore.js +++ b/examples/server_auth/sessionStore.js @@ -4,12 +4,14 @@ var path = require('path'); function get(authType, callback) { var fileName = path.join(__dirname, 'tmp/' + authType + '.json'); var podioOAuth = fs.readFile(fileName, 'utf8', function(err, data) { - if (err) { - if (err.errno !== 2) { // skip file not found errors + + // Throw error, unless it's file-not-found + if (err && err.errno !== 2) { throw new Error('Reading from the sessionStore failed'); - } } else if (data.length > 0) { callback(JSON.parse(data)); + } else { + callback(); } }); } @@ -20,7 +22,7 @@ function set(podioOAuth, authType, callback) { if (/server|client|password/.test(authType) === false) { throw new Error('Invalid authType'); } - + fs.writeFile(fileName, JSON.stringify(podioOAuth), 'utf8', function(err) { if (err) { throw new Error('Writing in the sessionStore failed'); diff --git a/examples/server_auth/tmp/server.json b/examples/server_auth/tmp/server.json old mode 100644 new mode 100755 diff --git a/lib/auth.js b/lib/auth.js index 94e5528..c8dac9e 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -37,8 +37,12 @@ module.exports = { grant_type: 'authorization_code', code: authCode, redirect_uri: redirectURL - }, function(responseData) { - this._onAccessTokenAcquired(responseData, callback); + }, function(err, responseData) { + if (err) { + callback(err); + } else { + this._onAccessTokenAcquired(responseData, callback); + } }.bind(this)); }, @@ -81,8 +85,12 @@ module.exports = { requestData.offering_id = offeringId; } - this._authenticate(requestData, function(responseData) { - this._onAccessTokenAcquired(responseData, callback); + this._authenticate(requestData, function(err, responseData) { + if(err) { + callback(err); + } else { + this._onAccessTokenAcquired(responseData, callback); + } }.bind(this)); }, @@ -96,8 +104,12 @@ module.exports = { requestData.offering_id = offeringId; } - this._authenticate(requestData, function(responseData) { - this._onAccessTokenAcquired(responseData, callback); + this._authenticate(requestData, function(err, responseData) { + if (err) { + callback(err); + } else { + this._onAccessTokenAcquired(responseData, callback); + } }.bind(this)); }, @@ -111,11 +123,9 @@ module.exports = { this._authenticate(requestData, function(err, responseData) { if (err) { callback(err); - - return; + } else { + this._onAccessTokenAcquired(responseData, callback); } - - this._onAccessTokenAcquired(responseData, callback); }.bind(this)); }, @@ -129,11 +139,9 @@ module.exports = { this._authenticate(requestData, function(err, responseData) { if (err) { callback(err); - - return; + } else { + this._onAccessTokenAcquired(responseData, callback); } - - this._onAccessTokenAcquired(responseData, callback); }.bind(this)); }, diff --git a/lib/push.js b/lib/push.js index 0d479b7..77a70b1 100644 --- a/lib/push.js +++ b/lib/push.js @@ -11,11 +11,19 @@ var subscriptionsData = {}; module.exports = { + _getSubscription: function (channel) { + return subscriptionsData[channel]; + }, + + _setSubscription: function (channel, subscription) { + subscriptionsData[channel] = subscription; + }, + // Hook that is run for all outgoing messages: // Attaches signature information to outgoing 'subscribe' messages for authentication purposes _fayeExtensionOutgoing: function(message, callback) { if (message.channel == "/meta/subscribe") { - var subscription = subscriptionsData[message.subscription]; + var subscription = this._getSubscription(message.subscription); if (!message.ext) { message.ext = {}; } @@ -28,10 +36,6 @@ module.exports = { callback(message); }, - _fayeExtensionIncoming: function(message, callback) { - callback(message); - }, - // Returns a reference to the Faye client and initializes it if needed _getFayeClient: function () { @@ -47,8 +51,7 @@ module.exports = { // Add extensions to Faye client this._fayeClient.addExtension({ - outgoing: this._fayeExtensionOutgoing, - incoming: this._fayeExtensionIncoming + outgoing: this._fayeExtensionOutgoing }); } @@ -66,7 +69,7 @@ module.exports = { return self._getAuth().isAuthenticated() .then(function() { - subscriptionsData[options.channel] = options; + self._setSubscription(options.channel, options); // Perform subscription and return Faye Subscription Object return self._getFayeClient().subscribe(options.channel, handler); diff --git a/lib/transport.js b/lib/transport.js index 8914fff..89b3fcf 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -168,9 +168,17 @@ module.exports = { req = request[method](url); if (options && options.basicAuth) { - req = _.compose(this._addRequestData.bind(this, data, method), this._setOptions.bind(this, options || {}))(req); + req = _.compose( + this._addRequestData.bind(this, data, method), + this._setOptions.bind(this, options || {}) + )(req); } else { - req = _.compose(this._addRequestData.bind(this, data, method), this._addHeaders.bind(this), this._addCORS.bind(this), this._setOptions.bind(this, options || {}))(req); + req = _.compose( + this._addRequestData.bind(this, data, method), + this._addHeaders.bind(this), + this._addCORS.bind(this), + this._setOptions.bind(this, options || {}) + )(req); } return new Promise(function(resolve, reject) { diff --git a/test/auth.spec.js b/test/auth.spec.js index 0e598be..35c9aa3 100644 --- a/test/auth.spec.js +++ b/test/auth.spec.js @@ -198,7 +198,7 @@ describe('auth', function() { it('should call onAccessTokenAcquired with correct parameters when authentication succeeds', function() { var responseData = {}; var host = { - _authenticate: sinon.stub().callsArgWith(1, responseData), + _authenticate: sinon.stub().callsArgWith(1, null, responseData), _onAccessTokenAcquired: sinon.stub() }; var username = 'user@podio.com'; diff --git a/test/push.spec.js b/test/push.spec.js index c4f9982..f34ca54 100644 --- a/test/push.spec.js +++ b/test/push.spec.js @@ -12,7 +12,7 @@ describe('push', function() { describe('_getFayeClient', function() { - it('should correctly set and return the client', function(){ + it('should initialize, set and return client, when it hasn\'t yet been set', function(){ var host = { apiURL: 'https://api.podio.com' @@ -24,8 +24,89 @@ describe('push', function() { expect(_.isObject(returnedClient)).toBe(true); expect(returnedClient._endpoint).toEqual('https://push.podio.com/faye'); }); + + it('shouldn\'t re-initialize client. Rather return the existing, when already set', function(){ + + var disableSpy = sinon.spy(); + var clientStub = sinon.stub().returns({ + disable: disableSpy + }); + + var host = { + apiURL: 'https://api.podio.com', + _fayeClient: clientStub + }; + + var returnedClient = pushLib._getFayeClient.call(host); + + expect(_.isObject(host._fayeClient)).toBe(true); + expect(_.isObject(returnedClient)).toBe(true); + expect(returnedClient).toEqual(clientStub); + expect(disableSpy.called).toEqual(false); + }); + }); + + describe('_getSubscription/_setSubscription', function() { + it('should correctly get and set subscriptions', function() { + + var message = { + channel: 'my/test/channel', + timestamp: 1234, + signature: 'myTestSignature' + }; + + pushLib._setSubscription(message.channel, message); + + expect(_.isObject(pushLib._getSubscription(message.channel))).toBe(true); + expect(pushLib._getSubscription(message.channel)).toEqual(message); + }); }); + describe('_fayeExtensionOutgoing', function () { + + it('should attach signature and timestamp to outgoing \'meta/subscribe\' messages', function(){ + + var callbackSpy = sinon.spy(); + + var message = { + channel: '/meta/subscribe', + signature: 'myTestSignature', + timestamp: 123456789 + }; + + var host = { + _getSubscription: sinon.stub().returns(message) + }; + + pushLib._fayeExtensionOutgoing.call(host, message, callbackSpy); + + expect(callbackSpy.calledWithExactly({ + channel: message.channel, + signature: message.signature, + timestamp: message.timestamp, + ext: { + private_pub_signature: message.signature, + private_pub_timestamp: message.timestamp + } + })).toBe(true); + }); + + it('shouldn\'t attach signature info to non-/meta/subscribe messages', function() { + + callbackSpy = sinon.spy(); + + var message = { + channel: 'some/other/channel', + timestamp: 123456789, + signature: 'myTestSignature' + }; + + pushLib._fayeExtensionOutgoing(message, callbackSpy); + + expect(callbackSpy.calledWithExactly(message)); + }); + }) + describe('subscribe', function() { it ('should reject if authentication has not been performed', function(done) { @@ -57,12 +138,13 @@ describe('push', function() { it ('should resolve if authentication has been performed', function() { var subscribeSpy = sinon.spy(); + var setSubscriptionSpy = sinon.spy(); var host = { _getFayeClient: sinon.stub().returns({ subscribe: subscribeSpy }), - + _setSubscription: setSubscriptionSpy, _getAuth: sinon.stub().returns({ isAuthenticated: sinon.stub().returns(Promise.resolve()) }) @@ -82,8 +164,9 @@ describe('push', function() { subscription.then(function() { // Assert that the Faye client is properly subscribed to expect(subscribeSpy.calledWith(options.channel, handler)).toBe(true); + expect(setSubscriptionSpy.called).toBe(true); }).catch(function(err) { - expect('There should not be an error').toBe(false); + expect(String(err)).toBe(null); }); }); });