diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml new file mode 100644 index 00000000..a273f65a --- /dev/null +++ b/.github/workflows/node.yml @@ -0,0 +1,42 @@ +# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: + - '17' + - '16' + - '14' + - '12' + - '10' + - '8' + - '6' + - '4' + # - '3' # io.js + # - '2' # io.js + # - '1' # io.js + - '0.12' + - '0.10' + # - '0.8' + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d3662771 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,76 @@ +# 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). + +## [Unreleased] + +## [0.6.0] - 2022-05-20 +### Added +- `authenticate()`, `req#login`, and `req#logout` accept a +`keepSessionInfo: true` option to keep session information after regenerating +the session. + +### Changed + +- `req#login()` and `req#logout()` regenerate the the session and clear session +information by default. +- `req#logout()` is now an asynchronous function and requires a callback +function as the last argument. + +### Security + +- Improved robustness against session fixation attacks in cases where there is +physical access to the same system or the application is susceptible to +cross-site scripting (XSS). + +## [0.5.3] - 2022-05-16 +### Fixed + +- `initialize()` middleware extends request with `login()`, `logIn()`, +`logout()`, `logOut()`, `isAuthenticated()`, and `isUnauthenticated()` functions +again, reverting change from 0.5.1. + +## [0.5.2] - 2021-12-16 +### Fixed +- Introduced a compatibility layer for strategies that depend directly on +`passport@0.4.x` or earlier (such as `passport-azure-ad`), which were +broken by the removal of private variables in `passport@0.5.1`. + +## [0.5.1] - 2021-12-15 +### Added +- Informative error message in session strategy if session support is not +available. + +### Changed + +- `authenticate()` middleware, rather than `initialize()` middleware, extends +request with `login()`, `logIn()`, `logout()`, `logOut()`, `isAuthenticated()`, +and `isUnauthenticated()` functions. + +## [0.5.0] - 2021-09-23 +### Changed + +- `initialize()` middleware extends request with `login()`, `logIn()`, +`logout()`, `logOut()`, `isAuthenticated()`, and `isUnauthenticated()` +functions. + +### Removed + +- `login()`, `logIn()`, `logout()`, `logOut()`, `isAuthenticated()`, and +`isUnauthenticated()` functions no longer added to `http.IncomingMessage.prototype`. + +### Fixed + +- `userProperty` option to `initialize()` middleware only affects the current +request, rather than all requests processed via singleton Passport instance, +eliminating a race condition in situations where `initialize()` middleware is +used multiple times in an application with `userProperty` set to different +values. + +[Unreleased]: https://github.com/jaredhanson/passport/compare/v0.6.0...HEAD +[0.6.0]: https://github.com/jaredhanson/passport/compare/v0.5.3...v0.6.0 +[0.5.3]: https://github.com/jaredhanson/passport/compare/v0.5.2...v0.5.3 +[0.5.2]: https://github.com/jaredhanson/passport/compare/v0.5.1...v0.5.2 +[0.5.1]: https://github.com/jaredhanson/passport/compare/v0.5.0...v0.5.1 diff --git a/LICENSE b/LICENSE index ba9eb282..2a3ab4d5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2011-2019 Jared Hanson +Copyright (c) 2011-2021 Jared Hanson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/Makefile b/Makefile index d148666d..85ff65a6 100644 --- a/Makefile +++ b/Makefile @@ -21,5 +21,8 @@ clean: clean-docs clean-cov clobber: clean -rm -r node_modules +html: + jsdoc -c etc/jsdoc.json -d ./doc $(SOURCES) + .PHONY: clean clobber diff --git a/README.md b/README.md index ffd80c08..e8b3bc87 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,22 @@ hooks for controlling what occurs when authentication succeeds or fails.

Sponsors
-
- LoginRadius is built for the developer community to integrate robust Authentication and Single Sign-On in just a few lines of code.
FREE Signup -


Your app, enterprise-ready.
Start selling to enterprise customers with just a few lines of code. Add Single Sign-On (and more) in minutes instead of months.
+
+
+ + + + + + +
+ Drag and drop your auth
Add authentication and user management to your consumer and business apps with a few lines of code.
+
+
+
+ Auth. Built for Devs, by Devs
Add login, registration, SSO, MFA, and a bazillion other features to your app in minutes. Integrates with any codebase and installs on any server, anywhere in the world.

--- @@ -171,4 +182,4 @@ that build upon or integrate with Passport. [The MIT License](http://opensource.org/licenses/MIT) -Copyright (c) 2011-2019 Jared Hanson <[http://jaredhanson.net/](http://jaredhanson.net/)> +Copyright (c) 2011-2021 Jared Hanson <[https://www.jaredhanson.me/](https://www.jaredhanson.me/)> diff --git a/SPONSORS.md b/SPONSORS.md index b4df8e6b..be45816a 100644 --- a/SPONSORS.md +++ b/SPONSORS.md @@ -1,11 +1,17 @@ ## Gold Sponsors -[![LoginRadius](https://raw.githubusercontent.com/jaredhanson/passport/master/sponsors/loginradius.png)](https://www.loginradius.com/) -

[![WorkOS](https://raw.githubusercontent.com/jaredhanson/passport/master/sponsors/workos.png)](https://workos.com/) +
+[![Snyk](https://raw.githubusercontent.com/jaredhanson/passport/master/sponsors/snyk.png)](https://snyk.io/) ## Sponsors - [CodePilot.ai](https://codepilot.ai/) +- [Jeremy Combs](https://github.com/jmcombs) +- [Gadget](https://gadget.dev/) - Kelly Burke - [Matt Miller](https://mmiller.me/) + +## Past Sponsors + +- [LoginRadius](https://www.loginradius.com/) diff --git a/etc/jsdoc.json b/etc/jsdoc.json new file mode 100644 index 00000000..df0756c6 --- /dev/null +++ b/etc/jsdoc.json @@ -0,0 +1,3 @@ +{ + "plugins": ["plugins/markdown"] +} diff --git a/lib/authenticator.js b/lib/authenticator.js index 01605469..98e663c7 100644 --- a/lib/authenticator.js +++ b/lib/authenticator.js @@ -1,14 +1,13 @@ -/** - * Module dependencies. - */ +// Module dependencies. var SessionStrategy = require('./strategies/session') , SessionManager = require('./sessionmanager'); /** - * `Authenticator` constructor. + * Create a new `Authenticator` object. * - * @api public + * @public + * @class */ function Authenticator() { this._key = 'passport'; @@ -17,7 +16,6 @@ function Authenticator() { this._deserializers = []; this._infoTransformers = []; this._framework = null; - this._userProperty = 'user'; this.init(); } @@ -25,28 +23,36 @@ function Authenticator() { /** * Initialize authenticator. * - * @api protected + * Initializes the `Authenticator` instance by creating the default `{@link SessionManager}`, + * {@link Authenticator#use `use()`}'ing the default `{@link SessionStrategy}`, and + * adapting it to work as {@link https://github.com/senchalabs/connect#readme Connect}-style + * middleware, which is also compatible with {@link https://expressjs.com/ Express}. + * + * @private */ Authenticator.prototype.init = function() { this.framework(require('./framework/connect')()); - this.use(new SessionStrategy(this.deserializeUser.bind(this))); + this.use(new SessionStrategy({ key: this._key }, this.deserializeUser.bind(this))); this._sm = new SessionManager({ key: this._key }, this.serializeUser.bind(this)); }; /** - * Utilize the given `strategy` with optional `name`, overridding the strategy's - * default name. - * - * Examples: - * - * passport.use(new TwitterStrategy(...)); - * - * passport.use('api', new http.BasicStrategy(...)); - * - * @param {String|Strategy} name - * @param {Strategy} strategy - * @return {Authenticator} for chaining - * @api public + * Register a strategy for later use when authenticating requests. The name + * with which the strategy is registered is passed to {@link Authenticator#authenticate `authenticate()`}. + * + * @public + * @param {string} [name=strategy.name] - Name of the strategy. When specified, + * this value overrides the strategy's name. + * @param {Strategy} strategy - Authentication strategy. + * @returns {this} + * + * @example Register strategy. + * passport.use(new GoogleStrategy(...)); + * + * @example Register strategy and override name. + * passport.use('password', new LocalStrategy(function(username, password, cb) { + * // ... + * })); */ Authenticator.prototype.use = function(name, strategy) { if (!strategy) { @@ -60,23 +66,18 @@ Authenticator.prototype.use = function(name, strategy) { }; /** - * Un-utilize the `strategy` with given `name`. - * - * In typical applications, the necessary authentication strategies are static, - * configured once and always available. As such, there is often no need to - * invoke this function. - * - * However, in certain situations, applications may need dynamically configure - * and de-configure authentication strategies. The `use()`/`unuse()` - * combination satisfies these scenarios. + * Deregister a strategy that was previously registered with the given name. * - * Examples: + * In a typical application, the necessary authentication strategies are + * registered when initializing the app and, once registered, are always + * available. As such, it is typically not necessary to call this function. * - * passport.unuse('legacy-api'); + * @public + * @param {string} name - Name of the strategy. + * @returns {this} * - * @param {String} name - * @return {Authenticator} for chaining - * @api public + * @example + * passport.unuse('acme'); */ Authenticator.prototype.unuse = function(name) { delete this._strategies[name]; @@ -84,23 +85,15 @@ Authenticator.prototype.unuse = function(name) { }; /** - * Setup Passport to be used under framework. - * - * By default, Passport exposes middleware that operate using Connect-style - * middleware using a `fn(req, res, next)` signature. Other popular frameworks - * have different expectations, and this function allows Passport to be adapted - * to operate within such environments. - * - * If you are using a Connect-compatible framework, including Express, there is - * no need to invoke this function. - * - * Examples: + * Adapt this `Authenticator` to work with a specific framework. * - * passport.framework(require('hapi-passport')()); + * By default, Passport works as {@link https://github.com/senchalabs/connect#readme Connect}-style + * middleware, which makes it compatible with {@link https://expressjs.com/ Express}. + * For any app built using Express, there is no need to call this function. * - * @param {Object} name - * @return {Authenticator} for chaining - * @api public + * @public + * @param {Object} fw + * @returns {this} */ Authenticator.prototype.framework = function(fw) { this._framework = fw; @@ -108,82 +101,104 @@ Authenticator.prototype.framework = function(fw) { }; /** - * Passport's primary initialization middleware. - * - * This middleware must be in use by the Connect/Express application for - * Passport to operate. - * - * Options: - * - `userProperty` Property to set on `req` upon login, defaults to _user_ - * - * Examples: - * - * app.use(passport.initialize()); - * - * app.use(passport.initialize({ userProperty: 'currentUser' })); - * - * @param {Object} options - * @return {Function} middleware - * @api public + * Create initialization middleware. + * + * Returns middleware that initializes Passport to authenticate requests. + * + * As of v0.6.x, it is typically no longer necessary to use this middleware. It + * exists for compatiblity with apps built using previous versions of Passport, + * in which this middleware was necessary. + * + * The primary exception to the above guidance is when using strategies that + * depend directly on `passport@0.4.x` or earlier. These earlier versions of + * Passport monkeypatch Node.js `http.IncomingMessage` in a way that expects + * certain Passport-specific properties to be available. This middleware + * provides a compatibility layer for this situation. + * + * @public + * @param {Object} [options] + * @param {string} [options.userProperty='user'] - Determines what property on + * `req` will be set to the authenticated user object. + * @param {boolean} [options.compat=true] - When `true`, enables a compatibility + * layer for packages that depend on `passport@0.4.x` or earlier. + * @returns {function} + * + * @example + * app.use(passport.initialize()); */ Authenticator.prototype.initialize = function(options) { options = options || {}; - this._userProperty = options.userProperty || 'user'; - return this._framework.initialize(this, options); }; /** - * Middleware that will authenticate a request using the given `strategy` name, - * with optional `options` and `callback`. + * Create authentication middleware. * - * Examples: + * Returns middleware that authenticates the request by applying the given + * strategy (or strategies). * - * passport.authenticate('local', { successRedirect: '/', failureRedirect: '/login' })(req, res); + * Examples: * * passport.authenticate('local', function(err, user) { * if (!user) { return res.redirect('/login'); } * res.end('Authenticated!'); * })(req, res); * - * passport.authenticate('basic', { session: false })(req, res); - * - * app.get('/auth/twitter', passport.authenticate('twitter'), function(req, res) { - * // request will be redirected to Twitter - * }); - * app.get('/auth/twitter/callback', passport.authenticate('twitter'), function(req, res) { - * res.json(req.user); - * }); - * - * @param {String} strategy - * @param {Object} options - * @param {Function} callback - * @return {Function} middleware - * @api public + * @public + * @param {string|string[]|Strategy} strategy + * @param {Object} [options] + * @param {boolean} [options.session=true] + * @param {boolean} [options.keepSessionInfo=false] + * @param {string} [options.failureRedirect] + * @param {boolean|string|Object} [options.failureFlash=false] + * @param {boolean|string} [options.failureMessage=false] + * @param {boolean|string|Object} [options.successFlash=false] + * @param {string} [options.successReturnToOrRedirect] + * @param {string} [options.successRedirect] + * @param {boolean|string} [options.successMessage=false] + * @param {boolean} [options.failWithError=false] + * @param {string} [options.assignProperty] + * @param {boolean} [options.authInfo=true] + * @param {function} [callback] + * @returns {function} + * + * @example Authenticate username and password submitted via HTML form. + * app.get('/login/password', passport.authenticate('local', { successRedirect: '/', failureRedirect: '/login' })); + * + * @example Authenticate bearer token used to access an API resource. + * app.get('/api/resource', passport.authenticate('bearer', { session: false })); */ Authenticator.prototype.authenticate = function(strategy, options, callback) { return this._framework.authenticate(this, strategy, options, callback); }; /** - * Middleware that will authorize a third-party account using the given - * `strategy` name, with optional `options`. + * Create third-party service authorization middleware. * - * If authorization is successful, the result provided by the strategy's verify - * callback will be assigned to `req.account`. The existing login session and - * `req.user` will be unaffected. + * Returns middleware that will authorize a connection to a third-party service. * - * This function is particularly useful when connecting third-party accounts - * to the local account of a user that is currently authenticated. + * This middleware is identical to using {@link Authenticator#authenticate `authenticate()`} + * middleware with the `assignProperty` option set to `'account'`. This is + * useful when a user is already authenticated (for example, using a username + * and password) and they want to connect their account with a third-party + * service. * - * Examples: + * In this scenario, the user's third-party account will be set at + * `req.account`, and the existing `req.user` and login session data will be + * be left unmodified. A route handler can then link the third-party account to + * the existing local account. * - * passport.authorize('twitter-authz', { failureRedirect: '/account' }); + * All arguments to this function behave identically to those accepted by + * `{@link Authenticator#authenticate}`. * - * @param {String} strategy - * @param {Object} options - * @return {Function} middleware - * @api public + * @public + * @param {string|string[]|Strategy} strategy + * @param {Object} [options] + * @param {function} [callback] + * @returns {function} + * + * @example + * app.get('/oauth/callback/twitter', passport.authorize('twitter')); */ Authenticator.prototype.authorize = function(strategy, options, callback) { options = options || {}; diff --git a/lib/framework/connect.js b/lib/framework/connect.js index 5c5beb09..7d2db6e1 100644 --- a/lib/framework/connect.js +++ b/lib/framework/connect.js @@ -8,32 +8,15 @@ var initialize = require('../middleware/initialize') * Framework support for Connect/Express. * * This module provides support for using Passport with Express. It exposes - * middleware that conform to the `fn(req, res, next)` signature and extends - * Node's built-in HTTP request object with useful authentication-related - * functions. + * middleware that conform to the `fn(req, res, next)` signature. * * @return {Object} * @api protected */ exports = module.exports = function() { - // HTTP extensions. - exports.__monkeypatchNode(); - return { initialize: initialize, authenticate: authenticate }; }; - -exports.__monkeypatchNode = function() { - var http = require('http'); - var IncomingMessageExt = require('../http/request'); - - http.IncomingMessage.prototype.login = - http.IncomingMessage.prototype.logIn = IncomingMessageExt.logIn; - http.IncomingMessage.prototype.logout = - http.IncomingMessage.prototype.logOut = IncomingMessageExt.logOut; - http.IncomingMessage.prototype.isAuthenticated = IncomingMessageExt.isAuthenticated; - http.IncomingMessage.prototype.isUnauthenticated = IncomingMessageExt.isUnauthenticated; -}; diff --git a/lib/http/request.js b/lib/http/request.js index 0206abb8..cdb5432f 100644 --- a/lib/http/request.js +++ b/lib/http/request.js @@ -1,10 +1,3 @@ -/** - * Module dependencies. - */ -//var http = require('http') -// , req = http.IncomingMessage.prototype; - - var req = exports = module.exports = {}; /** @@ -35,19 +28,15 @@ req.logIn = function(user, options, done) { } options = options || {}; - var property = 'user'; - if (this._passport && this._passport.instance) { - property = this._passport.instance._userProperty || 'user'; - } + var property = this._userProperty || 'user'; var session = (options.session === undefined) ? true : options.session; this[property] = user; - if (session) { - if (!this._passport) { throw new Error('passport.initialize() middleware not in use'); } + if (session && this._sessionManager) { if (typeof done != 'function') { throw new Error('req#login requires a callback function'); } var self = this; - this._passport.instance._sm.logIn(this, user, function(err) { + this._sessionManager.logIn(this, user, options, function(err) { if (err) { self[property] = null; return done(err); } done(); }); @@ -62,15 +51,22 @@ req.logIn = function(user, options, done) { * @api public */ req.logout = -req.logOut = function() { - var property = 'user'; - if (this._passport && this._passport.instance) { - property = this._passport.instance._userProperty || 'user'; +req.logOut = function(options, done) { + if (typeof options == 'function') { + done = options; + options = {}; } + options = options || {}; + + var property = this._userProperty || 'user'; this[property] = null; - if (this._passport) { - this._passport.instance._sm.logOut(this); + if (this._sessionManager) { + if (typeof done != 'function') { throw new Error('req#logout requires a callback function'); } + + this._sessionManager.logOut(this, options, done); + } else { + done && done(); } }; @@ -81,11 +77,7 @@ req.logOut = function() { * @api public */ req.isAuthenticated = function() { - var property = 'user'; - if (this._passport && this._passport.instance) { - property = this._passport.instance._userProperty || 'user'; - } - + var property = this._userProperty || 'user'; return (this[property]) ? true : false; }; diff --git a/lib/index.js b/lib/index.js index ab174691..dcfa770c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,4 @@ -/** - * Module dependencies. - */ +// Module dependencies. var Passport = require('./authenticator') , SessionStrategy = require('./strategies/session'); @@ -19,7 +17,7 @@ exports.Passport = exports.Authenticator = Passport; exports.Strategy = require('passport-strategy'); -/** +/* * Expose strategies. */ exports.strategies = {}; diff --git a/lib/middleware/authenticate.js b/lib/middleware/authenticate.js index 841d2821..accdc0ae 100644 --- a/lib/middleware/authenticate.js +++ b/lib/middleware/authenticate.js @@ -92,11 +92,14 @@ module.exports = function authenticate(passport, name, options, callback) { } return function authenticate(req, res, next) { - if (http.IncomingMessage.prototype.logIn - && http.IncomingMessage.prototype.logIn !== IncomingMessageExt.logIn) { - require('../framework/connect').__monkeypatchNode(); - } + req.login = + req.logIn = req.logIn || IncomingMessageExt.logIn; + req.logout = + req.logOut = req.logOut || IncomingMessageExt.logOut; + req.isAuthenticated = req.isAuthenticated || IncomingMessageExt.isAuthenticated; + req.isUnauthenticated = req.isUnauthenticated || IncomingMessageExt.isUnauthenticated; + req._sessionManager = passport._sm; // accumulator for failures from each strategy in the chain var failures = []; diff --git a/lib/middleware/initialize.js b/lib/middleware/initialize.js index 53ce3d86..0b6306df 100644 --- a/lib/middleware/initialize.js +++ b/lib/middleware/initialize.js @@ -1,3 +1,9 @@ +/** + * Module dependencies. + */ +var IncomingMessageExt = require('../http/request'); + + /** * Passport initialization. * @@ -39,17 +45,56 @@ * @return {Function} * @api public */ -module.exports = function initialize(passport) { +module.exports = function initialize(passport, options) { + options = options || {}; return function initialize(req, res, next) { - req._passport = {}; - req._passport.instance = passport; - - if (req.session && req.session[passport._key]) { - // load data from existing session - req._passport.session = req.session[passport._key]; + req.login = + req.logIn = req.logIn || IncomingMessageExt.logIn; + req.logout = + req.logOut = req.logOut || IncomingMessageExt.logOut; + req.isAuthenticated = req.isAuthenticated || IncomingMessageExt.isAuthenticated; + req.isUnauthenticated = req.isUnauthenticated || IncomingMessageExt.isUnauthenticated; + + req._sessionManager = passport._sm; + + if (options.userProperty) { + req._userProperty = options.userProperty; } - + + var compat = (options.compat === undefined) ? true : options.compat; + if (compat) { + // `passport@0.5.1` [removed][1] all internal use of `req._passport`. + // From the standpoint of this package, this should have been a + // non-breaking change. However, some strategies (such as `passport-azure-ad`) + // depend directly on `passport@0.4.x` or earlier. `require`-ing earlier + // versions of `passport` has the effect of monkeypatching `http.IncomingMessage` + // with `logIn`, `logOut`, `isAuthenticated` and `isUnauthenticated` + // functions that [expect][2] the `req._passport` property to exist. + // Since pre-existing functions on `req` are given [preference][3], this + // results in [issues][4]. + // + // The changes here restore the expected properties needed when earlier + // versions of `passport` are `require`-ed. This compatibility mode is + // enabled by default, and can be disabld by simply not `use`-ing `passport.initialize()` + // middleware or setting `compat: false` as an option to the middleware. + // + // An alternative approach to addressing this issue would be to not + // preferentially use pre-existing functions on `req`, but rather always + // overwrite `req.logIn`, etc. with the versions of those functions shiped + // with `authenticate()` middleware. This option should be reconsidered + // in a future major version release. + // + // [1]: https://github.com/jaredhanson/passport/pull/875 + // [2]: https://github.com/jaredhanson/passport/blob/v0.4.1/lib/http/request.js + // [3]: https://github.com/jaredhanson/passport/blob/v0.5.1/lib/middleware/authenticate.js#L96 + // [4]: https://github.com/jaredhanson/passport/issues/877 + passport._userProperty = options.userProperty || 'user'; + + req._passport = {}; + req._passport.instance = passport; + } + next(); }; }; diff --git a/lib/sessionmanager.js b/lib/sessionmanager.js index 0fdbd8bd..81b59b1d 100644 --- a/lib/sessionmanager.js +++ b/lib/sessionmanager.js @@ -1,3 +1,5 @@ +var merge = require('utils-merge'); + function SessionManager(options, serializeUser) { if (typeof options == 'function') { serializeUser = options; @@ -9,29 +11,85 @@ function SessionManager(options, serializeUser) { this._serializeUser = serializeUser; } -SessionManager.prototype.logIn = function(req, user, cb) { +SessionManager.prototype.logIn = function(req, user, options, cb) { + if (typeof options == 'function') { + cb = options; + options = {}; + } + options = options || {}; + + if (!req.session) { return cb(new Error('Login sessions require session support. Did you forget to use `express-session` middleware?')); } + var self = this; - this._serializeUser(user, req, function(err, obj) { + var prevSession = req.session; + + // regenerate the session, which is good practice to help + // guard against forms of session fixation + req.session.regenerate(function(err) { if (err) { return cb(err); } - if (!req._passport.session) { - req._passport.session = {}; - } - req._passport.session.user = obj; - if (!req.session) { - req.session = {}; - } - req.session[self._key] = req._passport.session; - cb(); + + self._serializeUser(user, req, function(err, obj) { + if (err) { + return cb(err); + } + if (options.keepSessionInfo) { + merge(req.session, prevSession); + } + if (!req.session[self._key]) { + req.session[self._key] = {}; + } + // store user information in session, typically a user id + req.session[self._key].user = obj; + // save the session before redirection to ensure page + // load does not happen before session is saved + req.session.save(function(err) { + if (err) { + return cb(err); + } + cb(); + }); + }); }); } -SessionManager.prototype.logOut = function(req, cb) { - if (req._passport && req._passport.session) { - delete req._passport.session.user; +SessionManager.prototype.logOut = function(req, options, cb) { + if (typeof options == 'function') { + cb = options; + options = {}; } - cb && cb(); + options = options || {}; + + if (!req.session) { return cb(new Error('Login sessions require session support. Did you forget to use `express-session` middleware?')); } + + var self = this; + + // clear the user from the session object and save. + // this will ensure that re-using the old session id + // does not have a logged in user + if (req.session[this._key]) { + delete req.session[this._key].user; + } + var prevSession = req.session; + + req.session.save(function(err) { + if (err) { + return cb(err) + } + + // regenerate the session, which is good practice to help + // guard against forms of session fixation + req.session.regenerate(function(err) { + if (err) { + return cb(err); + } + if (options.keepSessionInfo) { + merge(req.session, prevSession); + } + cb(); + }); + }); } diff --git a/lib/strategies/session.js b/lib/strategies/session.js index 92b57923..af8cc07c 100644 --- a/lib/strategies/session.js +++ b/lib/strategies/session.js @@ -1,15 +1,43 @@ -/** - * Module dependencies. - */ +// Module dependencies. var pause = require('pause') , util = require('util') , Strategy = require('passport-strategy'); /** - * `SessionStrategy` constructor. + * Create a new `SessionStrategy` object. + * + * An instance of this strategy is automatically used when creating an + * `{@link Authenticator}`. As such, it is typically unnecessary to create an + * instance using this constructor. + * + * @classdesc This `Strategy` authenticates HTTP requests based on the contents + * of session data. * - * @api public + * The login session must have been previously initiated, typically upon the + * user interactively logging in using a HTML form. During session initiation, + * the logged-in user's information is persisted to the session so that it can + * be restored on subsequent requests. + * + * Note that this strategy merely restores the authentication state from the + * session, it does not authenticate the session itself. Authenticating the + * underlying session is assumed to have been done by the middleware + * implementing session support. This is typically accomplished by setting a + * signed cookie, and verifying the signature of that cookie on incoming + * requests. + * + * In {@link https://expressjs.com/ Express}-based apps, session support is + * commonly provided by {@link https://github.com/expressjs/session `express-session`} + * or {@link https://github.com/expressjs/cookie-session `cookie-session`}. + * + * @public + * @class + * @augments base.Strategy + * @param {Object} [options] + * @param {string} [options.key='passport'] - Determines what property ("key") on + * the session data where login session data is located. The login + * session is stored and read from `req.session[key]`. + * @param {function} deserializeUser - Function which deserializes user. */ function SessionStrategy(options, deserializeUser) { if (typeof options == 'function') { @@ -19,36 +47,60 @@ function SessionStrategy(options, deserializeUser) { options = options || {}; Strategy.call(this); + + /** The name of the strategy, set to `'session'`. + * + * @type {string} + * @readonly + */ this.name = 'session'; + this._key = options.key || 'passport'; this._deserializeUser = deserializeUser; } -/** - * Inherit from `Strategy`. - */ +// Inherit from `passport.Strategy`. util.inherits(SessionStrategy, Strategy); /** - * Authenticate request based on the current session state. + * Authenticate request based on current session data. + * + * When login session data is present in the session, that data will be used to + * restore login state across across requests by calling the deserialize user + * function. + * + * If login session data is not present, the request will be passed to the next + * middleware, rather than failing authentication - which is the behavior of + * most other strategies. This deviation allows session authentication to be + * performed at the application-level, rather than the individual route level, + * while allowing both authenticated and unauthenticated requests and rendering + * responses accordingly. Routes that require authentication will need to guard + * that condition. * - * The session authentication strategy uses the session to restore any login - * state across requests. If a login session has been established, `req.user` - * will be populated with the current user. + * This function is protected, and should not be called directly. Instead, + * use `passport.authenticate()` middleware and specify the {@link SessionStrategy#name `name`} + * of this strategy and any options. * - * This strategy is registered automatically by Passport. + * @protected + * @param {http.IncomingMessage} req - The Node.js {@link https://nodejs.org/api/http.html#class-httpincomingmessage `IncomingMessage`} + * object. + * @param {Object} [options] + * @param {boolean} [options.pauseStream=false] - When `true`, data events on + * the request will be paused, and then resumed after the asynchronous + * `deserializeUser` function has completed. This is only necessary in + * cases where later middleware in the stack are listening for events, + * and ensures that those events are not missed. * - * @param {Object} req - * @param {Object} options - * @api protected + * @example + * passport.authenticate('session'); */ SessionStrategy.prototype.authenticate = function(req, options) { - if (!req._passport) { return this.error(new Error('passport.initialize() middleware not in use')); } + if (!req.session) { return this.error(new Error('Login sessions require session support. Did you forget to use `express-session` middleware?')); } options = options || {}; var self = this, su; - if (req._passport.session) { - su = req._passport.session.user; + if (req.session[this._key]) { + su = req.session[this._key].user; } if (su || su === 0) { @@ -60,10 +112,9 @@ SessionStrategy.prototype.authenticate = function(req, options) { this._deserializeUser(su, req, function(err, user) { if (err) { return self.error(err); } if (!user) { - delete req._passport.session.user; + delete req.session[self._key].user; } else { - // TODO: Remove instance access - var property = req._passport.instance._userProperty || 'user'; + var property = req._userProperty || 'user'; req[property] = user; } self.pass(); @@ -76,8 +127,5 @@ SessionStrategy.prototype.authenticate = function(req, options) { } }; - -/** - * Expose `SessionStrategy`. - */ +// Export `SessionStrategy`. module.exports = SessionStrategy; diff --git a/package.json b/package.json index e6d8b926..3bef6d3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passport", - "version": "0.4.1", + "version": "0.6.0", "description": "Simple, unobtrusive authentication for Node.js.", "keywords": [ "express", @@ -12,27 +12,32 @@ "author": { "name": "Jared Hanson", "email": "jaredhanson@gmail.com", - "url": "http://www.jaredhanson.net/" + "url": "https://www.jaredhanson.me/" }, - "homepage": "http://passportjs.org/", + "homepage": "https://www.passportjs.org/", "repository": { "type": "git", "url": "git://github.com/jaredhanson/passport.git" }, "bugs": { - "url": "http://github.com/jaredhanson/passport/issues" + "url": "https://github.com/jaredhanson/passport/issues" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" }, "license": "MIT", "licenses": [ { "type": "MIT", - "url": "http://opensource.org/licenses/MIT" + "url": "https://opensource.org/licenses/MIT" } ], "main": "./lib", "dependencies": { "passport-strategy": "1.x.x", - "pause": "0.0.1" + "pause": "0.0.1", + "utils-merge": "^1.0.1" }, "devDependencies": { "make-node": "0.3.x", diff --git a/sponsors/descope-dark.svg b/sponsors/descope-dark.svg new file mode 100644 index 00000000..fe58596d --- /dev/null +++ b/sponsors/descope-dark.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sponsors/descope.svg b/sponsors/descope.svg new file mode 100644 index 00000000..c21f203d --- /dev/null +++ b/sponsors/descope.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sponsors/fusionauth.png b/sponsors/fusionauth.png new file mode 100644 index 00000000..da471b83 Binary files /dev/null and b/sponsors/fusionauth.png differ diff --git a/sponsors/fusionauth.svg b/sponsors/fusionauth.svg new file mode 100644 index 00000000..da4b8f77 --- /dev/null +++ b/sponsors/fusionauth.svg @@ -0,0 +1,171 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sponsors/snyk.png b/sponsors/snyk.png new file mode 100644 index 00000000..b14de59b Binary files /dev/null and b/sponsors/snyk.png differ diff --git a/sponsors/workos.png b/sponsors/workos.png index 3afdfbae..514b691e 100644 Binary files a/sponsors/workos.png and b/sponsors/workos.png differ diff --git a/test/authenticator.middleware.test.js b/test/authenticator.middleware.test.js index 580759f4..a6d77ef0 100644 --- a/test/authenticator.middleware.test.js +++ b/test/authenticator.middleware.test.js @@ -35,22 +35,20 @@ describe('Authenticator', function() { expect(error).to.be.undefined; }); - it('should set user property on authenticator', function() { - expect(passport._userProperty).to.equal('user'); + it('should not set user property on request', function() { + expect(request._userProperty).to.be.undefined; }); it('should not initialize namespace within session', function() { expect(request.session.passport).to.be.undefined; }); - + it('should expose authenticator on internal request property', function() { expect(request._passport).to.be.an('object'); expect(request._passport.instance).to.be.an.instanceOf(Authenticator); expect(request._passport.instance).to.equal(passport); - }); - - it('should not expose session storage on internal request property', function() { - expect(request._passport.session).to.be.undefined; + expect(request._passport.instance._sm).to.be.an('object'); + expect(request._passport.instance._userProperty).to.equal('user'); }); }); @@ -75,22 +73,20 @@ describe('Authenticator', function() { expect(error).to.be.undefined; }); - it('should set user property on authenticator', function() { - expect(passport._userProperty).to.equal('currentUser'); + it('should set user property on request', function() { + expect(request._userProperty).to.equal('currentUser'); }); it('should not initialize namespace within session', function() { expect(request.session.passport).to.be.undefined; }); - + it('should expose authenticator on internal request property', function() { expect(request._passport).to.be.an('object'); expect(request._passport.instance).to.be.an.instanceOf(Authenticator); expect(request._passport.instance).to.equal(passport); - }); - - it('should not expose session storage on internal request property', function() { - expect(request._passport.session).to.be.undefined; + expect(request._passport.instance._sm).to.be.an('object'); + expect(request._passport.instance._userProperty).to.equal('currentUser'); }); }); @@ -278,8 +274,9 @@ describe('Authenticator', function() { req._passport = {}; req._passport.instance = {}; - req._passport.session = {}; - req._passport.session.user = '123456'; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '123456'; }) .next(function(err) { error = err; @@ -298,8 +295,8 @@ describe('Authenticator', function() { }); it('should maintain session', function() { - expect(request._passport.session).to.be.an('object'); - expect(request._passport.session.user).to.equal('123456'); + expect(request.session['passport']).to.be.an('object'); + expect(request.session['passport'].user).to.equal('123456'); }); }); diff --git a/test/http/request.test.js b/test/http/request.test.js index f268ee5e..d94dc9f2 100644 --- a/test/http/request.test.js +++ b/test/http/request.test.js @@ -1,14 +1,14 @@ /* global describe, it, expect, before */ /* jshint expr: true */ -var http = require('http') +var request = require('../../lib/http/request') , Passport = require('../..').Passport; -require('../../lib/framework/connect').__monkeypatchNode(); - describe('http.ServerRequest', function() { + // TODO: Test that these are extended by initialize/authenticate + /* describe('prototoype', function() { var req = new http.IncomingMessage(); @@ -30,16 +30,21 @@ describe('http.ServerRequest', function() { expect(req.isUnauthenticated).to.be.an('function'); }); }); + */ describe('#login', function() { describe('not establishing a session', function() { var passport = new Passport(); - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req._passport = {}; req._passport.instance = passport; - req._passport.session = {}; + req.session = {}; + req.session['passport'] = {}; var error; @@ -68,18 +73,22 @@ describe('http.ServerRequest', function() { }); it('should not serialize user', function() { - expect(req._passport.session.user).to.be.undefined; + expect(req.session['passport'].user).to.be.undefined; }); }); describe('not establishing a session and setting custom user property', function() { var passport = new Passport(); - passport._userProperty = 'currentUser'; - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req._passport = {}; req._passport.instance = passport; - req._passport.session = {}; + req.session = {}; + req.session['passport'] = {}; + req._userProperty = 'currentUser'; var error; @@ -112,17 +121,21 @@ describe('http.ServerRequest', function() { }); it('should not serialize user', function() { - expect(req._passport.session.user).to.be.undefined; + expect(req.session['passport'].user).to.be.undefined; }); }); describe('not establishing a session and invoked without a callback', function() { var passport = new Passport(); - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req._passport = {}; req._passport.instance = passport; - req._passport.session = {}; + req.session = {}; + req.session['passport'] = {}; var user = { id: '1', username: 'root' }; req.login(user, { session: false }); @@ -139,12 +152,15 @@ describe('http.ServerRequest', function() { }); it('should not serialize user', function() { - expect(req._passport.session.user).to.be.undefined; + expect(req.session['passport'].user).to.be.undefined; }); }); describe('not establishing a session, without passport.initialize() middleware', function() { - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; var error; @@ -179,10 +195,21 @@ describe('http.ServerRequest', function() { done(null, user.id); }); - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req._passport = {}; req._passport.instance = passport; - req._passport.session = {}; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(cb); + }; + process.nextTick(cb); + }; var error; @@ -204,6 +231,134 @@ describe('http.ServerRequest', function() { expect(req.isUnauthenticated()).to.be.false; }); + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + + it('should set user', function() { + expect(req.user).to.be.an('object'); + expect(req.user.id).to.equal('1'); + expect(req.user.username).to.equal('root'); + }); + + it('should serialize user', function() { + expect(req.session['passport'].user).to.equal('1'); + }); + }); + + describe('establishing a session and not keeping previous session data', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { cart: [ '1', '2', ] }; + Object.defineProperty(req.session, 'id', { value: '1' }); + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(cb); + }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should be authenticated', function() { + expect(req.isAuthenticated()).to.be.true; + expect(req.isUnauthenticated()).to.be.false; + }); + + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + + it('should not keep session data', function() { + expect(req.session.cart).to.be.undefined; + }); + + it('should set user', function() { + expect(req.user).to.be.an('object'); + expect(req.user.id).to.equal('1'); + expect(req.user.username).to.equal('root'); + }); + + it('should serialize user', function() { + expect(req.session['passport'].user).to.equal('1'); + }); + }); + + describe('establishing a session and keeping previous session data', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { cart: [ '1', '2', ] }; + Object.defineProperty(req.session, 'id', { value: '1' }); + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(cb); + }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, { keepSessionInfo: true }, function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should be authenticated', function() { + expect(req.isAuthenticated()).to.be.true; + expect(req.isUnauthenticated()).to.be.false; + }); + + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + + it('should keep session data', function() { + expect(req.session.cart).to.deep.equal([ '1', '2' ]); + }); + it('should set user', function() { expect(req.user).to.be.an('object'); expect(req.user.id).to.equal('1'); @@ -211,7 +366,7 @@ describe('http.ServerRequest', function() { }); it('should serialize user', function() { - expect(req._passport.session.user).to.equal('1'); + expect(req.session['passport'].user).to.equal('1'); }); }); @@ -220,12 +375,23 @@ describe('http.ServerRequest', function() { passport.serializeUser(function(user, done) { done(null, user.id); }); - passport._userProperty = 'currentUser'; - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req._passport = {}; req._passport.instance = passport; - req._passport.session = {}; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(cb); + }; + process.nextTick(cb); + } + req._userProperty = 'currentUser'; var error; @@ -247,6 +413,10 @@ describe('http.ServerRequest', function() { expect(req.isUnauthenticated()).to.be.false; }); + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + it('should not set user', function() { expect(req.user).to.be.undefined; }); @@ -258,7 +428,62 @@ describe('http.ServerRequest', function() { }); it('should serialize user', function() { - expect(req._passport.session.user).to.equal('1'); + expect(req.session['passport'].user).to.equal('1'); + }); + }); + + describe('encountering an error when regenerating session', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session.regenerate = function(cb) { + process.nextTick(function(){ + cb(new Error('something went wrong')); + }) + } + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('something went wrong'); + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should not regenerate session', function() { + expect(req.session.id).to.equal('1'); + }); + + it('should not set user', function() { + expect(req.user).to.be.null; + }); + + it('should not serialize user', function() { + expect(req.session['passport'].user).to.be.undefined; }); }); @@ -268,10 +493,19 @@ describe('http.ServerRequest', function() { done(new Error('something went wrong')); }); - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req._passport = {}; req._passport.instance = passport; - req._passport.session = {}; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + } var error; @@ -294,17 +528,82 @@ describe('http.ServerRequest', function() { expect(req.isUnauthenticated()).to.be.true; }); + it('should regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + it('should not set user', function() { expect(req.user).to.be.null; }); it('should not serialize user', function() { - expect(req._passport.session.user).to.be.undefined; + expect(req.session['passport']).to.be.undefined; }); }); + describe('encountering an error when saving session', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + req.session.save = function(cb) { + process.nextTick(function(){ + cb(new Error('something went wrong')); + }); + }; + process.nextTick(cb); + } + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('something went wrong'); + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should not regenerate session', function() { + expect(req.session.id).to.equal('2'); + }); + + it('should not set user', function() { + expect(req.user).to.be.null; + }); + + it('should not serialize user', function() { + expect(req.session['passport'].user).to.equal('1'); + }); + }); + + /* describe('establishing a session, without passport.initialize() middleware', function() { - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; var user = { id: '1', username: 'root' }; it('should throw an exception', function() { @@ -313,6 +612,7 @@ describe('http.ServerRequest', function() { }).to.throw(Error, 'passport.initialize() middleware not in use'); }); }); + */ describe('establishing a session, but not passing a callback argument', function() { var passport = new Passport(); @@ -320,10 +620,13 @@ describe('http.ServerRequest', function() { done(null, user.id); }); - var req = new http.IncomingMessage(); + var req = new Object(); + req.login = request.login; req._passport = {}; req._passport.instance = passport; - req._passport.session = {}; + req._sessionManager = passport._sm; + req.session = {}; + req.session['passport'] = {}; var user = { id: '1', username: 'root' }; @@ -334,6 +637,35 @@ describe('http.ServerRequest', function() { }); }); + describe('establishing a session without session support', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.login = request.login; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + + var error; + + before(function(done) { + var user = { id: '1', username: 'root' }; + + req.login(user, function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('Login sessions require session support. Did you forget to use `express-session` middleware?'); + }); + }); + }); @@ -342,14 +674,144 @@ describe('http.ServerRequest', function() { describe('existing session', function() { var passport = new Passport(); - var req = new http.IncomingMessage(); + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req.user = { id: '1', username: 'root' }; req._passport = {}; req._passport.instance = passport; - req._passport.session = {}; - req._passport.session.user = '1'; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; - req.logout(); + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + + it('should clear serialized user', function() { + expect(req.session['passport']).to.be.undefined; + }); + }); + + describe('existing session and not keeping session data', function() { + var passport = new Passport(); + + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { cart: [ '1', '2', ] }; + Object.defineProperty(req.session, 'id', { value: '1' }); + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + + it('should clear serialized user', function() { + expect(req.session['passport']).to.be.undefined; + }); + + it('should keep session data', function() { + expect(req.session.cart).to.be.undefined; + }); + }); + + describe('existing session and keeping session data', function() { + var passport = new Passport(); + + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { cart: [ '1', '2', ] }; + Object.defineProperty(req.session, 'id', { value: '1' }); + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + req.logout({ keepSessionInfo: true }, function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); it('should not be authenticated', function() { expect(req.isAuthenticated()).to.be.false; @@ -361,22 +823,50 @@ describe('http.ServerRequest', function() { }); it('should clear serialized user', function() { - expect(req._passport.session.user).to.be.undefined; + expect(req.session['passport'].user).to.be.undefined; + }); + + it('should keep session data', function() { + expect(req.session.cart).to.deep.equal([ '1', '2' ]); }); }); describe('existing session and clearing custom user property', function() { var passport = new Passport(); - var req = new http.IncomingMessage(); + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req.currentUser = { id: '1', username: 'root' }; req._passport = {}; req._passport.instance = passport; - req._passport.instance._userProperty = 'currentUser'; - req._passport.session = {}; - req._passport.session.user = '1'; + req._userProperty = 'currentUser'; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; - req.logout(); + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); it('should not be authenticated', function() { expect(req.isAuthenticated()).to.be.false; @@ -388,12 +878,15 @@ describe('http.ServerRequest', function() { }); it('should clear serialized user', function() { - expect(req._passport.session.user).to.be.undefined; + expect(req.session['passport']).to.be.undefined; }); }); describe('existing session, without passport.initialize() middleware', function() { - var req = new http.IncomingMessage(); + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req.user = { id: '1', username: 'root' }; req.logout(); @@ -408,13 +901,199 @@ describe('http.ServerRequest', function() { }); }); + describe('existing session, without passport.initialize() middleware, and invoked with a callback', function() { + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + }); + + describe('encountering an error saving existing session', function() { + var passport = new Passport(); + + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(function() { + cb(new Error('something went wrong')); + }); + }; + req.session.regenerate = function(cb) { + req.session = { id: '2' }; + process.nextTick(cb); + }; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('something went wrong'); + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + + it('should clear serialized user', function() { + expect(req.session['passport'].user).to.be.undefined; + }); + }); + + describe('encountering an error regenerating session', function() { + var passport = new Passport(); + + var req = new Object(); + req.logout = request.logout; + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; + req.user = { id: '1', username: 'root' }; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = { id: '1' }; + req.session['passport'] = {}; + req.session['passport'].user = '1'; + req.session.save = function(cb) { + expect(req.session['passport'].user).to.be.undefined; + process.nextTick(cb); + }; + req.session.regenerate = function(cb) { + process.nextTick(function() { + cb(new Error('something went wrong')); + }); + }; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('something went wrong'); + }); + + it('should not be authenticated', function() { + expect(req.isAuthenticated()).to.be.false; + expect(req.isUnauthenticated()).to.be.true; + }); + + it('should clear user', function() { + expect(req.user).to.be.null; + }); + + it('should clear serialized user', function() { + expect(req.session['passport'].user).to.be.undefined; + }); + }); + + describe('existing session, but not passing a callback argument', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.logout = request.logout; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '1'; + + it('should throw an exception', function() { + expect(function() { + req.logout(); + }).to.throw(Error, 'req#logout requires a callback function'); + }); + }); + + describe('without session support', function() { + var passport = new Passport(); + passport.serializeUser(function(user, done) { + done(null, user.id); + }); + + var req = new Object(); + req.logout = request.logout; + req._passport = {}; + req._passport.instance = passport; + req._sessionManager = passport._sm; + + var error; + + before(function(done) { + req.logout(function(err) { + error = err; + done(); + }); + }); + + it('should error', function() { + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.equal('Login sessions require session support. Did you forget to use `express-session` middleware?'); + }); + }); + }); describe('#isAuthenticated', function() { describe('with a user', function() { - var req = new http.IncomingMessage(); + var req = new Object(); + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req.user = { id: '1', username: 'root' }; it('should be authenticated', function() { @@ -424,11 +1103,13 @@ describe('http.ServerRequest', function() { }); describe('with a user set on custom property', function() { - var req = new http.IncomingMessage(); + var req = new Object(); + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req.currentUser = { id: '1', username: 'root' }; req._passport = {}; req._passport.instance = {}; - req._passport.instance._userProperty = 'currentUser'; + req._userProperty = 'currentUser'; it('should be authenticated', function() { expect(req.isAuthenticated()).to.be.true; @@ -437,7 +1118,9 @@ describe('http.ServerRequest', function() { }); describe('without a user', function() { - var req = new http.IncomingMessage(); + var req = new Object(); + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; it('should not be authenticated', function() { expect(req.isAuthenticated()).to.be.false; @@ -446,7 +1129,9 @@ describe('http.ServerRequest', function() { }); describe('with a null user', function() { - var req = new http.IncomingMessage(); + var req = new Object(); + req.isAuthenticated = request.isAuthenticated; + req.isUnauthenticated = request.isUnauthenticated; req.user = null; it('should not be authenticated', function() { diff --git a/test/middleware/initialize.test.js b/test/middleware/initialize.test.js index 073c4d0e..b572f93b 100644 --- a/test/middleware/initialize.test.js +++ b/test/middleware/initialize.test.js @@ -36,10 +36,8 @@ describe('middleware/initialize', function() { expect(request._passport).to.be.an('object'); expect(request._passport.instance).to.be.an.instanceOf(Passport); expect(request._passport.instance).to.equal(passport); - }); - - it('should not expose empty object as session storage on internal request property', function() { - expect(request._passport.session).to.be.undefined; + expect(request._passport.instance._sm).to.be.an('object'); + expect(request._passport.instance._userProperty).to.equal('user'); }); }); @@ -73,10 +71,8 @@ describe('middleware/initialize', function() { expect(request._passport).to.be.an('object'); expect(request._passport.instance).to.be.an.instanceOf(Passport); expect(request._passport.instance).to.equal(passport); - }); - - it('should not expose session storage on internal request property', function() { - expect(request._passport.session).to.be.undefined; + expect(request._passport.instance._sm).to.be.an('object'); + expect(request._passport.instance._userProperty).to.equal('user'); }); }); @@ -114,12 +110,8 @@ describe('middleware/initialize', function() { expect(request._passport).to.be.an('object'); expect(request._passport.instance).to.be.an.instanceOf(Passport); expect(request._passport.instance).to.equal(passport); - }); - - it('should expose session storage on internal request property', function() { - expect(request._passport.session).to.be.an('object'); - expect(Object.keys(request._passport.session)).to.have.length(1); - expect(request._passport.session.user).to.equal('123456'); + expect(request._passport.instance._sm).to.be.an('object'); + expect(request._passport.instance._userProperty).to.equal('user'); }); }); @@ -158,12 +150,39 @@ describe('middleware/initialize', function() { expect(request._passport).to.be.an('object'); expect(request._passport.instance).to.be.an.instanceOf(Passport); expect(request._passport.instance).to.equal(passport); + expect(request._passport.instance._sm).to.be.an('object'); + expect(request._passport.instance._userProperty).to.equal('user'); + }); + }); + + describe('handling a request with a new session without compat mode', function() { + var passport = new Passport(); + var request, error; + + before(function(done) { + chai.connect.use(initialize(passport, { compat: false })) + .req(function(req) { + request = req; + + req.session = {}; + }) + .next(function(err) { + error = err; + done(); + }) + .dispatch(); + }); + + it('should not error', function() { + expect(error).to.be.undefined; + }); + + it('should not initialize namespace within session', function() { + expect(request.session.passport).to.be.undefined; }); - it('should expose session storage on internal request property', function() { - expect(request._passport.session).to.be.an('object'); - expect(Object.keys(request._passport.session)).to.have.length(1); - expect(request._passport.session.user).to.equal('123456'); + it('should expose authenticator on internal request property', function() { + expect(request._passport).to.be.undefined; }); }); diff --git a/test/strategies/session.pause.test.js b/test/strategies/session.pause.test.js index 60f80d60..20b6e07b 100644 --- a/test/strategies/session.pause.test.js +++ b/test/strategies/session.pause.test.js @@ -33,8 +33,9 @@ describe('SessionStrategy', function() { req._passport = {}; req._passport.instance = {}; - req._passport.session = {}; - req._passport.session.user = '123456'; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '123456'; }) .authenticate({ pauseStream: true }); }); @@ -53,8 +54,8 @@ describe('SessionStrategy', function() { }); it('should maintain session', function() { - expect(request._passport.session).to.be.an('object'); - expect(request._passport.session.user).to.equal('123456'); + expect(request.session['passport']).to.be.an('object'); + expect(request.session['passport'].user).to.equal('123456'); }); it('should pause request', function() { @@ -95,8 +96,9 @@ describe('SessionStrategy', function() { req._passport = {}; req._passport.instance = {}; - req._passport.session = {}; - req._passport.session.user = '123456'; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '123456'; }) .authenticate({ pauseStream: true }); }); @@ -114,8 +116,8 @@ describe('SessionStrategy', function() { }); it('should remove user from session', function() { - expect(request._passport.session).to.be.an('object'); - expect(request._passport.session.user).to.be.undefined; + expect(request.session['passport']).to.be.an('object'); + expect(request.session['passport'].user).to.be.undefined; }); it('should pause request', function() { diff --git a/test/strategies/session.test.js b/test/strategies/session.test.js index 366a2b80..15b174ab 100644 --- a/test/strategies/session.test.js +++ b/test/strategies/session.test.js @@ -26,7 +26,8 @@ describe('SessionStrategy', function() { request = req; req._passport = {}; - req._passport.session = {}; + req.session = {}; + req.session['passport'] = {}; }) .authenticate(); }); @@ -58,8 +59,9 @@ describe('SessionStrategy', function() { req._passport = {}; req._passport.instance = {}; - req._passport.session = {}; - req._passport.session.user = '123456'; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '123456'; }) .authenticate(); }); @@ -74,8 +76,8 @@ describe('SessionStrategy', function() { }); it('should maintain session', function() { - expect(request._passport.session).to.be.an('object'); - expect(request._passport.session.user).to.equal('123456'); + expect(request.session['passport']).to.be.an('object'); + expect(request.session['passport'].user).to.equal('123456'); }); }); @@ -97,8 +99,9 @@ describe('SessionStrategy', function() { req._passport = {}; req._passport.instance = {}; - req._passport.session = {}; - req._passport.session.user = 0; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = 0; }) .authenticate(); }); @@ -113,8 +116,8 @@ describe('SessionStrategy', function() { }); it('should maintain session', function() { - expect(request._passport.session).to.be.an('object'); - expect(request._passport.session.user).to.equal(0); + expect(request.session['passport']).to.be.an('object'); + expect(request.session['passport'].user).to.equal(0); }); }); @@ -136,8 +139,9 @@ describe('SessionStrategy', function() { req._passport = {}; req._passport.instance = {}; - req._passport.session = {}; - req._passport.session.user = '123456'; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '123456'; }) .authenticate(); }); @@ -151,8 +155,8 @@ describe('SessionStrategy', function() { }); it('should remove user from session', function() { - expect(request._passport.session).to.be.an('object'); - expect(request._passport.session.user).to.be.undefined; + expect(request.session['passport']).to.be.an('object'); + expect(request.session['passport'].user).to.be.undefined; }); }); @@ -174,9 +178,10 @@ describe('SessionStrategy', function() { req._passport = {}; req._passport.instance = {}; - req._passport.instance._userProperty = 'currentUser'; - req._passport.session = {}; - req._passport.session.user = '123456'; + req._userProperty = 'currentUser'; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '123456'; }) .authenticate(); }); @@ -213,8 +218,9 @@ describe('SessionStrategy', function() { req._passport = {}; req._passport.instance = {}; - req._passport.session = {}; - req._passport.session.user = '123456'; + req.session = {}; + req.session['passport'] = {}; + req.session['passport'].user = '123456'; }) .authenticate(); }); @@ -229,8 +235,8 @@ describe('SessionStrategy', function() { }); it('should maintain session', function() { - expect(request._passport.session).to.be.an('object'); - expect(request._passport.session.user).to.equal('123456'); + expect(request.session['passport']).to.be.an('object'); + expect(request.session['passport'].user).to.equal('123456'); }); }); @@ -251,7 +257,7 @@ describe('SessionStrategy', function() { it('should error', function() { expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.equal('passport.initialize() middleware not in use'); + expect(error.message).to.equal('Login sessions require session support. Did you forget to use `express-session` middleware?'); }); it('should not set user on request', function() {