-
-
Notifications
You must be signed in to change notification settings - Fork 345
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #138 from jaredhanson/state-opt
Automatic state support
- Loading branch information
Showing
8 changed files
with
1,798 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
|
@@ -250,7 +252,17 @@ OAuth2Strategy.prototype.authenticate = function(req, options) { | |
} | ||
|
||
var state = options.state; | ||
if (state) { | ||
if (state && typeof state == 'string') { | ||
// NOTE: In [email protected] 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 +287,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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.