diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..05642c5
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+github: jaredhanson
+patreon: jaredhanson
+ko_fi: jaredhanson
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..90ab29f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,19 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.6.1] - 2021-09-24
+### Fixed
+- Error in cases where the authorization server returns a successful access
+token response which is missing an `access_token` parameter.
+
+## [1.6.0] - 2021-07-01
+### Added
+
+- Support for `store: true` option to `Strategy` constructor, which initializes
+a state store capable of storing application-level state.
+- Support for `state` object passed as option to `authenticate`, which will be
+persisted in the session by state store.
+- `callbackURL` property added to metadata passed to state store.
diff --git a/README.md b/README.md
index b0cac30..9812fba 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,16 @@ that is not already supported are encouraged to sub-class this strategy. If you
choose to open source the new provider-specific strategy, please add it to the
list so other people can find it.
+---
+
+
+ Advertisement
+
+ Learn OAuth 2.0 - Get started as an API Security Expert
Just imagine what could happen to YOUR professional career if you had skills in OAuth > 8500 satisfied students
+
+
+---
+
[](https://www.npmjs.com/package/passport-oauth2)
[](https://travis-ci.org/jaredhanson/passport-oauth2)
[](https://coveralls.io/github/jaredhanson/passport-oauth2)
@@ -103,23 +113,10 @@ $ make test-cov
$ make view-cov
```
-## Support
-
-#### Funding
-
-This software is provided to you as open source, free of charge. The time and
-effort to develop and maintain this project is dedicated by [@jaredhanson](https://github.com/jaredhanson).
-If you (or your employer) benefit from this project, please consider a financial
-contribution. Your contribution helps continue the efforts that produce this
-and other open source software.
-
-Funds are accepted via [PayPal](https://paypal.me/jaredhanson), [Venmo](https://venmo.com/jaredhanson),
-and [other](http://jaredhanson.net/pay) methods. Any amount is appreciated.
-
## License
[The MIT License](http://opensource.org/licenses/MIT)
Copyright (c) 2011-2016 Jared Hanson <[http://jaredhanson.net/](http://jaredhanson.net/)>
-
+
diff --git a/lib/state/pkcesession.js b/lib/state/pkcesession.js
index 0864014..69ab443 100644
--- a/lib/state/pkcesession.js
+++ b/lib/state/pkcesession.js
@@ -39,13 +39,14 @@ PKCESessionStore.prototype.store = function(req, verifier, state, meta, callback
if (!req.session) { return callback(new Error('OAuth 2.0 authentication requires session support when using state. Did you forget to use express-session middleware?')); }
var key = this._key;
- var state = {
+ var sstate = {
handle: uid(24),
code_verifier: verifier
};
+ if (state) { sstate.state = state; }
if (!req.session[key]) { req.session[key] = {}; }
- req.session[key].state = state;
- callback(null, state.handle);
+ req.session[key].state = sstate;
+ callback(null, sstate.handle);
};
/**
@@ -81,7 +82,7 @@ PKCESessionStore.prototype.verify = function(req, providedState, callback) {
return callback(null, false, { message: 'Invalid authorization request state.' });
}
- return callback(null, state.code_verifier);
+ return callback(null, state.code_verifier, state.state);
};
// Expose constructor.
diff --git a/lib/state/store.js b/lib/state/store.js
new file mode 100644
index 0000000..a49e6e5
--- /dev/null
+++ b/lib/state/store.js
@@ -0,0 +1,88 @@
+var uid = require('uid2');
+
+/**
+ * Creates an instance of `SessionStore`.
+ *
+ * This is the state store implementation for the OAuth2Strategy used when
+ * the `state` option is enabled. It generates a random state and stores it in
+ * `req.session` and verifies it when the service provider redirects the user
+ * back to the application.
+ *
+ * This state store requires session support. If no session exists, an error
+ * will be thrown.
+ *
+ * Options:
+ *
+ * - `key` The key in the session under which to store the state
+ *
+ * @constructor
+ * @param {Object} options
+ * @api public
+ */
+function SessionStore(options) {
+ if (!options.key) { throw new TypeError('Session-based state store requires a session key'); }
+ this._key = options.key;
+}
+
+/**
+ * Store request state.
+ *
+ * This implementation simply generates a random string and stores the value in
+ * the session, where it will be used for verification when the user is
+ * redirected back to the application.
+ *
+ * @param {Object} req
+ * @param {Function} callback
+ * @api protected
+ */
+SessionStore.prototype.store = function(req, state, meta, callback) {
+ if (!req.session) { return callback(new Error('OAuth 2.0 authentication requires session support when using state. Did you forget to use express-session middleware?')); }
+
+ var key = this._key;
+ var sstate = {
+ handle: uid(24)
+ };
+ if (state) { sstate.state = state; }
+ if (!req.session[key]) { req.session[key] = {}; }
+ req.session[key].state = sstate;
+ callback(null, sstate.handle);
+};
+
+/**
+ * Verify request state.
+ *
+ * This implementation simply compares the state parameter in the request to the
+ * value generated earlier and stored in the session.
+ *
+ * @param {Object} req
+ * @param {String} providedState
+ * @param {Function} callback
+ * @api protected
+ */
+SessionStore.prototype.verify = function(req, providedState, callback) {
+ if (!req.session) { return callback(new Error('OAuth 2.0 authentication requires session support when using state. Did you forget to use express-session middleware?')); }
+
+ var key = this._key;
+ if (!req.session[key]) {
+ return callback(null, false, { message: 'Unable to verify authorization request state.' });
+ }
+
+ var state = req.session[key].state;
+ if (!state) {
+ return callback(null, false, { message: 'Unable to verify authorization request state.' });
+ }
+
+ delete req.session[key].state;
+ if (Object.keys(req.session[key]).length === 0) {
+ delete req.session[key];
+ }
+
+ if (state.handle !== providedState) {
+ return callback(null, false, { message: 'Invalid authorization request state.' });
+ }
+
+ return callback(null, true, state.state);
+};
+
+// Expose constructor.
+module.exports = SessionStore;
diff --git a/lib/strategy.js b/lib/strategy.js
index 8ac16e4..b05aacc 100644
--- a/lib/strategy.js
+++ b/lib/strategy.js
@@ -7,9 +7,10 @@ var passport = require('passport-strategy')
, util = require('util')
, utils = require('./utils')
, OAuth2 = require('oauth').OAuth2
- , NullStateStore = require('./state/null')
- , SessionStateStore = require('./state/session')
- , PKCESessionStateStore = require('./state/pkcesession')
+ , NullStore = require('./state/null')
+ , NonceStore = require('./state/session')
+ , StateStore = require('./state/store')
+ , PKCEStateStore = require('./state/pkcesession')
, AuthorizationError = require('./errors/authorizationerror')
, TokenError = require('./errors/tokenerror')
, InternalOAuthError = require('./errors/internaloautherror');
@@ -101,15 +102,15 @@ function OAuth2Strategy(options, verify) {
this._pkceMethod = (options.pkce === true) ? 'S256' : options.pkce;
this._key = options.sessionKey || ('oauth2:' + url.parse(options.authorizationURL).hostname);
- if (options.store) {
+ if (options.store && typeof options.store == 'object') {
this._stateStore = options.store;
+ } else if (options.store) {
+ this._stateStore = options.pkce ? new PKCEStateStore({ key: this._key }) : new StateStore({ key: this._key });
+ } else if (options.state) {
+ this._stateStore = options.pkce ? new PKCEStateStore({ key: this._key }) : new NonceStore({ key: this._key });
} else {
- if (options.state) {
- this._stateStore = options.pkce ? new PKCESessionStateStore({ key: this._key }) : new SessionStateStore({ key: this._key });
- } else {
- if (options.pkce) { throw new TypeError('OAuth2Strategy requires `state: true` option when PKCE is enabled'); }
- this._stateStore = new NullStateStore();
- }
+ if (options.pkce) { throw new TypeError('OAuth2Strategy requires `state: true` option when PKCE is enabled'); }
+ this._stateStore = new NullStore();
}
this._trustProxy = options.proxy;
this._passReqToCallback = options.passReqToCallback;
@@ -151,7 +152,8 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
var meta = {
authorizationURL: this._oauth2._authorizeUrl,
tokenURL: this._oauth2._accessTokenUrl,
- clientID: this._oauth2._clientId
+ clientID: this._oauth2._clientId,
+ callbackURL: callbackURL
}
if (req.query && req.query.code) {
@@ -173,6 +175,7 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
self._oauth2.getOAuthAccessToken(code, params,
function(err, accessToken, refreshToken, params) {
if (err) { return self.error(self._createOAuthError('Failed to obtain access token', err)); }
+ if (!accessToken) { return self.error(new Error('Failed to obtain access token')); }
self._loadUserProfile(accessToken, function(err, profile) {
if (err) { return self.error(err); }
@@ -250,7 +253,17 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
}
var state = options.state;
- if (state) {
+ if (state && typeof state == 'string') {
+ // NOTE: In passport-oauth2@1.5.0 and earlier, `state` could be passed as
+ // an object. However, it would result in an empty string being
+ // serialized as the value of the query parameter by `url.format()`,
+ // effectively ignoring the option. This implies that `state` was
+ // only functional when passed as a string value.
+ //
+ // This fact is taken advantage of here to fall into the `else`
+ // branch below when `state` is passed as an object. In that case
+ // the state will be automatically managed and persisted by the
+ // state store.
params.state = state;
var parsed = url.parse(this._oauth2._authorizeUrl, true);
@@ -275,7 +288,9 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
try {
var arity = this._stateStore.store.length;
if (arity == 5) {
- this._stateStore.store(req, verifier, undefined, meta, stored);
+ this._stateStore.store(req, verifier, state, meta, stored);
+ } else if (arity == 4) {
+ this._stateStore.store(req, state, meta, stored);
} else if (arity == 3) {
this._stateStore.store(req, meta, stored);
} else { // arity == 2
diff --git a/package.json b/package.json
index c8a939c..182007f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "passport-oauth2",
- "version": "1.5.0",
+ "version": "1.6.1",
"description": "OAuth 2.0 authentication strategy for Passport.",
"keywords": [
"passport",
@@ -24,6 +24,10 @@
"bugs": {
"url": "http://github.com/jaredhanson/passport-oauth2/issues"
},
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ },
"license": "MIT",
"licenses": [
{
diff --git a/test/oauth2.pkce.test.js b/test/oauth2.pkce.test.js
index 845fe48..90a5f47 100644
--- a/test/oauth2.pkce.test.js
+++ b/test/oauth2.pkce.test.js
@@ -89,6 +89,109 @@ describe('OAuth2Strategy', function() {
expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ expect(request.session['oauth2:www.example.com'].state.state).to.be.undefined;
+ });
+ });
+
+ describe('handling a request to be redirected for authorization with state', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: { returnTo: '/somewhere' }});
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM')
+ expect(u.query.code_challenge_method).to.equal('S256');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ expect(request.session['oauth2:www.example.com'].state.state).to.deep.equal({ returnTo: '/somewhere' });
+ });
+ });
+
+ describe('handling a request to be redirected for authorization with state set to boolean true', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: true });
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM')
+ expect(u.query.code_challenge_method).to.equal('S256');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ expect(request.session['oauth2:www.example.com'].state.state).to.equal(true);
+ });
+ });
+
+ describe('handling a request to be redirected for authorization with state set to boolean false', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: false });
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM')
+ expect(u.query.code_challenge_method).to.equal('S256');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ expect(request.session['oauth2:www.example.com'].state.state).to.be.undefined;
});
});
@@ -161,6 +264,48 @@ describe('OAuth2Strategy', function() {
it('should supply info', function() {
expect(info).to.be.an.object;
expect(info.message).to.equal('Hello');
+ expect(info.state).to.be.undefined;
+ });
+
+ it('should remove state with verifier from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ });
+
+ describe('processing response to authorization request with state', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK', code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk', state: { returnTo: '/somewhere' } };
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ expect(info.state).to.deep.equal({ returnTo: '/somewhere' });
});
it('should remove state with verifier from session', function() {
diff --git a/test/oauth2.state.custom.test.js b/test/oauth2.state.custom.test.js
index dea5b9b..0c6defe 100644
--- a/test/oauth2.state.custom.test.js
+++ b/test/oauth2.state.custom.test.js
@@ -6,6 +6,7 @@ var OAuth2Strategy = require('../lib/strategy')
, uri = require('url');
+ // TODO: renam ethis file to oauth2.store.custom
describe('OAuth2Strategy', function() {
describe('with custom state store that accepts meta argument', function() {
diff --git a/test/oauth2.state.session.test.js b/test/oauth2.state.session.test.js
index 93f5865..4e69787 100644
--- a/test/oauth2.state.session.test.js
+++ b/test/oauth2.state.session.test.js
@@ -6,6 +6,7 @@ var OAuth2Strategy = require('../lib/strategy')
, uri = require('url');
+ // TODO: rename this file to oauth2.state.nonce
describe('OAuth2Strategy', function() {
describe('using default session state store', function() {
diff --git a/test/oauth2.store.pkce.test.js b/test/oauth2.store.pkce.test.js
new file mode 100644
index 0000000..8fb6bf4
--- /dev/null
+++ b/test/oauth2.store.pkce.test.js
@@ -0,0 +1,889 @@
+var chai = require('chai')
+ , uri = require('url')
+ , OAuth2Strategy = require('../lib/strategy');
+
+
+describe('OAuth2Strategy', function() {
+
+ describe('with store and PKCE true transformation method', function() {
+ var mockCrypto = {
+ pseudoRandomBytes: function(len) {
+ if (len !== 32) { throw new Error('xyz'); }
+ // https://tools.ietf.org/html/rfc7636#appendix-B
+ return new Buffer(
+ [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
+ 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
+ 132, 141, 121]
+ );
+ }
+ }
+
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: true
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+ strategy._oauth2.getOAuthAccessToken = function(code, options, callback) {
+ if (code !== 'SplxlOBeZQQYbYS6WxSbIA') { return callback(new Error('incorrect code argument')); }
+ if (options.grant_type !== 'authorization_code') { return callback(new Error('incorrect options.grant_type argument')); }
+ if (options.redirect_uri !== 'https://www.example.net/auth/example/callback') { return callback(new Error('incorrect options.redirect_uri argument')); }
+ if (options.code_verifier !== 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk') { return callback(new Error('incorrect options.verifier loaded from session')); }
+
+ return callback(null, '2YotnFZFEjr1zCsicMWpAA', 'tGzv3JOkF0XG5Qx2TlKWIA', { token_type: 'example' });
+ }
+
+ describe('handling a request to be redirected for authorization', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM')
+ expect(u.query.code_challenge_method).to.equal('S256');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ expect(request.session['oauth2:www.example.com'].state.state).to.be.undefined;
+ });
+ });
+
+ describe('handling a request to be redirected for authorization with state', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: { returnTo: '/somewhere' }});
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM')
+ expect(u.query.code_challenge_method).to.equal('S256');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ expect(request.session['oauth2:www.example.com'].state.state).to.deep.equal({ returnTo: '/somewhere' });
+ });
+ });
+
+ describe('handling a request to be redirected for authorization with state as boolean true', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: true });
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM')
+ expect(u.query.code_challenge_method).to.equal('S256');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ expect(request.session['oauth2:www.example.com'].state.state).to.equal(true);
+ });
+ });
+
+ describe('handling a request to be redirected for authorization with state as boolean false', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: false });
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM')
+ expect(u.query.code_challenge_method).to.equal('S256');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ expect(request.session['oauth2:www.example.com'].state.state).to.be.undefined;
+ });
+ });
+
+ describe('that redirects to service provider with other data in session', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com'].foo = 'bar';
+ })
+ .authenticate();
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ });
+
+ it('should save state in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ });
+
+ it('should preserve other data in session', function() {
+ expect(request.session['oauth2:www.example.com'].foo).to.equal('bar');
+ });
+ }); // that redirects to service provider with other data in session
+
+ describe('processing response to authorization request', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK', code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' };
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ expect(info.state).to.be.undefined;
+ });
+
+ it('should remove state with verifier from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ });
+
+ describe('processing response to authorization request with state', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK', code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk', state: { returnTo: '/somewhere' } };
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ expect(info.state).to.deep.equal({ returnTo: '/somewhere' });
+ });
+
+ it('should remove state with verifier from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ });
+
+ describe('that was approved with other data in the session', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK', code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' };
+ req.session['oauth2:www.example.com'].foo = 'bar';
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ });
+
+ it('should preserve other data from session', function() {
+ expect(request.session['oauth2:www.example.com'].state).to.be.undefined;
+ expect(request.session['oauth2:www.example.com'].foo).to.equal('bar');
+ });
+ }); // that was approved with other data in the session
+
+ describe('that errors due to lack of session support in app', function() {
+ var request, err;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .error(function(e) {
+ err = e;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ })
+ .authenticate();
+ });
+
+ it('should error', function() {
+ expect(err).to.be.an.instanceof(Error)
+ expect(err.message).to.equal('OAuth 2.0 authentication requires session support when using state. Did you forget to use express-session middleware?');
+ });
+ }); // that errors due to lack of session support in app
+ });
+
+ describe('with store and PKCE plain transformation method', function() {
+ var mockCrypto = {
+ pseudoRandomBytes: function(len) {
+ if (len !== 32) { throw new Error('xyz'); }
+ return new Buffer(
+ [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
+ 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
+ 132, 141, 121]
+ );
+ }
+ }
+
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: 'plain'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+ strategy._oauth2.getOAuthAccessToken = function(code, options, callback) {
+ if (code !== 'SplxlOBeZQQYbYS6WxSbIA') { return callback(new Error('incorrect code argument')); }
+ if (options.grant_type !== 'authorization_code') { return callback(new Error('incorrect options.grant_type argument')); }
+ if (options.redirect_uri !== 'https://www.example.net/auth/example/callback') { return callback(new Error('incorrect options.redirect_uri argument')); }
+ if (options.code_verifier !== 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk') { return callback(new Error('incorrect options.verifier loaded from session')); }
+
+ return callback(null, '2YotnFZFEjr1zCsicMWpAA', 'tGzv3JOkF0XG5Qx2TlKWIA', { token_type: 'example' });
+ }
+
+ describe('handling a request to be redirected for authorization', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk')
+ expect(u.query.code_challenge_method).to.equal('plain');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ });
+ });
+
+ describe('processing response to authorization request', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK', code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' };
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ });
+
+ it('should remove state with verifier from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ });
+ });
+
+ describe('with store and PKCE S256 transformation method', function() {
+ var mockCrypto = {
+ pseudoRandomBytes: function(len) {
+ if (len !== 32) { throw new Error('xyz'); }
+ // https://tools.ietf.org/html/rfc7636#appendix-B
+ return new Buffer(
+ [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
+ 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
+ 132, 141, 121]
+ );
+ }
+ }
+
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: 'S256'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+ strategy._oauth2.getOAuthAccessToken = function(code, options, callback) {
+ if (code !== 'SplxlOBeZQQYbYS6WxSbIA') { return callback(new Error('incorrect code argument')); }
+ if (options.grant_type !== 'authorization_code') { return callback(new Error('incorrect options.grant_type argument')); }
+ if (options.redirect_uri !== 'https://www.example.net/auth/example/callback') { return callback(new Error('incorrect options.redirect_uri argument')); }
+ if (options.code_verifier !== 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk') { return callback(new Error('incorrect options.verifier loaded from session')); }
+
+ return callback(null, '2YotnFZFEjr1zCsicMWpAA', 'tGzv3JOkF0XG5Qx2TlKWIA', { token_type: 'example' });
+ }
+
+ describe('handling a request to be redirected for authorization', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ expect(u.query.code_challenge).to.have.length(43);
+ expect(u.query.code_challenge).to.equal('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM')
+ expect(u.query.code_challenge_method).to.equal('S256');
+ });
+
+ it('should save verifier in session', function() {
+ var u = uri.parse(url, true);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.have.length(43);
+ expect(request.session['oauth2:www.example.com'].state.code_verifier).to.equal('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
+ });
+ });
+
+ describe('processing response to authorization request', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK', code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' };
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ });
+
+ it('should remove state with verifier from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ });
+ });
+
+ describe('with exceptions', function() {
+ var mockCrypto = {
+ pseudoRandomBytes: function(len) {
+ if (len !== 32) { throw new Error('xyz'); }
+ // https://tools.ietf.org/html/rfc7636#appendix-B
+ return new Buffer(
+ [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
+ 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
+ 132, 141, 121]
+ );
+ }
+ }
+
+ describe('with store and unknown encoding method', function() {
+
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: 'unknown'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+ var err;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .error(function(e) {
+ err = e;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should error', function() {
+ expect(err.message).to.equal('Unsupported code verifier transformation method: unknown');
+ });
+ });
+
+ describe('with store and unknown verifier', function() {
+
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: 'S256'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+ var err;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .fail(function(i) {
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK'};
+ })
+ .authenticate();
+ });
+
+ it('should not supply info', function() {
+ expect(info).to.be.undefined;
+ });
+ });
+
+ describe('store and that fails due to state being invalid', function() {
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: 'S256'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+
+ var request
+ , info, status;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .fail(function(i, s) {
+ info = i;
+ status = s;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK-WRONG';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK', code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' };
+ })
+ .authenticate();
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Invalid authorization request state.');
+ });
+
+ it('should supply status', function() {
+ expect(status).to.equal(403);
+ });
+
+ it('should remove state from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ }); // that fails due to state being invalid
+
+ describe('store and that fails due to provider-specific state not found in session', function() {
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: 'S256'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+
+ var request
+ , info, status;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .fail(function(i, s) {
+ info = i;
+ status = s;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Unable to verify authorization request state.');
+ });
+
+ it('should supply status', function() {
+ expect(status).to.equal(403);
+ });
+ }); // that fails due to state not found in session
+
+ describe('store and that fails due to provider-specific state lacking state value', function() {
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: 'S256'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+
+ var request
+ , info, status;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .fail(function(i, s) {
+ info = i;
+ status = s;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ })
+ .authenticate();
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Unable to verify authorization request state.');
+ });
+
+ it('should supply status', function() {
+ expect(status).to.equal(403);
+ });
+ }); // that fails due to provider-specific state lacking state value
+
+ describe('store and that errors due to lack of session support in app', function() {
+ var OAuth2Strategy = require('proxyquire')('../lib/strategy', { crypto: mockCrypto });
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ pkce: 'S256'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken == '2YotnFZFEjr1zCsicMWpAA' && refreshToken == 'tGzv3JOkF0XG5Qx2TlKWIA') {
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ }
+ return done(null, false);
+ });
+
+
+ var request
+ , err;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .error(function(e) {
+ err = e;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ })
+ .authenticate();
+ });
+
+ it('should error', function() {
+ expect(err).to.be.an.instanceof(Error)
+ expect(err.message).to.equal('OAuth 2.0 authentication requires session support when using state. Did you forget to use express-session middleware?');
+ });
+ }); // that errors due to lack of session support in app
+
+ });
+});
diff --git a/test/oauth2.store.test.js b/test/oauth2.store.test.js
new file mode 100644
index 0000000..c23743e
--- /dev/null
+++ b/test/oauth2.store.test.js
@@ -0,0 +1,642 @@
+var OAuth2Strategy = require('../lib/strategy')
+ , AuthorizationError = require('../lib/errors/authorizationerror')
+ , TokenError = require('../lib/errors/tokenerror')
+ , InternalOAuthError = require('../lib/errors/internaloautherror')
+ , chai = require('chai')
+ , uri = require('url');
+
+
+ // TODO: rename this file to oauth2.state.nonce
+describe('OAuth2Strategy', function() {
+
+ describe('using default session state store through store option', function() {
+
+ describe('issuing authorization request', function() {
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true
+ },
+ function(accessToken, refreshToken, profile, done) {});
+
+
+ describe('that redirects to service provider', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ });
+
+ it('should save state in session', function() {
+ var u = uri.parse(url, true);
+
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.state).to.be.undefined;
+ });
+ }); // that redirects to service provider
+
+ describe('that redirects to service provider with state', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: { returnTo: '/somewhere' } });
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ });
+
+ it('should save state in session', function() {
+ var u = uri.parse(url, true);
+
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.state).to.deep.equal({ returnTo: '/somewhere' });
+ });
+ }); // that redirects to service provider with state
+
+ describe('that redirects to service provider with state set to boolean true', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: true });
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ });
+
+ it('should save state in session', function() {
+ var u = uri.parse(url, true);
+
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.state).to.equal(true);
+ });
+ }); // that redirects to service provider with state set to boolean true
+
+ describe('that redirects to service provider with state set to boolean false', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate({ state: false });
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ });
+
+ it('should save state in session', function() {
+ var u = uri.parse(url, true);
+
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.state).to.be.undefined;
+ });
+ }); // that redirects to service provider with state set to boolean false
+
+ describe('that redirects to service provider with other data in session', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com'].foo = 'bar';
+ })
+ .authenticate();
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ });
+
+ it('should save state in session', function() {
+ var u = uri.parse(url, true);
+
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:www.example.com'].state.state).to.be.undefined;
+ });
+
+ it('should preserve other data in session', function() {
+ expect(request.session['oauth2:www.example.com'].foo).to.equal('bar');
+ });
+ }); // that redirects to service provider with other data in session
+
+ describe('that errors due to lack of session support in app', function() {
+ var request, err;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .error(function(e) {
+ err = e;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ })
+ .authenticate();
+ });
+
+ it('should error', function() {
+ expect(err).to.be.an.instanceof(Error)
+ expect(err.message).to.equal('OAuth 2.0 authentication requires session support when using state. Did you forget to use express-session middleware?');
+ });
+ }); // that errors due to lack of session support in app
+
+ }); // issuing authorization request
+
+ describe('issuing authorization request to authorization server using authorization endpoint that has query parameters including state', function() {
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize?foo=bar&state=baz',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true
+ },
+ function(accessToken, refreshToken, profile, done) {});
+
+
+ describe('that redirects to service provider', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.foo).equal('bar');
+ expect(u.query.state).to.have.length(24);
+ });
+
+ it('should save state in session', function() {
+ var u = uri.parse(url, true);
+
+ expect(request.session['oauth2:www.example.com'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:www.example.com'].state.handle).to.equal(u.query.state);
+ });
+ }); // that redirects to service provider
+
+ }); // issuing authorization request to authorization server using authorization endpoint that has query parameters including state
+
+ describe('processing response to authorization request', function() {
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken !== '2YotnFZFEjr1zCsicMWpAA') { return done(new Error('incorrect accessToken argument')); }
+ if (refreshToken !== 'tGzv3JOkF0XG5Qx2TlKWIA') { return done(new Error('incorrect refreshToken argument')); }
+ if (typeof profile !== 'object') { return done(new Error('incorrect profile argument')); }
+ if (Object.keys(profile).length !== 0) { return done(new Error('incorrect profile argument')); }
+
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ });
+
+ strategy._oauth2.getOAuthAccessToken = function(code, options, callback) {
+ if (code !== 'SplxlOBeZQQYbYS6WxSbIA') { return callback(new Error('incorrect code argument')); }
+ if (options.grant_type !== 'authorization_code') { return callback(new Error('incorrect options.grant_type argument')); }
+ if (options.redirect_uri !== 'https://www.example.net/auth/example/callback') { return callback(new Error('incorrect options.redirect_uri argument')); }
+
+ return callback(null, '2YotnFZFEjr1zCsicMWpAA', 'tGzv3JOkF0XG5Qx2TlKWIA', { token_type: 'example' });
+ }
+
+
+ describe('that was approved', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK' };
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ expect(info.state).to.be.undefined;
+ });
+
+ it('should remove state from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ }); // that was approved
+
+ describe('that was approved with state', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK', state: { returnTo: '/somewhere' } };
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ expect(info.state).to.deep.equal({ returnTo: '/somewhere' });
+ });
+
+ it('should remove state from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ }); // that was approved with state
+
+ describe('that was approved with other data in the session', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK' };
+ req.session['oauth2:www.example.com'].foo = 'bar';
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ });
+
+ it('should preserve other data from session', function() {
+ expect(request.session['oauth2:www.example.com'].state).to.be.undefined;
+ expect(request.session['oauth2:www.example.com'].foo).to.equal('bar');
+ });
+ }); // that was approved with other data in the session
+
+ describe('that fails due to state being invalid', function() {
+ var request
+ , info, status;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .fail(function(i, s) {
+ info = i;
+ status = s;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK-WRONG';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ req.session['oauth2:www.example.com']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK' };
+ })
+ .authenticate();
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Invalid authorization request state.');
+ });
+
+ it('should supply status', function() {
+ expect(status).to.equal(403);
+ });
+
+ it('should remove state from session', function() {
+ expect(request.session['oauth2:www.example.com']).to.be.undefined;
+ });
+ }); // that fails due to state being invalid
+
+ describe('that fails due to provider-specific state not found in session', function() {
+ var request
+ , info, status;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .fail(function(i, s) {
+ info = i;
+ status = s;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Unable to verify authorization request state.');
+ });
+
+ it('should supply status', function() {
+ expect(status).to.equal(403);
+ });
+ }); // that fails due to state not found in session
+
+ describe('that fails due to provider-specific state lacking state value', function() {
+ var request
+ , info, status;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .fail(function(i, s) {
+ info = i;
+ status = s;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:www.example.com'] = {};
+ })
+ .authenticate();
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Unable to verify authorization request state.');
+ });
+
+ it('should supply status', function() {
+ expect(status).to.equal(403);
+ });
+ }); // that fails due to provider-specific state lacking state value
+
+ describe('that errors due to lack of session support in app', function() {
+ var request
+ , err;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .error(function(e) {
+ err = e;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ })
+ .authenticate();
+ });
+
+ it('should error', function() {
+ expect(err).to.be.an.instanceof(Error)
+ expect(err.message).to.equal('OAuth 2.0 authentication requires session support when using state. Did you forget to use express-session middleware?');
+ });
+ }); // that errors due to lack of session support in app
+
+ }); // processing response to authorization request
+
+ }); // using default session state store
+
+
+ describe('using default session state store through store option with session key option', function() {
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ store: true,
+ sessionKey: 'oauth2:example'
+ },
+ function(accessToken, refreshToken, profile, done) {
+ if (accessToken !== '2YotnFZFEjr1zCsicMWpAA') { return done(new Error('incorrect accessToken argument')); }
+ if (refreshToken !== 'tGzv3JOkF0XG5Qx2TlKWIA') { return done(new Error('incorrect refreshToken argument')); }
+ if (typeof profile !== 'object') { return done(new Error('incorrect profile argument')); }
+ if (Object.keys(profile).length !== 0) { return done(new Error('incorrect profile argument')); }
+
+ return done(null, { id: '1234' }, { message: 'Hello' });
+ });
+
+ strategy._oauth2.getOAuthAccessToken = function(code, options, callback) {
+ if (code !== 'SplxlOBeZQQYbYS6WxSbIA') { return callback(new Error('incorrect code argument')); }
+ if (options.grant_type !== 'authorization_code') { return callback(new Error('incorrect options.grant_type argument')); }
+ if (options.redirect_uri !== 'https://www.example.net/auth/example/callback') { return callback(new Error('incorrect options.redirect_uri argument')); }
+
+ return callback(null, '2YotnFZFEjr1zCsicMWpAA', 'tGzv3JOkF0XG5Qx2TlKWIA', { token_type: 'example' });
+ }
+
+
+ describe('issuing authorization request', function() {
+
+ describe('that redirects to service provider', function() {
+ var request, url;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .redirect(function(u) {
+ url = u;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+ req.session = {};
+ })
+ .authenticate();
+ });
+
+ it('should be redirected', function() {
+ var u = uri.parse(url, true);
+ expect(u.query.state).to.have.length(24);
+ });
+
+ it('should save state in session', function() {
+ var u = uri.parse(url, true);
+
+ expect(request.session['oauth2:example'].state.handle).to.have.length(24);
+ expect(request.session['oauth2:example'].state.handle).to.equal(u.query.state);
+ expect(request.session['oauth2:example'].state.state).to.be.undefined;
+ });
+ }); // that redirects to service provider
+
+ }); // issuing authorization request
+
+ describe('processing response to authorization request', function() {
+
+ describe('that was approved', function() {
+ var request
+ , user
+ , info;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .success(function(u, i) {
+ user = u;
+ info = i;
+ done();
+ })
+ .req(function(req) {
+ request = req;
+
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ req.query.state = 'DkbychwKu8kBaJoLE5yeR5NK';
+ req.session = {};
+ req.session['oauth2:example'] = {};
+ req.session['oauth2:example']['state'] = { handle: 'DkbychwKu8kBaJoLE5yeR5NK' };
+ })
+ .authenticate();
+ });
+
+ it('should supply user', function() {
+ expect(user).to.be.an.object;
+ expect(user.id).to.equal('1234');
+ });
+
+ it('should supply info', function() {
+ expect(info).to.be.an.object;
+ expect(info.message).to.equal('Hello');
+ });
+
+ it('should remove state from session', function() {
+ expect(request.session['oauth2:example']).to.be.undefined;
+ });
+ }); // that was approved
+
+ }); // processing response to authorization request
+
+ }); // using default session state store with session key option
+
+});
diff --git a/test/oauth2.test.js b/test/oauth2.test.js
index e829751..36552b6 100644
--- a/test/oauth2.test.js
+++ b/test/oauth2.test.js
@@ -1256,6 +1256,45 @@ describe('OAuth2Strategy', function() {
});
}); // that errors due to token request error, in node-oauth object literal form with text body
+ describe('that errors due to not receiving an access token', function() {
+ var strategy = new OAuth2Strategy({
+ authorizationURL: 'https://www.example.com/oauth2/authorize',
+ tokenURL: 'https://www.example.com/oauth2/token',
+ clientID: 'ABC123',
+ clientSecret: 'secret',
+ callbackURL: 'https://www.example.net/auth/example/callback',
+ },
+ function(accessToken, refreshToken, params, profile, done) {
+ return done(new Error('something went wrong'));
+ });
+
+ strategy._oauth2.getOAuthAccessToken = function(code, options, callback) {
+ return callback(null, undefined, undefined, undefined);
+ }
+
+
+ var err;
+
+ before(function(done) {
+ chai.passport.use(strategy)
+ .error(function(e) {
+ err = e;
+ done();
+ })
+ .req(function(req) {
+ req.query = {};
+ req.query.code = 'SplxlOBeZQQYbYS6WxSbIA';
+ })
+ .authenticate();
+ });
+
+ it('should error', function() {
+ expect(err).to.be.an.instanceof(Error);
+ expect(err).to.not.be.an.instanceof(InternalOAuthError)
+ expect(err.message).to.equal('Failed to obtain access token');
+ });
+ }); // that errors due to not receiving an access token
+
describe('that errors due to verify callback supplying error', function() {
var strategy = new OAuth2Strategy({
authorizationURL: 'https://www.example.com/oauth2/authorize',