Skip to content

Commit

Permalink
adds refresh token exchange fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
rahil-p committed May 27, 2021
1 parent 1fd6652 commit bad4f02
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 24 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const DiscordTokenStrategy = require('passport-discord-token');

passport.use(new DiscordTokenStrategy({
clientID: DISCORD_CLIENT_ID,
clientSecret: DISCORD_CLIENT_SECRET,
}, (accessToken, refreshToken, profile, done) => {
User.findOrCreate({discordId: profile.id}, (error, user) => {
return done(error, user);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "passport-discord-token",
"version": "2.0.0",
"version": "2.1.0",
"description": "Discord access token authentication strategy for Passport",
"keywords": [
"passport",
Expand Down
56 changes: 40 additions & 16 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class DiscordTokenStrategy extends OAuth2Strategy {
* Constructs an instance of the `DiscordTokenStrategy`
* @param {Object} options
* @param {string} options.clientID - client ID of application registered in the Discord Developer Portal
* @param {string} [options.clientSecret] - client secret of application registered in the Discord Developer Portal
* @param {string} [options.accessTokenField='access_token'] - the field in which to look up the access token
* @param {string} [options.refreshTokenField='refresh_token'] the field in which to look up the refresh token
* @param {boolean} [options.checkOAuth2Header] - check for the access token in an OAuth 2.0 `Authorization` header
Expand All @@ -35,6 +36,7 @@ class DiscordTokenStrategy extends OAuth2Strategy {
*/
constructor({
clientID,
clientSecret,
accessTokenField = 'access_token',
refreshTokenField = 'refresh_token',
checkOAuth2Header = true,
Expand Down Expand Up @@ -62,35 +64,57 @@ class DiscordTokenStrategy extends OAuth2Strategy {
}

/**
* Authenticates a request by delegating its provided access token to Discord to retrieve the user profile
* Authenticates a request by delegating its provided access token to Discord to retrieve the user profile; if an
* only a refresh token could be parsed, this method will first attempt to exchange it for an access token
* @param {Object} req = HTTP request object
*/
authenticate(req) {
const accessToken = this.lookup(req, this._accessTokenField)
let accessToken = this.lookup(req, this._accessTokenField)
|| (this._checkOAuth2Header && this.constructor.parseOAuth2Header(req));
const refreshToken = this.lookup(req, this._refreshTokenField);
let refreshToken = this.lookup(req, this._refreshTokenField);

if (!accessToken) {
if (!accessToken && !refreshToken) {
return this.fail({
message: `Access token not found in the ${this._accessTokenField} field`,
message: 'Neither access token nor refresh token could be parsed from the request',
});
}

this._loadUserProfile(accessToken, (error, profile) => {
if (error) return this.error(error);
/* Called once an access token is obtained (either immediately or after refresh token exchange) */
const loadUserProfile = () => {
this._loadUserProfile(accessToken, (error, profile) => {
if (error) return this.error(error);

const done = (err, user, info) => {
if (err) return this.error(err);
if (!user) return this.fail(info);
const done = (err, user, info) => {
if (err) return this.error(err);
if (!user) return this.fail(info);

return this.success(user, info);
};
return this.success(user, info);
};

const args = [accessToken, refreshToken, profile, done];
if (this._passReqToCallback) args.unshift(req);
const args = [accessToken, refreshToken, profile, done];
if (this._passReqToCallback) args.unshift(req);

this._verify(...args);
});
this._verify(...args);
});
};

if (!accessToken && refreshToken) {
/* Exchange the refresh token */
return this._oauth2.getOAuthAccessToken(
refreshToken,
{grant_type: 'refresh_token'},
(error, _accessToken, _refreshToken) => {
if (error) return this.error(error);

accessToken = _accessToken;
refreshToken = _refreshToken;

loadUserProfile();
},
);
}

loadUserProfile();
}

/**
Expand Down
32 changes: 25 additions & 7 deletions test/strategy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const DiscordTokenStrategy = require('..');
const {assert} = chai;
chai.use(chaiPassportStrategy);

const CLIENT_CONFIG = {clientID: 'foo'};
const CLIENT_CONFIG = {clientID: 'foo', clientSecret: 'bar'};
const FAKE_PROFILE = JSON.stringify({
id: '268473310986240001',
username: 'Discord',
Expand Down Expand Up @@ -53,7 +53,7 @@ describe('DiscordTokenStrategy', function discordTokenStrategyTests() {

after(function strategyCleanup() { strategy._oauth2.get.restore(); });

const ChaiPassportTest = (done, reqConfig) => chai.passport
const ChaiPassportParseTest = (done, reqConfig) => chai.passport
.use(strategy)
.success((user, info) => {
assert.typeOf(user, 'object');
Expand All @@ -63,7 +63,7 @@ describe('DiscordTokenStrategy', function discordTokenStrategyTests() {
}).req(reqConfig).authenticate({});

it('Should properly parse `access_token` from body', function bodyParseTest(done) {
ChaiPassportTest(done, (req) => {
ChaiPassportParseTest(done, (req) => {
req.body = {
access_token: 'access_token',
refresh_token: 'refresh_token',
Expand All @@ -72,7 +72,7 @@ describe('DiscordTokenStrategy', function discordTokenStrategyTests() {
});

it('Should properly parse `access_token` from query parameters', function queryParseTest(done) {
ChaiPassportTest(done, (req) => {
ChaiPassportParseTest(done, (req) => {
req.query = {
access_token: 'access_token',
refresh_token: 'refresh_token',
Expand All @@ -81,7 +81,7 @@ describe('DiscordTokenStrategy', function discordTokenStrategyTests() {
});

it('Should properly parse access token from OAuth2 bearer header', function headerTest1(done) {
ChaiPassportTest(done, (req) => {
ChaiPassportParseTest(done, (req) => {
req.headers = {
Authorization: 'Bearer access_token',
refresh_token: 'refresh_token',
Expand All @@ -90,21 +90,39 @@ describe('DiscordTokenStrategy', function discordTokenStrategyTests() {
});

it('Should properly parse access token from OAuth2 bearer header (lowercase)', function headerTest2(done) {
ChaiPassportTest(done, (req) => {
ChaiPassportParseTest(done, (req) => {
req.headers = {
authorization: 'Bearer access_token',
refresh_token: 'refresh_token',
};
});
});

it('Should call fail if access token is not provided', function failTest(done) {
it('Should call fail if neither access token nor refresh token is provided', function failTest(done) {
chai.passport.use(strategy).fail((error) => {
assert.typeOf(error, 'object');
assert.typeOf(error.message, 'string');
assert.valueOf(error.message,
'Neither access token nor refresh token could be parsed from the request');
done();
}).authenticate({});
});

it('Should call error if only a refresh token is provided (with invalid client credentials)',
function refreshTest(done) {
chai.passport.use(strategy).error((error) => {
assert.typeOf(error, 'object');
assert.typeOf(error.statusCode, 'number');
assert.typeOf(error.data, 'string');
assert.valueOf(error.statusCode, 400);
assert.valueOf(error.data, '{"client_id": ["Value \\"foo\\" is not snowflake."]}');
done();
}).req((req) => {
req.body = {
refresh_token: 'refresh_token',
};
}).authenticate({});
});
});

describe('Authenticate with `passReqToCallback`', function authPassReqToCallbackTest() {
Expand Down

0 comments on commit bad4f02

Please sign in to comment.