diff --git a/README.md b/README.md index bd967fd..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. @@ -107,7 +108,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 +116,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 +153,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 +184,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 +194,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 +213,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]. diff --git a/coffee/lib/authentication.coffee b/coffee/lib/authentication.coffee index f515a18..933a01a 100644 --- a/coffee/lib/authentication.coffee +++ b/coffee/lib/authentication.coffee @@ -2,8 +2,88 @@ request = require 'request' Q = require 'q' module.exports = (cache, requestio) -> - return { - authenticate: (code, req) -> + a = { + refresh_tokens: (credentials, session, force) -> + defer = Q.defer() + 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 + 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] + 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() + + if opts?.code + return a.authenticate(opts.code, session) + + if opts?.credentials + 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], session, opts?.force_refresh) + .then (credentials) -> + defer.resolve(a.construct_request_object(credentials)) + else + 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') + 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 +106,16 @@ 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 + 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 || {} + 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..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) -> @@ -40,40 +36,21 @@ 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) -> - authentication.authenticate code, req - 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 + auth: (provider, session, opts) -> + authentication.auth provider, session, opts + refreshCredentials: (credentials, session) -> + return authentication.refresh_tokens credentials, session, true } \ No newline at end of file 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": { diff --git a/tests/unit/spec/authentication.spec.js b/tests/unit/spec/authentication.spec.js index 682c488..be7c475 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(); @@ -91,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/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 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