Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add get long live token and tests #74

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export default class FacebookTokenStrategy extends OAuth2Strategy {
const options = _options || {};
const verify = _verify;
const _fbGraphVersion = options.fbGraphVersion || 'v2.6';


options.authorizationURL = options.authorizationURL || `https://www.facebook.com/${_fbGraphVersion}/dialog/oauth`;
options.tokenURL = options.tokenURL || `https://graph.facebook.com/${_fbGraphVersion}/oauth/access_token`;
Expand All @@ -42,10 +41,14 @@ export default class FacebookTokenStrategy extends OAuth2Strategy {
this._profileFields = options.profileFields || ['id', 'displayName', 'name', 'emails'];
this._profileImage = options.profileImage || {};
this._clientSecret = options.clientSecret;
this._clientID = options.clientID;
this._enableProof = typeof options.enableProof === 'boolean' ? options.enableProof : true;
this._passReqToCallback = options.passReqToCallback;
this._oauth2.useAuthorizationHeaderforGET(false);
this._fbGraphVersion = _fbGraphVersion;
this.tokenURL = options.tokenURL;
this._getLongLivedToken = options.getLongLivedToken || false;
this._storeLongLiveToken = options.storeLongLiveToken || false;
}

/**
Expand All @@ -56,7 +59,7 @@ export default class FacebookTokenStrategy extends OAuth2Strategy {
authenticate(req, options) {
const accessToken = this.lookup(req, this._accessTokenField);
const refreshToken = this.lookup(req, this._refreshTokenField);

const self=this;
if (!accessToken) return this.fail({message: `You should provide ${this._accessTokenField}`});

this._loadUserProfile(accessToken, (error, profile) => {
Expand All @@ -68,11 +71,32 @@ export default class FacebookTokenStrategy extends OAuth2Strategy {

return this.success(user, info);
};
const passCallback=(req,accessToken,refreshToken,profile) => {
if (self._storeLongLiveToken){
accessToken=req.body.longLivedToken
}

if (self._passReqToCallback) {
self._verify(req, accessToken, refreshToken, profile, verified);
} else {
self._verify(accessToken, refreshToken, profile, verified);
}
};
if(self._getLongLivedToken) {
self._getLLT(accessToken, function (err, longLivedToken, expires) {
if(err){
self.error(err);
return;
}

if (this._passReqToCallback) {
this._verify(req, accessToken, refreshToken, profile, verified);
req.body.longLivedToken = longLivedToken;
if(expires !== null){
req.body.longLivedTokenExpires = expires;
}
passCallback(req, accessToken, refreshToken, profile);
});
} else {
this._verify(accessToken, refreshToken, profile, verified);
passCallback(req, accessToken, refreshToken, profile);
}
});
}
Expand Down Expand Up @@ -210,4 +234,50 @@ export default class FacebookTokenStrategy extends OAuth2Strategy {

return profileFields.reduce((acc, field) => acc.concat(map[field] || field), []).join(',');
}
/**
* Attempts to get a Long-Lived Token from Facebook.
* Requires a valid clientID (AppID), clientSecret (AppSecret) and accessToken
*
* @param {String} accessToken
* @param {Function} done
* @api private
*/
_getLLT(accessToken,done){
let url = this.tokenURL + "?" +
"grant_type=fb_exchange_token" + "&" +
"client_id=" + this._clientID + "&" +
"client_secret=" + this._clientSecret + "&" +
"fb_exchange_token=" + accessToken;
url = uri.parse(url);

if (this._enableProof) {
// Secure API call by adding proof of the app secret. This is required when
// the "Require AppSecret Proof for Server API calls" setting has been
// enabled. The proof is a SHA256 hash of the access token, using the app
// secret as the key.
//
// For further details, refer to:
// https://developers.facebook.com/docs/reference/api/securing-graph-api/
const proof = crypto.createHmac('sha256', this._clientSecret).update(accessToken).digest('hex');
url.search = (url.search ? url.search + '&' : '') + 'appsecret_proof=' + encodeURIComponent(proof);
}
url = uri.format(url);
this._oauth2.getProtectedResource(url, accessToken, function (err, body, res) {
if (err) {
return done(new InternalOAuthError('failed to get long-lived token', err)); }
try {
body=JSON.parse(body);
if(typeof body.access_token === "undefined"){
return done(new InternalOAuthError('facebook was unable to provide a long-lived token'));
}
if(typeof body.expires_in === "undefined"){
body.expires_in = null;
}
return done(null,body.access_token,body.expires_in)
} catch(e) {
done(e);
}
});
}

}
8 changes: 8 additions & 0 deletions test/fixtures/longLiveToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Created by andreaboetto on 03/05/17.
*/
export default JSON.stringify({
"access_token":"LONGLIVETOKEN",
"token_type":"bearer",
"expires_in":5182904
});
210 changes: 205 additions & 5 deletions test/unit/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chai, { assert } from 'chai';
import sinon from 'sinon';
import FacebookTokenStrategy from '../../src/index';
import fakeProfile from '../fixtures/profile';
import fakeLongLiveToken from '../fixtures/longLiveToken';

const STRATEGY_CONFIG = {
clientID: '123',
Expand All @@ -25,30 +26,31 @@ describe('FacebookTokenStrategy:init', () => {
it('Should properly throw exception when options is empty', () => {
assert.throw(() => new FacebookTokenStrategy(), Error);
});

it('Should use the default fb graph version when no explicit version is specified', () => {
let strategy = new FacebookTokenStrategy(STRATEGY_CONFIG, BLANK_FUNCTION);
assert.equal(strategy._fbGraphVersion, 'v2.6');
assert.equal(strategy._oauth2._accessTokenUrl,'https://graph.facebook.com/v2.6/oauth/access_token');
assert.equal(strategy._oauth2._authorizeUrl,'https://www.facebook.com/v2.6/dialog/oauth');
assert.equal(strategy._profileURL,'https://graph.facebook.com/v2.6/me');
});

it('Should use the explicit version, if specified', () => {
let strategy = new FacebookTokenStrategy({
clientID: '123',
clientSecret: '123',
fbGraphVersion: 'v2.4'
}, BLANK_FUNCTION);
assert.equal(strategy._fbGraphVersion, 'v2.4');
assert.equal(strategy._fbGraphVersion, 'v2.4');
assert.equal(strategy._oauth2._accessTokenUrl,'https://graph.facebook.com/v2.4/oauth/access_token');
assert.equal(strategy._oauth2._authorizeUrl,'https://www.facebook.com/v2.4/dialog/oauth');
assert.equal(strategy._profileURL,'https://graph.facebook.com/v2.4/me');
assert.equal(strategy._profileURL,'https://graph.facebook.com/v2.4/me');
});

});

describe('FacebookTokenStrategy:authenticate', () => {

describe('Authenticate without passReqToCallback', () => {
let strategy;

Expand Down Expand Up @@ -213,7 +215,204 @@ describe('FacebookTokenStrategy:authenticate', () => {
});
});

describe('Authenticate without passReqToCallback with long live token, not stored', () => {
let strategy;

before(() => {
strategy = new FacebookTokenStrategy({
clientID: '123',
clientSecret: '123',
getLongLivedToken: true
}, (accessToken, refreshToken, profile, next) => {
assert.equal(accessToken, 'access_token');
assert.equal(refreshToken, 'refresh_token');
assert.typeOf(profile, 'object');
assert.typeOf(next, 'function');
return next(null, profile, {info: 'foo'});
});
sinon.stub(strategy._oauth2, 'get', (url, accessToken, next) => next(null, fakeProfile, null));
sinon.stub(strategy, '_getLLT', (accessToken, next) => next(null, fakeProfile, null));
});

after(() => strategy._oauth2.get.restore());
});
describe('Authenticate without passReqToCallback with long live token stored', () => {
let strategy;

before(() => {
strategy = new FacebookTokenStrategy({
clientID: '123',
clientSecret: '123',
getLongLivedToken: true,
storeLongLiveToken:true
}, (accessToken, refreshToken, profile, next) => {
assert.equal(accessToken, 'LONGLIVETOKEN');
assert.equal(refreshToken, 'refresh_token');
assert.typeOf(profile, 'object');
assert.typeOf(next, 'function');
return next(null, profile, {info: 'foo'});
});
sinon.stub(strategy._oauth2, 'get', (url, accessToken, next) => next(null, fakeProfile, null));
sinon.stub(strategy, '_getLLT', (accessToken, next) => next(null, fakeProfile, null));
});

after(() => strategy._oauth2.get.restore());
});

describe('Authenticate with passReqToCallback with long live token not stored', () => {
let strategy;

before(() => {
strategy = new FacebookTokenStrategy({
clientID: '123',
clientSecret: '123',
passReqToCallback: true,
getLongLivedToken: true
}, (req, accessToken, refreshToken, profile, next) => {
assert.typeOf(req, 'object');
assert.equal(accessToken, 'access_token');
assert.equal(req.body.longLivedToken, 'LONGLIVETOKEN');
assert.equal(refreshToken, 'refresh_token');
assert.typeOf(profile, 'object');
assert.typeOf(next, 'function');
return next(null, profile, {info: 'foo'});
});
sinon.stub(strategy._oauth2, 'get', (url, accessToken, next) => next(null, fakeProfile, null));
sinon.stub(strategy._oauth2, 'getProtectedResource', (url, accessToken, next) => next(null, JSON.stringify({
"access_token":"LONGLIVETOKEN",
"token_type":"bearer"
}), null));
});

after(() => {strategy._oauth2.get.restore(); strategy._oauth2.getProtectedResource.restore()});

it('Should properly call _verify with req', done => {
chai
.passport
.use(strategy)
.success((user, info) => {
assert.typeOf(user, 'object');
assert.typeOf(info, 'object');
assert.deepEqual(info, {info: 'foo'});
done();
})
.req(req => {
req.body = {
access_token: 'access_token',
refresh_token: 'refresh_token'
}
})
.authenticate({});
});
});
describe('Authenticate with passReqToCallback with long live token stored', () => {
let strategy;

before(() => {
strategy = new FacebookTokenStrategy({
clientID: '123',
clientSecret: '123',
passReqToCallback: true,
getLongLivedToken: true,
storeLongLiveToken:true
}, (req, accessToken, refreshToken, profile, next) => {
assert.typeOf(req, 'object');
assert.equal(accessToken, 'LONGLIVETOKEN');
assert.equal(req.body.longLivedToken, 'LONGLIVETOKEN');
assert.equal(refreshToken, 'refresh_token');
assert.typeOf(profile, 'object');
assert.typeOf(next, 'function');
return next(null, profile, {info: 'foo'});
});
sinon.stub(strategy._oauth2, 'get', (url, accessToken, next) => next(null, fakeProfile, null));
sinon.stub(strategy._oauth2, 'getProtectedResource', (url, accessToken, next) => next(null, fakeLongLiveToken, null));
});

after(() => {strategy._oauth2.get.restore(); strategy._oauth2.getProtectedResource.restore()});

it('Should properly call _verify with req', done => {
chai
.passport
.use(strategy)
.success((user, info) => {
assert.typeOf(user, 'object');
assert.typeOf(info, 'object');
assert.deepEqual(info, {info: 'foo'});
done();
})
.req(req => {
req.body = {
access_token: 'access_token',
refresh_token: 'refresh_token'
}
})
.authenticate({});
});
});

describe('Failed authentications', () => {
it('Should properly return error on _getLLT', done => {
let strategy = new FacebookTokenStrategy({
clientID: '123',
clientSecret: '123',
getLongLivedToken: true
}, (accessToken, refreshToken, profile, next) => {
assert.equal(accessToken, 'access_token');
assert.equal(refreshToken, 'refresh_token');
assert.typeOf(profile, 'object');
assert.typeOf(next, 'function');
return next(null, profile, {info: 'foo'});
});
sinon.stub(strategy._oauth2, 'getProtectedResource', (url, access_token, next) => next(new Error('Some error occurred'),null,null));

strategy._getLLT('accessToken', (error, longLivedToken, expires) => {
console.log("ERROR",error);
assert.instanceOf(error, Error);
strategy._oauth2.getProtectedResource.restore();
done();
});
});
it('Should properly return error on facebook response invalid json data', done => {
let strategy = new FacebookTokenStrategy({
clientID: '123',
clientSecret: '123',
getLongLivedToken: true
}, (accessToken, refreshToken, profile, next) => {
assert.equal(accessToken, 'access_token');
assert.equal(refreshToken, 'refresh_token');
assert.typeOf(profile, 'object');
assert.typeOf(next, 'function');
return next(null, profile, {info: 'foo'});
});
sinon.stub(strategy._oauth2, 'getProtectedResource', (url, access_token, next) => next(null,"not validJsone",null));

strategy._getLLT('accessToken', (error, longLivedToken, expires) => {
assert.instanceOf(error, Error);
strategy._oauth2.getProtectedResource.restore();
done();
});
});
it('Should properly return error on facebook response invalid json data content', done => {
let strategy = new FacebookTokenStrategy({
clientID: '123',
clientSecret: '123',
getLongLivedToken: true
}, (accessToken, refreshToken, profile, next) => {
assert.equal(accessToken, 'access_token');
assert.equal(refreshToken, 'refresh_token');
assert.typeOf(profile, 'object');
assert.typeOf(next, 'function');
return next(null, profile, {info: 'foo'});
});
sinon.stub(strategy._oauth2, 'getProtectedResource', (url, access_token, next) => next(null,"{}",null));

strategy._getLLT('accessToken', (error, longLivedToken, expires) => {
console.log("ERROR",error);
assert.instanceOf(error, Error);
strategy._oauth2.getProtectedResource.restore();
done();
});
});
it('Should properly return error on loadUserProfile', done => {
let strategy = new FacebookTokenStrategy(STRATEGY_CONFIG, (accessToken, refreshToken, profile, next) => {
assert.equal(accessToken, 'access_token');
Expand Down Expand Up @@ -297,6 +496,7 @@ describe('FacebookTokenStrategy:authenticate', () => {
})
.authenticate({});
});

});
});

Expand Down