From b9b98128f6b81b6825e5fc848305de61df0d53f9 Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Fri, 6 Jun 2014 17:27:09 +0200 Subject: [PATCH 1/9] Added new syntax for .auth --- coffee/lib/authentication.coffee | 81 ++++++++++++++++++++++++-------- coffee/lib/csrf_generator.coffee | 8 ++-- coffee/main.coffee | 12 +++-- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/coffee/lib/authentication.coffee b/coffee/lib/authentication.coffee index f515a18..46ba762 100644 --- a/coffee/lib/authentication.coffee +++ b/coffee/lib/authentication.coffee @@ -2,8 +2,61 @@ request = require 'request' Q = require 'q' module.exports = (cache, requestio) -> - return { - authenticate: (code, req) -> + a = { + refresh_tokens: (credentials, force) -> + defer = Q.defer() + # call refresher here + defer.resolve credentials + # set credentials.refreshed to true if refreshed + return defer.promise + auth: (provider, session, opts) -> + defer = Q.defer() + console.log 'OPTS', opts + if opts?.code + console.log 'Calling Authenticate' + return a.authenticate(opts.code, session) + + if opts?.credentials + console.log 'Rebuilding from credentials' + a.refresh_tokens(opts.credentials, opts?.force_refresh) + .then (credentials) -> + defer.resolve(a.construct_request_object(credentials)) + return defer.promise + + if not opts?.credentials and not opts?.code + console.log 'Getting from session' + if session.oauth[provider] + a.refresh_tokens(session.oauth[provider], opts?.force_refresh) + .then (credentials) -> + defer.resolve(a.construct_request_object(credentials)) + else + defer.reject new Error('Could not authenticate from session') + return defer.promise + + defer.reject new Error('Could not authenticate, parameters are missing or wrong') + return defer.promise + construct_request_object: (credentials) -> + request_object = {} + for k of credentials + request_object[k] = credentials[k] + request_object.get = (url, options) -> + return requestio.make_request(request_object, 'GET', url, options) + request_object.post = (url, options) -> + return requestio.make_request(request_object, 'POST',url, options) + request_object.patch = (url, options) -> + return requestio.make_request(request_object, 'PATCH', url, options) + request_object.put = (url, options) -> + return requestio.make_request(request_object, 'PUT', url, options) + request_object.del = (url, options) -> + return requestio.make_request(request_object, 'DELETE', url, options) + request_object.me = (options) -> + return requestio.make_me_request(request_object, options) + request_object.getCredentials = () -> + return credentials + request_object.wasRefreshed = () -> + return credentials.refreshed + return request_object + authenticate: (code, session) -> defer = Q.defer() request.post { url: cache.oauthd_url + '/auth/access_token', @@ -26,25 +79,15 @@ module.exports = (cache, requestio) -> if (not response.state?) defer.reject new Error 'State is missing from response' return - - if (not req?.session?.csrf_tokens? or response.state not in req.session.csrf_tokens) + if (not session?.csrf_tokens? or response.state not in session.csrf_tokens) defer.reject new Error 'State is not matching' - response.get = (url, options) -> - return requestio.make_request(response, 'GET', url, options) - response.post = (url, options) -> - return requestio.make_request(response, 'POST',url, options) - response.patch = (url, options) -> - return requestio.make_request(response, 'PATCH', url, options) - response.put = (url, options) -> - return requestio.make_request(response, 'PUT', url, options) - response.del = (url, options) -> - return requestio.make_request(response, 'DELETE', url, options) - response.me = (options) -> - return requestio.make_me_request(response, options) - if (req?.session?) - req.session.oauth = req.session.oauth || {} - req.session.oauth[response.provider] = response + response = a.construct_request_object response + if (session?) + session.oauth = session.oauth || {} + session.oauth[response.provider] = response defer.resolve response return defer.promise + } + return a diff --git a/coffee/lib/csrf_generator.coffee b/coffee/lib/csrf_generator.coffee index e7f0b99..e34e433 100644 --- a/coffee/lib/csrf_generator.coffee +++ b/coffee/lib/csrf_generator.coffee @@ -1,7 +1,7 @@ module.exports = (guid) -> - return (req) -> + return (session) -> csrf_token = guid() - req.session.csrf_tokens = req.session.csrf_tokens or [] - req.session.csrf_tokens.push csrf_token - req.session.csrf_tokens.shift() if req.session.csrf_tokens.length > 4 + session.csrf_tokens = session.csrf_tokens or [] + session.csrf_tokens.push csrf_token + session.csrf_tokens.shift() if session.csrf_tokens.length > 4 return csrf_token \ No newline at end of file diff --git a/coffee/main.coffee b/coffee/main.coffee index 583502f..1cf1ccc 100644 --- a/coffee/main.coffee +++ b/coffee/main.coffee @@ -40,20 +40,22 @@ module.exports = -> return cache.public_key getAppSecret: -> return cache.secret_key - getCsrfTokens: (req) -> - return req.session.csrf_tokens + getCsrfTokens: (session) -> + return session.csrf_tokens setOAuthdUrl: (url) -> cache.oauthd_url = url getOAuthdUrl: -> return cache.oauthd_url getVersion: -> package_info.version - generateStateToken: (req) -> - csrf_generator(req) + generateStateToken: (session) -> + csrf_generator(session) initEndpoints: (app) -> endpoints_initializer app - auth: (code, req) -> + auth2: (code, req) -> authentication.authenticate code, req + auth: (provider, session, opts) -> + authentication.auth provider, session, opts create: (req, provider_name) -> response = req?.session?.oauth?[provider_name] if not response? From ff0f4fad054c2e2e3f77480128c8b37513391872 Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Fri, 6 Jun 2014 20:14:49 +0200 Subject: [PATCH 2/9] Updated tests for new auth method --- coffee/lib/authentication.coffee | 10 +- tests/unit/spec/authentication.spec.js | 19 ++- tests/unit/spec/request.spec.js | 175 +++++++++++++++---------- 3 files changed, 120 insertions(+), 84 deletions(-) diff --git a/coffee/lib/authentication.coffee b/coffee/lib/authentication.coffee index 46ba762..026ef35 100644 --- a/coffee/lib/authentication.coffee +++ b/coffee/lib/authentication.coffee @@ -11,26 +11,22 @@ module.exports = (cache, requestio) -> return defer.promise auth: (provider, session, opts) -> defer = Q.defer() - console.log 'OPTS', opts + if opts?.code - console.log 'Calling Authenticate' return a.authenticate(opts.code, session) if opts?.credentials - console.log 'Rebuilding from credentials' a.refresh_tokens(opts.credentials, opts?.force_refresh) .then (credentials) -> defer.resolve(a.construct_request_object(credentials)) return defer.promise - - if not opts?.credentials and not opts?.code - console.log 'Getting from session' + if (not opts?.credentials) and (not opts?.code) if session.oauth[provider] a.refresh_tokens(session.oauth[provider], opts?.force_refresh) .then (credentials) -> defer.resolve(a.construct_request_object(credentials)) else - defer.reject new Error('Could not authenticate from session') + defer.reject new Error('Cannot authenticate from session for provider \'' + provider + '\'') return defer.promise defer.reject new Error('Could not authenticate, parameters are missing or wrong') diff --git a/tests/unit/spec/authentication.spec.js b/tests/unit/spec/authentication.spec.js index 682c488..47d1e30 100644 --- a/tests/unit/spec/authentication.spec.js +++ b/tests/unit/spec/authentication.spec.js @@ -9,7 +9,7 @@ describe("OAuth authentication", function() { it("should be able to send the code to an oauthd instance in exchange for an access token", function(done) { values.OAuth.initialize('somekey', 'somesecret'); - values.OAuth.generateStateToken(values.express_app.req); + values.OAuth.generateStateToken(values.express_app.req.session); var scope = nock('https://oauth.io') .post('/auth/access_token', { code: 'somecode', @@ -24,12 +24,15 @@ describe("OAuth authentication", function() { provider: 'facebook' }); - values.OAuth.auth('somecode', values.express_app.req) + values.OAuth.auth('facebook', values.express_app.req.session, { + code: 'somecode' + }) .then(function(result) { expect(result.access_token).toBe('result_access_token'); done(); }) .fail(function(error) { + console.log(error); expect(error).not.toBeDefined(); done(); }); @@ -37,7 +40,7 @@ describe("OAuth authentication", function() { it("should throw 'State is missing from response' when state is not returned", function(done) { values.OAuth.initialize('somekey', 'somesecret'); - values.OAuth.generateStateToken(values.express_app.req); + values.OAuth.generateStateToken(values.express_app.req.session); var scope = nock('https://oauth.io') .post('/auth/access_token', { code: 'somecode', @@ -51,7 +54,9 @@ describe("OAuth authentication", function() { provider: 'facebook' }); - values.OAuth.auth('somecode', values.express_app.req) + values.OAuth.auth('facebook', values.express_app.req.session, { + code: 'somecode' + }) .then(function(result) { expect(result).not.toBeDefined(); done(); @@ -65,7 +70,7 @@ describe("OAuth authentication", function() { it("should throw 'State is not matching' when state from response is not matching a state in cache", function(done) { values.OAuth.initialize('somekey', 'somesecret'); - values.OAuth.generateStateToken(values.express_app.req); + values.OAuth.generateStateToken(values.express_app.req.session); var scope = nock('https://oauth.io') .post('/auth/access_token', { code: 'somecode', @@ -80,7 +85,9 @@ describe("OAuth authentication", function() { provider: 'facebook' }); - values.OAuth.auth('somecode', values.express_app.req) + values.OAuth.auth('facebook', values.express_app.req.session, { + code: 'somecode' + }) .then(function(result) { expect(result).not.toBeDefined(); done(); diff --git a/tests/unit/spec/request.spec.js b/tests/unit/spec/request.spec.js index 147df8b..2cff4ad 100644 --- a/tests/unit/spec/request.spec.js +++ b/tests/unit/spec/request.spec.js @@ -6,7 +6,7 @@ describe('OAuth requests', function() { values = require('../init_tests')(); values.OAuth.initialize('somekey', 'somesecret'); - values.OAuth.generateStateToken(values.express_app.req); + values.OAuth.generateStateToken(values.express_app.req.session); var scope = nock('https://oauth.io') .post('/auth/access_token', { @@ -22,7 +22,9 @@ describe('OAuth requests', function() { provider: 'facebook' }); - values.OAuth.auth('somecode', values.express_app.req) + values.OAuth.auth('facebook', values.express_app.req.session, { + code: 'somecode' + }) .then(function(result) { expect(result.access_token).toBe('result_access_token'); done(); @@ -33,34 +35,37 @@ describe('OAuth requests', function() { }); }); - it('OAuth.create() should exist', function(done) { - expect(typeof values.OAuth.create).toBe('function'); - done(); - }); - - it('OAuth.create() should return an object with the provider info + the methods get,post, patch, put and delete', function(done) { - expect(typeof values.OAuth.create(values.express_app.req, 'facebook')).toBe('object'); - expect(values.OAuth.create(values.express_app.req, 'facebook').access_token).toBe('result_access_token'); - expect(typeof values.OAuth.create(values.express_app.req, 'facebook').get).toBe('function'); - expect(typeof values.OAuth.create(values.express_app.req, 'facebook').post).toBe('function'); - expect(typeof values.OAuth.create(values.express_app.req, 'facebook').patch).toBe('function'); - expect(typeof values.OAuth.create(values.express_app.req, 'facebook').put).toBe('function'); - expect(typeof values.OAuth.create(values.express_app.req, 'facebook').del).toBe('function'); - expect(typeof values.OAuth.create(values.express_app.req, 'facebook').me).toBe('function'); - done(); + it('OAuth.auth() should callback with a request_object containing the provider info + the methods get, post, patch, put, del, me, wasRefreshed and getCredentials', function(done) { + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + expect(typeof request_object).toBe('object'); + expect(request_object.access_token).toBe('result_access_token'); + expect(typeof request_object.get).toBe('function'); + expect(typeof request_object.post).toBe('function'); + expect(typeof request_object.patch).toBe('function'); + expect(typeof request_object.put).toBe('function'); + expect(typeof request_object.del).toBe('function'); + expect(typeof request_object.me).toBe('function'); + expect(typeof request_object.wasRefreshed).toBe('function'); + expect(typeof request_object.getCredentials).toBe('function'); + done(); + }); }); - it('OAuth.create().get|patch|post|put|del|me() should fail with "Not authenticated for provider \'provider\'" if not authenticated', function(done) { + it('OAuth.auth() with session only should fail with "Not authenticated for provider \'provider\'" if not authenticated', function(done) { values.express_app.req.session.oauth['facebook'] = undefined; - values.OAuth.create(values.express_app.req, 'facebook') - .get('/me') + + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.get('/me'); + }) .fail(function(e) { - expect(e.message).toBe('Not authenticated for provider \'facebook\''); + expect(e.message).toBe('Cannot authenticate from session for provider \'facebook\''); done(); }); }); - it('OAuth.create().get() should call oauth.io to make a GET request to an API endpoint', function(done) { + it('request_object.get() should call oauth.io to make a GET request to an API endpoint', function(done) { var url = '/me'; url = encodeURIComponent(url); if (url[0] !== '/') @@ -75,7 +80,10 @@ describe('OAuth requests', function() { .reply(200, { 'name': 'User Name' }); - values.OAuth.create(values.express_app.req, 'facebook').get('/me') + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.get('/me'); + }) .then(function(r) { expect(r.name).toBe('User Name'); done(); @@ -86,7 +94,7 @@ describe('OAuth requests', function() { }); }); - it('OAuth.create().post() should call oauth.io to make a POST request to an API endpoint', function(done) { + it('request_object.post() should call oauth.io to make a POST request to an API endpoint', function(done) { var url = '/me/feed'; url = encodeURIComponent(url); if (url[0] !== '/') @@ -103,9 +111,12 @@ describe('OAuth requests', function() { .reply(200, { 'id': 'SOMEID' }); - values.OAuth.create(values.express_app.req, 'facebook').post('/me/feed', { - message: "Hello World" - }) + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.post('/me/feed', { + message: "Hello World" + }); + }) .then(function(r) { expect(r.id).toBe('SOMEID'); done(); @@ -116,7 +127,7 @@ describe('OAuth requests', function() { }); }); - it('OAuth.create().put() should call oauth.io to make a PUT request to an API endpoint', function(done) { + it('request_object.put() should call oauth.io to make a PUT request to an API endpoint', function(done) { var url = '/me/feed'; url = encodeURIComponent(url); if (url[0] !== '/') @@ -133,9 +144,12 @@ describe('OAuth requests', function() { .reply(200, { 'id': 'SOMEID' }); - values.OAuth.create(values.express_app.req, 'facebook').put('/me/feed', { - message: "Hello World" - }) + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.put('/me/feed', { + message: "Hello World" + }); + }) .then(function(r) { expect(r.id).toBe('SOMEID'); done(); @@ -146,7 +160,7 @@ describe('OAuth requests', function() { }); }); - it('OAuth.create().patch() should call oauth.io to make a PATCH request to an API endpoint', function(done) { + it('request_object.patch() should call oauth.io to make a PATCH request to an API endpoint', function(done) { var url = '/me/feed'; url = encodeURIComponent(url); if (url[0] !== '/') @@ -164,9 +178,12 @@ describe('OAuth requests', function() { 'id': 'SOMEID' }); - values.OAuth.create(values.express_app.req, 'facebook').patch('/me/feed', { - message: "Hello World" - }) + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.patch('/me/feed', { + message: "Hello World" + }) + }) .then(function(r) { expect(r.id).toBe('SOMEID'); done(); @@ -177,7 +194,7 @@ describe('OAuth requests', function() { }); }); - it('OAuth.create().del() should call oauth.io to make a DELETE request to an API endpoint', function(done) { + it('request_object.del() should call oauth.io to make a DELETE request to an API endpoint', function(done) { var url = '/me/feed'; url = encodeURIComponent(url); if (url[0] !== '/') @@ -195,9 +212,12 @@ describe('OAuth requests', function() { 'id': 'SOMEID' }); - values.OAuth.create(values.express_app.req, 'facebook').del('/me/feed', { - message: "Hello World" - }) + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.del('/me/feed', { + message: "Hello World" + }); + }) .then(function(r) { expect(r.id).toBe('SOMEID'); done(); @@ -208,7 +228,7 @@ describe('OAuth requests', function() { }); }); - it('OAuth.create().me() should call a GET on oauthd/auth/me to get user info', function (done) { + it('request_object.me() should call a GET on oauthd/auth/me to get user info', function(done) { var url = '/auth/facebook/me'; url = encodeURIComponent(url); var scope = nock('https://oauth.io') @@ -223,18 +243,21 @@ describe('OAuth requests', function() { } }); - values.OAuth.create(values.express_app.req, 'facebook').me() - .then(function (r) { - expect(r.firstname).toBe('Archibald'); - done(); - }) - .fail(function (e) { - expect(e).not.toBeDefined(); - done(); - }); + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.me(); + }) + .then(function(r) { + expect(r.firstname).toBe('Archibald'); + done(); + }) + .fail(function(e) { + expect(e).not.toBeDefined(); + done(); + }); }); - it('OAuth.create().me(filter) should call a GET on oauthd/auth/me?filter to get user info', function(done) { + it('request_object.me(filter) should call a GET on oauthd/auth/me?filter to get user info', function(done) { var url = '/auth/facebook/me?' + qs.stringify({ filter: 'firstname,lastname' @@ -252,7 +275,11 @@ describe('OAuth requests', function() { } }); - values.OAuth.create(values.express_app.req, 'facebook').me(['firstname', 'lastname']) + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.me(['firstname', 'lastname']); + + }) .then(function(r) { expect(r.firstname).toBe('Archibald'); expect(r.lastname).toBe('De la Testitude'); @@ -264,7 +291,7 @@ describe('OAuth requests', function() { }); }); - it('OAuth.create().me() should be able to handle an 501 error when a provider\'s me is not implemented', function (done) { + it('request_object.me() should be able to handle an 501 error when a provider\'s me is not implemented', function(done) { var url = '/auth/facebook/me'; url = encodeURIComponent(url); var scope = nock('https://oauth.io') @@ -275,18 +302,21 @@ describe('OAuth requests', function() { .get('/auth/facebook/me') .reply(501, 'Returned provider name does not match asked provider'); - values.OAuth.create(values.express_app.req, 'facebook').me() - .then(function (r) { - expect(r).not.toBeDefined(); - done(); - }) - .fail(function (e) { - expect(e.message).toBe('Returned provider name does not match asked provider'); - done(); - }); + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.me(); + }) + .then(function(r) { + expect(r).not.toBeDefined(); + done(); + }) + .fail(function(e) { + expect(e.message).toBe('Returned provider name does not match asked provider'); + done(); + }); }); - it('OAuth.create().me() should be able to handle any other error with a standard message', function (done) { + it('request_object.me() should be able to handle any other error with a standard message', function(done) { var url = '/auth/facebook/me'; url = encodeURIComponent(url); var scope = nock('https://oauth.io') @@ -297,15 +327,18 @@ describe('OAuth requests', function() { .get('/auth/facebook/me') .reply(500, 'Returned provider name does not match asked provider'); - values.OAuth.create(values.express_app.req, 'facebook').me() - .then(function (r) { - expect(r).not.toBeDefined(); - done(); - }) - .fail(function (e) { - expect(e.message).toBe('An error occured while retrieving the user\'s information'); - done(); - }); + values.OAuth.auth('facebook', values.express_app.req.session) + .then(function(request_object) { + return request_object.me(); + }) + .then(function(r) { + expect(r).not.toBeDefined(); + done(); + }) + .fail(function(e) { + expect(e.message).toBe('An error occured while retrieving the user\'s information'); + done(); + }); }); }); \ No newline at end of file From 5244e199b6871f547c7865f101670aa987babf02 Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Sat, 7 Jun 2014 19:25:35 +0200 Subject: [PATCH 3/9] Added auto token refresh if available --- coffee/lib/authentication.coffee | 48 +++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/coffee/lib/authentication.coffee b/coffee/lib/authentication.coffee index 026ef35..5847610 100644 --- a/coffee/lib/authentication.coffee +++ b/coffee/lib/authentication.coffee @@ -3,11 +3,44 @@ Q = require 'q' module.exports = (cache, requestio) -> a = { - refresh_tokens: (credentials, force) -> + refresh_tokens: (credentials, session, force) -> defer = Q.defer() - # call refresher here - defer.resolve credentials - # set credentials.refreshed to true if refreshed + credentials.refreshed = false + now = new Date() + if credentials.refresh_token and ((credentials.expires and now.getTime() > credentials.expires) or force) + request.post { + url: cache.oauthd_url + '/auth/refresh_token/' + credentials.provider, + form: { + token: credentials.refresh_token, + key: cache.public_key, + secret: cache.secret_key + } + }, (e, r, body) -> + if (e) + defer.reject e + return defer.promise + else + if typeof body is "string" + try + body = JSON.parse body + catch e + defer.reject e + console.log 'BODY', body + if typeof body == "object" and body.access_token and body.expires_in + credentials.expires = new Date().getTime() + body.expires_in * 1000 + for k of body + credentials[k] = body[k] + console.log 'NEW CREDS', credentials + if (session?) + session.oauth = session.oauth || {} + session.oauth[credentials.provider] = credentials + credentials.refreshed = true + credentials.last_refresh = new Date().getTime() + defer.resolve credentials + else + defer.resolve credentials + else + defer.resolve credentials return defer.promise auth: (provider, session, opts) -> defer = Q.defer() @@ -16,13 +49,13 @@ module.exports = (cache, requestio) -> return a.authenticate(opts.code, session) if opts?.credentials - a.refresh_tokens(opts.credentials, opts?.force_refresh) + a.refresh_tokens(opts.credentials, session, opts?.force_refresh) .then (credentials) -> defer.resolve(a.construct_request_object(credentials)) return defer.promise if (not opts?.credentials) and (not opts?.code) if session.oauth[provider] - a.refresh_tokens(session.oauth[provider], opts?.force_refresh) + a.refresh_tokens(session.oauth[provider], session, opts?.force_refresh) .then (credentials) -> defer.resolve(a.construct_request_object(credentials)) else @@ -77,7 +110,8 @@ module.exports = (cache, requestio) -> return if (not session?.csrf_tokens? or response.state not in session.csrf_tokens) defer.reject new Error 'State is not matching' - + if response.expires_in + response.expires = new Date().getTime() + response.expires_in * 1000 response = a.construct_request_object response if (session?) session.oauth = session.oauth || {} From c20dfb89578a1cf35238c5dcdfb57b7fb3b19153 Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Sat, 7 Jun 2014 20:11:31 +0200 Subject: [PATCH 4/9] Added method to manually refresh credentials --- coffee/main.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coffee/main.coffee b/coffee/main.coffee index 1cf1ccc..a03d26e 100644 --- a/coffee/main.coffee +++ b/coffee/main.coffee @@ -56,6 +56,8 @@ module.exports = -> authentication.authenticate code, req auth: (provider, session, opts) -> authentication.auth provider, session, opts + refreshCredentials: (credentials, session) -> + return authentication.refresh_tokens credentials, session, true create: (req, provider_name) -> response = req?.session?.oauth?[provider_name] if not response? From 8b09ac879ee4f7d325722dcca6b104b7fd7e8ec6 Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Sat, 7 Jun 2014 20:51:52 +0200 Subject: [PATCH 5/9] Added tests for token refreshing --- coffee/lib/authentication.coffee | 2 - tests/unit/spec/authentication.spec.js | 53 +++++++++++++++++++ tests/unit/spec/token_refresh.spec.js | 70 ++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 tests/unit/spec/token_refresh.spec.js diff --git a/coffee/lib/authentication.coffee b/coffee/lib/authentication.coffee index 5847610..933a01a 100644 --- a/coffee/lib/authentication.coffee +++ b/coffee/lib/authentication.coffee @@ -25,12 +25,10 @@ module.exports = (cache, requestio) -> body = JSON.parse body catch e defer.reject e - console.log 'BODY', body if typeof body == "object" and body.access_token and body.expires_in credentials.expires = new Date().getTime() + body.expires_in * 1000 for k of body credentials[k] = body[k] - console.log 'NEW CREDS', credentials if (session?) session.oauth = session.oauth || {} session.oauth[credentials.provider] = credentials diff --git a/tests/unit/spec/authentication.spec.js b/tests/unit/spec/authentication.spec.js index 47d1e30..be7c475 100644 --- a/tests/unit/spec/authentication.spec.js +++ b/tests/unit/spec/authentication.spec.js @@ -98,4 +98,57 @@ describe("OAuth authentication", function() { done(); }); }); + + it("should throw 'State is not matching' when state from response is not matching a state in cache", function(done) { + + + values.OAuth.initialize('somekey', 'somesecret'); + values.OAuth.generateStateToken(values.express_app.req.session); + var scope = nock('https://oauth.io') + .post('/auth/access_token', { + code: 'somecode', + key: 'somekey', + secret: 'somesecret' + }) + .reply(200, { + access_token: 'result_access_token', + expires_in: 'someday', + request: {}, + state: 'unique_id', + provider: 'google', + refresh_token: 'the_refresh_token', + expires_in: 1, + token_type: 'Bearer' + }); + var scope2 = nock('https://oauth.io') + .post('/auth/refresh_token/google', { + token: 'the_refresh_token', + key: 'somekey', + secret: 'somesecret' + }) + .reply(200, { + access_token: 'new_access_token', + refresh_token: 'new_refresh_token', + expires_in: 3600, + token_type: 'Bearer' + }); + + values.OAuth.auth('google', values.express_app.req.session, { + code: 'somecode' + }) + .then(function(result) { + expect(result.access_token).toBe('result_access_token'); + return values.OAuth.auth('google', values.express_app.req.session, { + force_refresh: true + }); + }) + .then(function(request_object) { + expect(request_object.wasRefreshed()).toBe(true); + done(); + }) + .fail(function(error) { + expect(error).not.toBeDefined(); + done(); + }); + }); }); \ No newline at end of file diff --git a/tests/unit/spec/token_refresh.spec.js b/tests/unit/spec/token_refresh.spec.js new file mode 100644 index 0000000..eec532e --- /dev/null +++ b/tests/unit/spec/token_refresh.spec.js @@ -0,0 +1,70 @@ +var values = {}; +var qs = require('querystring'); +var nock = require('nock'); +describe('Token refresh', function() { + var credentials = {}; + beforeEach(function(done) { + values = require('../init_tests')(); + values.OAuth.initialize('somekey', 'somesecret'); + + values.OAuth.generateStateToken(values.express_app.req.session); + + var scope = nock('https://oauth.io') + .post('/auth/access_token', { + code: 'somecode', + key: 'somekey', + secret: 'somesecret' + }) + .reply(200, { + access_token: 'result_access_token', + expires_in: 'someday', + request: {}, + state: 'unique_id', + provider: 'google', + refresh_token: 'the_refresh_token', + expires_in: 1, + token_type: 'Bearer' + }); + + values.OAuth.auth('google', values.express_app.req.session, { + code: 'somecode' + }) + .then(function(result) { + credentials = result.getCredentials(); + expect(result.access_token).toBe('result_access_token'); + done(); + }) + .fail(function(error) { + expect(error).not.toBeDefined(); + done(); + }); + }); + + it('OAuth.refreshCredentials() should refresh a set of credentials through oauth.io/auth/refresh_token', function (done) { + var scope = nock('https://oauth.io') + .post('/auth/refresh_token/google', { + token: 'the_refresh_token', + key: 'somekey', + secret: 'somesecret' + }) + .reply(200, { + access_token: 'new_access_token', + refresh_token: 'new_refresh_token', + expires_in: 3600, + token_type: 'Bearer' + }); + + values.OAuth.refreshCredentials(credentials) + .then(function (credentials) { + expect(credentials).toBeDefined(); + expect(credentials.access_token).toBe('new_access_token'); + expect(credentials.refresh_token).toBe('new_refresh_token'); + expect(credentials.expires).toBeGreaterThan(new Date().getTime() + 1000000); + done(); + }) + .fail(function (e) { + expect(true).toBe(false); + done(); + }); + }); +}); \ No newline at end of file From 69b3a6db7e21078c7523e95fdfdfd59f0b21ad81 Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Mon, 9 Jun 2014 14:00:22 +0200 Subject: [PATCH 6/9] Updated README for the new auth method --- README.md | 144 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 118 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index bd967fd..5d87c5e 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Let's say your endpoint will be on /oauth/state_token : ```JavaScript app.get('/oauth/state_token', function (req, res) { - var token = OAuth.generateStateToken(req); + var token = OAuth.generateStateToken(req.session); res.send(200, { token:token @@ -115,7 +115,34 @@ app.get('/oauth/state_token', function (req, res) { }); ``` -**Creating an authentication endpoint** +**Authentication** + +The SDK gives you an `auth` method that allows you to retrieve a `request_object`. That `request_object` allows you to make API calls, and contains the access token. + +The `auth` method takes : + +1. a provider name +2. the session array +3. (optional) an options object + +It returns a promise to let you handle the callback and error management. + +``` +OAuth.auth('provider', req.session, { + option_field: option_value, + //... +}); +``` + +The options object can contain the following fields : + +- code: an OAuth code, that will be used to get credentials from the provider and build a request_object +- credentials: a credentials object, that will be used to rebuild a refreshed request_object +- force_refresh: forces the credentials refresh if a refresh token is available + +If nothing is given in the options object, the auth method tries to build a request_object from the session. + +*Authenticating the user for the first time* When you launch the authentication flow from the front-end (that is to say when you show a popup to the user so that he can allow your app to use his/her data), you'll be given a code (see next section, "Integrating the front-end SDK" to learn how to get the code). @@ -125,26 +152,29 @@ To do that, you have to create an authentication endpoint on your backend. This ```JavaScript app.post('/api/signin', function (req, res) { - OAuth.auth(JSON.parse(req.body).code, req) - .then(function (result) { - //result contains the access_token if OAuth 2.0 - //or the couple oauth_token,oauth_token_secret if OAuth 1.0 + var code = JSON.parse(req.body).code; + + // Here the auth method takes the field 'code' + // in its options object. It will thus use that code + // to retrieve credentials from the provider. + OAuth.auth('facebook', req.session, { + code: code + }) + .then(function (request_object) { + // request_object contains the access_token if OAuth 2.0 + // or the couple oauth_token,oauth_token_secret if OAuth 1.0 - //result also contains methods get|post|patch|put|delete|me - result.get('/me') - .then(function (info) { - var user = { - email: info.email, - firstname: info.first_name, - lastname: info.last_name - }; - //login your user here. - res.send(200, 'Successfully logged in'); - }) - .fail(function (e) { - //handle errors here - res.send(500, 'An error occured'); - }); + // request_object also contains methods get|post|patch|put|delete|me + return request_object.get('/me'); + }) + .then(function (info) { + var user = { + email: info.email, + firstname: info.first_name, + lastname: info.last_name + }; + //login your user here. + res.send(200, 'Successfully logged in'); }) .fail(function (e) { //handle errors here @@ -153,9 +183,9 @@ app.post('/api/signin', function (req, res) { }); ``` -**Use the authentication info in other endpoints** +*Authenticating a user from the session* -Once a user is authenticaded on a service, the auth result object is stored +Once a user is authenticaded on a service, the credentials are stored in the session. You can access it very easily from any other endpoint to use it. Let's say for example that you want to post something on your user's wall on Facebook : ```JavaScript @@ -163,9 +193,11 @@ app.post('/api/wall_message', function (req, res){ var data = JSON.parse(req.body); //data contains field "message", containing the message to post - OAuth.create(req, 'facebook') - .post('/me/feed', { - message: data.message + OAuth.auth('facebook', req.session) + .then(function (request_object) { + return request_object.post('/me/feed', { + message: data.message + }); }) .then(function (r) { //r contains Facebook's response, normaly an id @@ -180,6 +212,66 @@ app.post('/api/wall_message', function (req, res){ }); ``` +*Authenticating a user from saved credentials* + +* Saving credentials +If you want to save the credentials to use them when the user is offline, (e.g. in a cron loading information), you can save the credentials in the data storage of your choice. All you need to do is to retrieve the credentials object from the request_object : + +```javascript +OAuth.auth('provider', req.session, { + code: code +}) + .then(function (request_object) { + var credentials = request_object->getCredentials(); + + // Here you can save the credentials object wherever you want + + }); +``` + +* Using saved credentials + +You can then rebuild a request_object from the credentials you saved earlier : +```javascript +// Here you retrieved the credentials object from your data storage + +OAuth.auth('provider', req.session, { + credentials: credentials +}) + .then(function (request_object) { + // Here request_object has been rebuilt from the credentials object + // If the credentials are expired and contain a refresh token, + // the auth method automatically refresh them. + }); + +``` + +*Refreshing saved credentials* + +Tokens are automatically refreshed when you use the `auth` method with the session or with saved credentials. The SDK checks that the access token is expired whenever it's called. + +If it is, and if a refresh token is available in the credentials (you may have to configure the OAuth.io app in a specific way for some providers), it automatically calls the OAuth.io refresh token endpoint. + +If you want to force a refresh from the `auth` method, you can pass the `force_refresh` field in the option object, like this : + +```javascript +OAuth.auth('provider', req.session, { + force_refresh: true +}); +``` + +You can also refresh a credentials object manually. To do that, call the OAuth.refreshCredentials on the request_object or on a credentials object : + +```javascript +OAuth.refreshCredentials(request_object, req.session) + .then(function (request_object) { + // Here request_object has been refreshed + }) + .fail(function (e) { + // Handle an error + }); +``` + **3. Integrating Front-end SDK** This SDK is available on our website : [Get it on oauth.io][1]. From faa5cb8bd0871df05e769d6cdc56d47967ecd982 Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Mon, 9 Jun 2014 14:36:28 +0200 Subject: [PATCH 7/9] Removed create method --- coffee/main.coffee | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/coffee/main.coffee b/coffee/main.coffee index a03d26e..2b5f553 100644 --- a/coffee/main.coffee +++ b/coffee/main.coffee @@ -13,15 +13,11 @@ cache = { } module.exports = -> - - guid = _guid() csrf_generator = _csrf_generator(guid) requestio = _requestio(cache) authentication = _authentication(cache, requestio) endpoints_initializer = _endpoints_initializer(csrf_generator, cache, authentication) - - return { initialize: (app_public_key, app_secret_key) -> @@ -52,32 +48,9 @@ module.exports = -> csrf_generator(session) initEndpoints: (app) -> endpoints_initializer app - auth2: (code, req) -> - authentication.authenticate code, req auth: (provider, session, opts) -> authentication.auth provider, session, opts refreshCredentials: (credentials, session) -> return authentication.refresh_tokens credentials, session, true - create: (req, provider_name) -> - response = req?.session?.oauth?[provider_name] - if not response? - response = { - error: true - provider: provider_name - } - - response.get = (url) -> - return requestio.make_request(response, 'GET', url) - response.post = (url, options) -> - return requestio.make_request(response, 'POST',url, options) - response.patch = (url, options) -> - return requestio.make_request(response, 'PATCH', url, options) - response.put = (url, options) -> - return requestio.make_request(response, 'PUT', url, options) - response.del = (url, options) -> - return requestio.make_request(response, 'DELETE', url, options) - response.me = (options) -> - return requestio.make_me_request(response, options) - return response } \ No newline at end of file From 016087fdfdeec6d616a11b5f8017ab4ddaff6921 Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Tue, 10 Jun 2014 18:29:24 +0200 Subject: [PATCH 8/9] Updated version to 0.2.0 for future release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3eb580..24bbed8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oauthio", - "version": "0.1.1", + "version": "0.2.0", "description": "OAuth that just works ! This is the Node.js SDK for OAuth.io.", "main": "index.js", "scripts": { From 7cfd26cd101d760e5f423a315bf141298b0257df Mon Sep 17 00:00:00 2001 From: Antoine Jackson Date: Tue, 10 Jun 2014 18:38:36 +0200 Subject: [PATCH 9/9] Added token renewal to features list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5d87c5e..8263f0f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Features - Server-side OAuth authentication flow - Requests to API from the backend, including the unified "me" method - Unified user information requests for available providers from the backend +- Access token renewal with the refresh_token when available With this SDK, your OAuth flow is also more secure as the oauth token never leaves your backend.