Skip to content

Commit

Permalink
Merge 5d964b1 into ad9a48e
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredhanson committed Mar 15, 2019
2 parents ad9a48e + 5d964b1 commit ddb336a
Show file tree
Hide file tree
Showing 6 changed files with 891 additions and 12 deletions.
88 changes: 88 additions & 0 deletions lib/state/pkcesession.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 PKCESessionStore(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
*/
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 = {
handle: uid(24),
code_verifier: verifier
};
if (!req.session[key]) { req.session[key] = {}; }
req.session[key].state = state;
callback(null, state.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
*/
PKCESessionStore.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, state.code_verifier);
};

// Expose constructor.
module.exports = PKCESessionStore;
8 changes: 4 additions & 4 deletions lib/state/session.js
Expand Up @@ -61,21 +61,21 @@ SessionStore.prototype.verify = function(req, providedState, callback) {

var key = this._key;
if (!req.session[key]) {
return callback(null, false, { message: 'Unable to verify authorization request state.' });
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.' });
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];
delete req.session[key];
}

if (state !== providedState) {
return callback(null, false, { message: 'Invalid authorization request state.' });
return callback(null, false, { message: 'Invalid authorization request state.' });
}

return callback(null, true);
Expand Down
45 changes: 37 additions & 8 deletions lib/strategy.js
@@ -1,11 +1,15 @@
// Load modules.
var passport = require('passport-strategy')
, url = require('url')
, uid = require('uid2')
, crypto = require('crypto')
, base64url = require('base64url')
, util = require('util')
, utils = require('./utils')
, OAuth2 = require('oauth').OAuth2
, NullStateStore = require('./state/null')
, SessionStateStore = require('./state/session')
, PKCESessionStateStore = require('./state/pkcesession')
, AuthorizationError = require('./errors/authorizationerror')
, TokenError = require('./errors/tokenerror')
, InternalOAuthError = require('./errors/internaloautherror');
Expand Down Expand Up @@ -89,19 +93,21 @@ function OAuth2Strategy(options, verify) {
// allowed to use it when making protected resource requests to retrieve
// the user profile.
this._oauth2 = new OAuth2(options.clientID, options.clientSecret,
'', options.authorizationURL, options.tokenURL, options.customHeaders);
'', options.authorizationURL, options.tokenURL, options.customHeaders);

this._callbackURL = options.callbackURL;
this._scope = options.scope;
this._scopeSeparator = options.scopeSeparator || ' ';
this._pkceMethod = (options.pkce === true) ? 'S256' : options.pkce;
this._key = options.sessionKey || ('oauth2:' + url.parse(options.authorizationURL).hostname);

if (options.store) {
this._stateStore = options.store;
} else {
if (options.state) {
this._stateStore = new SessionStateStore({ key: this._key });
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();
}
}
Expand Down Expand Up @@ -141,7 +147,7 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
callbackURL = url.resolve(utils.originalURL(req, { proxy: this._trustProxy }), callbackURL);
}
}

var meta = {
authorizationURL: this._oauth2._authorizeUrl,
tokenURL: this._oauth2._accessTokenUrl,
Expand All @@ -154,12 +160,15 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
if (!ok) {
return self.fail(state, 403);
}

var code = req.query.code;

var params = self.tokenParams(options);
params.grant_type = 'authorization_code';
if (callbackURL) { params.redirect_uri = callbackURL; }
if (typeof ok == 'string') { // PKCE
params.code_verifier = ok;
}

self._oauth2.getOAuthAccessToken(code, params,
function(err, accessToken, refreshToken, params) {
Expand All @@ -171,7 +180,7 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
function verified(err, user, info) {
if (err) { return self.error(err); }
if (!user) { return self.fail(info); }

info = info || {};
if (state) { info.state = state; }
self.success(user, info);
Expand Down Expand Up @@ -200,7 +209,7 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
}
);
}

var state = req.query.state;
try {
var arity = this._stateStore.verify.length;
Expand All @@ -221,6 +230,24 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
if (Array.isArray(scope)) { scope = scope.join(this._scopeSeparator); }
params.scope = scope;
}
var verifier, challenge;

if (this._pkceMethod) {
verifier = base64url(crypto.pseudoRandomBytes(32))
switch (this._pkceMethod) {
case 'plain':
challenge = verifier;
break;
case 'S256':
challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
break;
default:
return this.error(new Error('Unsupported code verifier transformation method: ' + this._pkceMethod));
}

params.code_challenge = challenge;
params.code_challenge_method = this._pkceMethod;
}

var state = options.state;
if (state) {
Expand All @@ -244,10 +271,12 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
var location = url.format(parsed);
self.redirect(location);
}

try {
var arity = this._stateStore.store.length;
if (arity == 3) {
if (arity == 5) {
this._stateStore.store(req, verifier, undefined, meta, stored);
} else if (arity == 3) {
this._stateStore.store(req, meta, stored);
} else { // arity == 2
this._stateStore.store(req, stored);
Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -33,6 +33,7 @@
],
"main": "./lib",
"dependencies": {
"base64url": "^3.0.0",
"oauth": "0.9.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
Expand All @@ -42,6 +43,7 @@
"make-node": "0.4.6",
"mocha": "2.x.x",
"chai": "2.x.x",
"proxyquire": "1.x.x",
"chai-passport-strategy": "1.x.x"
},
"engines": {
Expand Down
1 change: 1 addition & 0 deletions test/bootstrap/node.js
Expand Up @@ -4,4 +4,5 @@ var chai = require('chai')
chai.use(passport);


global.$require = require('proxyquire');
global.expect = chai.expect;

0 comments on commit ddb336a

Please sign in to comment.