Socket
Socket
Sign inDemoInstall

flowstate

Package Overview
Dependencies
9
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.5.1 to 0.6.0

etc/jsdoc.json

24

CHANGELOG.md

@@ -9,2 +9,24 @@ # Changelog

## [0.6.0] - 2023-10-18
TODO: Review this for accuracy.
### Added
- Added third `preventReturnTo` argument to `res.redirect` (or second, when
optional `status` is not supplied) that prevents adding the `return_to` query
parameter to the URL. Useful when the URL includes that a different parameter
set to that value.
### Changed
- Renamed `resumeState` property of state objects to `state`.
- Initializing `returnTo` property of current state in all cases. This avoids
needing to reference the `location` property of the state to resume,
optimizing state store access and allowing state targetting different
locations to be stored in different stores.
- Both `returnTo` and `state` parameters are added as query parameters when
redirecting.
- Both `returnTo` and `state` variables are set as locals when rendering.
- Both `returnTo` and `state` are set when pushing state.
## [0.5.1] - 2021-12-13

@@ -15,3 +37,3 @@ ### Added

[Unreleased]: https://github.com/jaredhanson/flowstate/compare/v0.5.0...HEAD
[Unreleased]: https://github.com/jaredhanson/flowstate/compare/v0.5.1...HEAD
[0.5.1]: https://github.com/jaredhanson/flowstate/compare/v0.5.0...v0.5.1

@@ -0,1 +1,12 @@

/**
* flowstate is....
*
* @module flowstate
*/
/*
* State middleware
*
* @type {function}
*/
exports = module.exports = require('./middleware/state');

@@ -2,0 +13,0 @@ exports.clean = require('./middleware/clean');

3

lib/meta.js
var Meta = module.exports = function Meta() {
this.originalHash = null;
this.savedHash = null;
this.complete = false;
//this.complete = false;
this.destroyed = false;
this.returning = false;
}

@@ -7,0 +8,0 @@

@@ -15,2 +15,10 @@ var SessionStore = require('../store/session');

// TODO: clean this up
states.forEach(function(state) {
if (typeof state.expires === 'string') {
// convert expires to a Date object
state.expires = new Date(state.expires);
}
})
var evict = 0;

@@ -17,0 +25,0 @@ if (limit) { evict = states.length - limit; }

@@ -0,1 +1,2 @@

// Module dependencies.
var uri = require('url')

@@ -10,2 +11,3 @@ , State = require('../state')

, debug = require('debug')('flowstate')
, qs = require('querystring')
, SessionStore = require('../store/session');

@@ -15,8 +17,4 @@

// TIPS: Generally, push a state before redirecting (unless wizard style
// alternatively, handler can lazily push, for its own state, to preserve storage
// which could be done based on the presence of a returnTo (which is not the handlers URL)
/**
* Load state.
* Create state middleware with the given `options`.
*

@@ -33,24 +31,26 @@ * This middleware is used to load state associated with a request. The state

*
* Options:
* @public
* @name module:flowstate
* @param {Object} [options]
* @param {string[]} [options.mutationMethods=['POST', 'PUT', 'PATCH', 'DELETE']] -
* An array of methods for which request handling is expected to
* complete a state.
* @param {function} [options.getHandle] - Function that the middleware will
* invoke to read the state handle from the request. The function is
* called as `getHandle(req)` and is expected to return the handle as a
* string.
*
* The default value is a function that reads the handle from the following
* locations, in order:
*
* name set req.state only if the state name is equal to the option value
* required require state, failing otherwise (default: false)
*
* Examples:
*
* app.get('/login',
* flowstate.loadState(),
* function(req, res, next) {
* res.locals.failureCount = req.state.failureCount;
* res.render('login');
* });
*
* app.get('/login/callback',
* flowstate.loadState({ name: 'oauth2-redirect', required: true }),
* function(req, res, next) {
* // ...
* });
*
* @return {Function}
* @api public
* - `req.query.state` - a built-in from Express.js to read from the URL query
* string.
* - `req.body.state` - typically generated by the `body-parser` module.
* @param {function} [options.genh] - Function to call to generate a new session
* handle. Provide a function that returns a string that will be used
* as a session handle. The default value is a function which uses the
* `uid2` library to generate IDs.
* @param {Store} [options.store] - The state store instance, defaults to a new
* `{@link SessionStore}` instance.
* @return {function}
*/

@@ -61,2 +61,3 @@ module.exports = function(options) {

var store = options.store || new SessionStore(options);
// TODO: Rename this to `handle` to match csurf conventions
var getHandle = options.getHandle || function(req) {

@@ -71,2 +72,4 @@ return (req.query && req.query.state) || (req.body && req.body.state);

: options.mutationMethods;
// TODO: add back support for a `required` options, which forces a matching state targetting location

@@ -80,2 +83,11 @@ return function state(req, res, next) {

/**
* Add state to be made available on a request for `url`.
*
* @public
* @name ServerResponse#pushState
* @function
* @param {object} data
* @param {string} url
*/
req.pushState = function(data, url, options, cb) {

@@ -88,8 +100,22 @@ if (typeof options == 'function') {

/*
var immediate = false;
if (typeof options == 'boolean') {
immediate = true;
options = {};
}
*/
// TODO: only support handle as option?
data.location = uri.resolve(utils.originalURL(req), url);
// TODO: Should this be taking the top of the state stack?? In case multiple pushes?
if (req.state.returnTo && !req.state.external) {
data.returnTo = req.state.returnTo;
} else if (req.state.resumeState) {
data.resumeState = req.state.resumeState;
}
if (req.state.state) {
data.state = req.state.state;
}

@@ -100,2 +126,3 @@ var state = new State(req, data);

if (cb) {
// TODO: Why this typeof check?
if (typeof store.set == 'function') { state.handle = options.handle || generateHandle(); }

@@ -107,4 +134,13 @@ state.save(options, function(err) {

}
// TODO: maybe return the new state here, add methods for controlling params
}
/**
* Remove a previously added, but uncommited, state.
*
* @public
* @name ServerResponse#popState
* @function
*/
req.popState = function() {

@@ -115,2 +151,10 @@ if (req._stateStack.length == 1) { return; } // don't pop current state

/**
* Resume prior state, if any, by redirecting its URL.
*
* @public
* @name ServerResponse#resumeState
* @function
* @param {object} [yields]
*/
res.resumeState = function(yields, cb) {

@@ -124,26 +168,84 @@ if (typeof yields == 'function') {

if (req.state.returnTo) {
// TODO: underscore the parameters, adn redirect with query
this.redirect(req.state.returnTo);
} else if (req.state.resumeState) {
var self = this;
store.get(req, req.state.resumeState, function(err, data) {
if (err) { return cb(err); }
var state = new State(req, data, req.state.resumeState);
merge(state, yields);
req._stateStack.push(state);
self.redirect(state.location);
});
} else {
cb();
if (!req.state.returnTo) { return cb(); }
var loc = uri.parse(req.state.returnTo, true);
delete loc.search;
// TODO: test case for preserving query params
if (yields) {
merge(loc.query, yields);
}
req.state.returning();
this.redirect(uri.format(loc));
}
/**
* Redirect to the given `url` with optional response `status` defaulting to
* 302.
*
* This function calls {@link https://expressjs.com/en/4x/api.html#res.redirect `redirect`},
* which is enhanced by {@link https://expressjs.com/ Express} on {@link https://nodejs.org/ Node.js}'s
* {@link https://nodejs.org/api/http.html#http_class_http_serverresponse `http.ServerResponse`}.
*
* Prior to redirecting, any uncommited state is commited. Either a
* `return_to` or `state` query parameter will be added to the given `url`
* to represent the state. The state is then available to the ensuing
* request to `url`.
*
* @public
* @name ServerResponse#redirect
* @function
* @param {number} [status=302]
* @param {string} url
*/
// swizzle redirect to commit the state
swizzleRedirect(res, function(url, cb) {
if ((mutationMethods.indexOf(req.method) != -1) && !req.state.isModified()) {
if ((mutationMethods.indexOf(req.method) != -1) && req.state.isComplete() === undefined) {
req.state.complete();
}
commit2(function(err, nstate) {
if (err) { return next(err); }
var l = uri.parse(url, true);
delete l.search;
if (nstate) {
var returnTo = nstate.location;
var state = nstate.handle;
if (!returnTo && !state) { return cb(); }
// Don't put a return_to on a external link
// TODO: rename so to validOrigin
var so = returnTo && validate.origin(uri.resolve(returnTo, url), req);
if (returnTo && so) {
l.query.return_to = returnTo;
}
if (state) { l.query.state = state; }
return cb(null, uri.format(l));
}
if (!req.state.handle) {
if (req.state.external && req.state.isComplete()) {
// TODO: clean this up to remove this empty branch
} else if (req.state.returnTo) {
if (uri.resolve(req.state.returnTo, url) !== req.state.returnTo && !req.state.isReturning()) {
l.query.return_to = req.state.returnTo;
}
if (req.state.state) { l.query.state = req.state.state; }
}
} else {
// TODO: what is correct here?
var currentlyAt = utils.originalURLWithoutState(req);
//var currentlyAt = utils.originalURLWithoutQuery(req);
if (uri.resolve(currentlyAt, url) !== currentlyAt) {
l.query.return_to = currentlyAt;
}
l.query.state = req.state.handle;
}
return cb(null, uri.format(l));
});
/*
commit(function(err, returnTo, state) {

@@ -155,11 +257,35 @@ if (err) { return next(err); }

delete l.search;
if (returnTo) { l.query.return_to = returnTo; }
else if (state) { l.query.state = state; }
if (returnTo && !preventReturnTo) { l.query.return_to = returnTo; }
if (state) { l.query.state = state; }
return cb(null, uri.format(l));
});
*/
});
/**
* Render `view` with the given `options` and optional `callback`. When a
* callback function is given a response will _not_ be made automatically,
* otherwise a response of _200_ and _text/html_ is given.
*
* This function calls {@link https://expressjs.com/en/4x/api.html#res.render `render`},
* which is enhanced by {@link https://expressjs.com/ Express} on {@link https://nodejs.org/ Node.js}'s
* {@link https://nodejs.org/api/http.html#http_class_http_serverresponse `http.ServerResponse`}.
*
* Prior to redirecting, any uncommited state is commited. Either a
* `return_to` or `state` variable will be set on {@link https://expressjs.com/en/4x/api.html#res.locals locals}
* to represent the state. The view can then make the state available to
* subsequent requests initiated via links, forms, or other methods.
*
* @public
* @name ServerResponse#render
* @function
* @param {string} view
* @param {object} [options]
* @param {function} [callback]
*/
// swizzle render to commit the state
swizzleRender(res, function(cb) {
// TODO: dont' complete the state if it is flagged to continue
// TODO: Check if the handler explicilty completed before doing this, add test case
if ((mutationMethods.indexOf(req.method) != -1) && (Math.floor(res.statusCode / 100) == 2)) {

@@ -169,2 +295,47 @@ req.state.complete();

commit2(function(err, nstate) {
if (err) { return next(err); } // FIXME: This should call cb?
if (nstate) {
console.log('TODO: handle push state on render');
return;
}
// TODO: handle case where there is an nstate (via pushState)
if (!req.state.handle) {
// The request handler is rendering without setting any state that
// was persisted for subsequent requests. This typically occurs under
// two situations:
//
// 1. State was initialized to eventually redirect to a URL with
// (optional) state using `return_to` and (optional) `state`
// parameters. No modifications were made to this initial state.
// 2. Previously persisted state intended for this endpoint was
// loaded using `state` parameter. Processing of this this state
// was completed.
//
// In either case, the current state set at `req.state` is complete,
// and the location (and state) to eventually redirect to needs to be
// propagated to the next request. Propagation is done by setting
// `returnTo` and `state` properties on `res.locals`. It is expected
// that the view being rendered will add these values to any necessary
// links and/or forms to ensure the state flows through subsequent
// user interactions.
if (req.state.returnTo) {
res.locals.returnTo = req.state.returnTo;
// TODO: test case for not populating initial state with an invalid state
// when return_to is not present.
if (req.state.state) { res.locals.state = req.state.state; }
}
} else {
// Processing of this request created state
// TODO: document this more
res.locals.state = req.state.handle;
}
return cb();
});
/*
commit(function(err, returnTo, state) {

@@ -174,9 +345,10 @@ if (err) { return next(err); } // FIXME: This should call cb?

if (returnTo) { res.locals.returnTo = returnTo; }
else if (state) { res.locals.state = state; }
if (state) { res.locals.state = state; }
cb();
});
*/
});
swizzleEnd(res, function(cb) {
commit(function() {
commit2(function() {
cb();

@@ -187,16 +359,21 @@ });

function generate() {
// To doc: a new state will have "returnTo" and "state", Such a state doesn't
// need to be serialized, since it can be passed as URL parameters.
function generate(url, state) {
var data = {
location: utils.originalURLWithoutQuery(req)
location: url
};
var returnTo = (options.external ? utils.originalURL(req) : undefined)
|| (req.query && req.query.return_to)
|| (req.body && req.body.return_to)
|| (req.header ? req.header('referer') : undefined);
if (returnTo) {
var valid = validateRedirect(returnTo, req);
if (valid) {
data.returnTo = returnTo;
}
// TODO: clean this up and handle external states in a clear condition
// TODO: add tests for preserving query params
// TODO: test case for not adding state or parsing params when external
var returnTo = options.external ? utils.originalURL(req)
: ((req.query && req.query.return_to) ||
(req.body && req.body.return_to) ||
(req.header ? req.header('referer') : undefined));
if (returnTo && validateRedirect(returnTo, req)) {
data.returnTo = returnTo;
if (state && !options.external) { data.state = state; }
}

@@ -208,7 +385,47 @@

function inflate(state, h) {
req.state = new State(req, state, h);
function inflate(data, h) {
req.state = new State(req, data, h);
req._stateStack = [ req.state ];
}
// TODO: dedupe this with commit below, if the same function works in all places
function commit2(cb) {
var stack = req._stateStack
, i = 0;
function iter(err, nstate) {
if (err) { return cb(err); } // TODO: Test case for this
var state = stack[i++];
if (!state) { return cb(null, i == 2 ? undefined : nstate); }
if (state.isComplete()) {
debug('destroying %O (%s)', state, state.handle);
// TODO: optimization: don't call destroy if its not actually persisted (doesn't have. ahandle)
state.destroy(function(err) {
if (err) { return iter(err); }
debug('destroyed');
iter(null, state);
});
} else if (state.isModified() || (i > 1 && !state.isSaved())) {
// FIXME: the i>1 condition gets pushed states (whcih are not modified but need saving)
//. make this more obvious by adding a method to check
if (!state.handle && (typeof store.set == 'function')) {
state.handle = generateHandle();
}
state.save(function(err) {
if (err) { return iter(err); }
debug('saved (%s)', state.handle);
iter(null, state);
});
} else {
iter(null, state);
}
}
iter();
}
// TODO: dead code, can be removed
/*
function commit(cb) {

@@ -229,3 +446,3 @@ var stack = req._stateStack

debug('destroyed');
iter(null, undefined, state.resumeState);
iter(null, state.returnTo, state.state);
});

@@ -240,7 +457,10 @@ } else if (state.isModified() || (i > 1 && !state.isSaved())) {

debug('saved (%s)', state.handle);
iter(null, undefined, state.handle);
iter(null, state.location, state.handle);
});
} else if (state.isNew()) {
iter(null, state.returnTo, state.resumeState);
iter(null, state.returnTo, state.state);
} else { // current
if (state.isContinue()) {
return iter(null, state.location, state.handle);
}
iter(null, undefined, state.handle);

@@ -251,16 +471,20 @@ }

}
*/
// Initialize a state if the browser didn't send a state handle, or the
// endpoint indicated that state, if any, is external to this app.
var url = utils.originalURLWithoutQuery(req);
var h = getHandle(req);
// Create a new "uninitialized" state if the request didn't send a state
// handle, or the endpoint is configured to consider any state as external
// to this application.
//
// The latter case is common with federated protocols, such as OpenID
// Connect and OAuth 2.0, in which clients make requests that initiate
// stateful transactions within the app making use of this middleware.
// These requests may have a `state` parameter, but its value is meant to be
// included when the browser is redirected back to the client so the client
// can maintain its state.
var h = getHandle(req);
// External state is typically relayed by third-party applications when
// making a request as part of a federated protocol, such as OAuth 2.0 or
// OpenID Connect. In such cases, the `state` parameter does not indentify
// state associated with this application, but rather a parameter that is
// expected to be echo'd back to the calling application so that it can
// subsequently load state when the user is redirected back.
if (!h || options.external) {
generate();
generate(url);
next();

@@ -270,7 +494,16 @@ return;

// TODO: can optimize this by not getting state when return_to parameter is present
//. YES, likely a good idea
// Attempt to load any previously serialized state identified by the state
// handle.
store.get(req, h, function(err, state) {
if (err) { return next(err); }
// No previously serialized state was found for the given state handle.
// Ignore the parameter and create a new "uninitialized" state. The
// uninitialized state will preserve the state handle if it is intended
// to be passed as a parameter to the redirect URL, if any.
if (!state) {
generate();
generate(url, h);
next();

@@ -280,19 +513,13 @@ return;

var url = utils.originalURLWithoutQuery(req);
if (state.location !== url) {
// The referened state is not intended for the endpoint that was
// requested.
//
// A new state will be initialized which, when complete, will trigger
// the referenced state to resume. This process is managed by building
// a logical stack of states as the browser is redirected or navigated
// to a series of pages, where a reference to the current state is
// relayed with each request. Once a request is processed which does
// not result in a further redirect, the stack unwinds by popping the
// state to be resumed.
inflate({ location: url, resumeState: h });
} else {
inflate(state, h);
// The loaded state is not intended for this endpoint. Ignore the state
// and create a new "uninitialized" state. The uninitialized state will
// preserve the state handle if it is intended to be passed as a
// parameter to the redirect URL, if any.
generate(url, h);
next();
return;
}
inflate(state, h);
next();

@@ -299,0 +526,0 @@ });

@@ -0,1 +1,2 @@

// Module dependencies.
var Meta = require('./meta')

@@ -5,2 +6,8 @@ , crc = require('crc').crc32;

/**
* Create a new `State` with the given request and `data`.
*
* @protected
* @class
*/
function State(req, data, handle, external) {

@@ -12,2 +19,6 @@ Object.defineProperty(this, '_req', { value: req });

if (data.handle) {
this.handle = data.handle;
}
if (typeof data === 'object' && data !== null) {

@@ -22,2 +33,7 @@ // merge data into this, ignoring prototype properties

if (typeof this.expires === 'string') {
// convert expires to a Date object
this.expires = new Date(this.expires);
}
this._meta.originalHash = hash(this);

@@ -29,7 +45,12 @@ if (this.handle) {

defineMethod(State.prototype, 'complete', function isDirty() {
this._meta.complete = true;
defineMethod(State.prototype, 'complete', function complete(v) {
this._meta.complete = v !== undefined ? v : true;
return this;
});
defineMethod(State.prototype, 'returning', function returning(v) {
this._meta.returning = v !== undefined ? v : true;
return this;
});
defineMethod(State.prototype, 'save', function save(options, cb) {

@@ -86,3 +107,3 @@ if (typeof options == 'function') {

defineMethod(State.prototype, 'isNew', function isDirty() {
defineMethod(State.prototype, 'isNew', function isNew() {
// TODO: test case for the !destroyed case

@@ -97,3 +118,7 @@ // TODO: destroyed can be removed, with a strict check for handle === undefined

defineMethod(State.prototype, 'isReturning', function isReturning() {
return this._meta.returning;
});
module.exports = State;

@@ -100,0 +125,0 @@

@@ -0,10 +1,38 @@

// Module dependencies.
var Store = require('../store');
var clone = require('clone');
var util = require('util');
/**
* Create a new session-based state store.
*
* @public
* @class
* @param {Object} [options]
* @param {string} [options.key='state'] - Determines what property ("key") on
* the session object where state is located. The state data is stored
* and read from `req.session[key]`.
*/
function SessionStore(options) {
options = options || {};
Store.call(this);
this._key = options.key || 'state';
}
// Inherit from `Store`.
util.inherits(SessionStore, Store)
/**
* Get all states.
*
* @param {IncomingRequest} req
* @param {function} callback
* @param {Error} callback.err
* @param {object[]} callback.states
* @public
*/
SessionStore.prototype.all = function(req, cb) {
if (!req.session) { return cb(new Error('State requires session support. Did you forget to use `express-session` middleware?')); }
var key = this._key;

@@ -21,16 +49,23 @@ if (!req.session || !req.session[key]) {

state = clone(req.session[key][h]);
if (typeof state.expires === 'string') {
// convert expires to a Date object
state.expires = new Date(state.expires);
}
state.handle = h;
arr.push(state);
}
return cb(null, arr);
}
/**
* Fetch state by the given state handle.
*
* @public
* @param {IncomingRequest} req
* @param {string} handle
* @param {function} callback
* @param {Error} callback.err
* @param {object} callback.state
*/
SessionStore.prototype.get = function(req, h, cb) {
if (!req.session) { return cb(new Error('State requires session support. Did you forget to use `express-session` middleware?')); }
var key = this._key;
if (!req.session || !req.session[key] || !req.session[key][h]) {
if (!req.session[key] || !req.session[key][h]) {
return cb();

@@ -40,19 +75,38 @@ }

var state = clone(req.session[key][h]);
if (typeof state.expires === 'string') {
// convert expires to a Date object
state.expires = new Date(state.expires);
}
return cb(null, state);
}
/**
* Commit the given state associated with the given state handle to the store.
*
* @param {IncomingRequest} req
* @param {string} handle
* @param {object} state
* @param {function} callback
* @param {Error} callback.err
* @public
*/
SessionStore.prototype.set = function(req, h, state, cb) {
if (!req.session) { return cb(new Error('State requires session support. Did you forget to use `express-session` middleware?')); }
var key = this._key;
req.session[key] = req.session[key] || {};
req.session[key][h] = clone(state);
req.session[key][h] = JSON.parse(JSON.stringify(state));
return cb(null);
}
/**
* Destroy the state associated with the given state handle.
*
* @param {IncomingRequest} req
* @param {string} handle
* @param {function} callback
* @param {Error} callback.err
* @public
*/
SessionStore.prototype.destroy = function(req, h, cb) {
if (!req.session) { return cb(new Error('State requires session support. Did you forget to use `express-session` middleware?')); }
var key = this._key;
if (!req.session || !req.session[key] || !req.session[key][h]) {
if (!req.session[key] || !req.session[key][h]) {
return cb();

@@ -68,3 +122,3 @@ }

// Export `SessionStore`.
module.exports = SessionStore;

@@ -41,1 +41,11 @@ var uri = require('url');

};
exports.originalURLWithoutState = function(req, options) {
var loc = uri.parse(exports.originalURL(req, options), false);
delete loc.search;
delete loc.query;
// TODO: test case for preserving query params, but not state
return uri.format(loc);
};

@@ -5,2 +5,4 @@ var uri = require('url');

var u = uri.parse(url);
if (!u.host) { return true; }
// TODO: Need to respect proxy settings here

@@ -7,0 +9,0 @@ var host = req.headers['host'];

{
"name": "flowstate",
"version": "0.5.1",
"description": "Stateful, transactional flows using page-based navigation.",
"version": "0.6.0",
"description": "Per-request state management middleware.",
"keywords": [

@@ -6,0 +6,0 @@ "express",

# flowstate
This middleware manages and propagates per-request state across HTTP requests to
a web application. This allows for implementing flows which are sequences of
requests and responses that, taken together, culminate in a desired outcome.
By default, this state is kept in the session. The session itself stores state
by setting a cookie which applies to _all_ requests to an application. This
middleware isolates that state so it can be applied to an individual sequence of
requests. To do this, state is propagated in `return_to` and `state` parameters
across requests. This middleware does this automatically whenever possible,
such as when redirecting. When not possible, such as when rendering a view,
locals and helpers are made available to the view so that `return_to` and
`state` parameters can be added to links and forms.
This middleware emerged from the state management functionality implemented by
authentication-related packages, in particular [passport-oauth2](https://www.passportjs.org/packages/passport-oauth2/) and
[oauth2orize](https://www.oauth2orize.org/) which implement OAuth 2.0. With
this package, the functionality is made generic so that it can be applied to
any HTTP endpoint.
<div align="right">
<sup>Developed by <a href="#authors">Jared Hanson</a>.</sub>
</div>
## Install
```bash
$ npm install flowstate
```
## Usage
#### Add Middleware
Add state middleware to your application or route:
```js
var flowstate = require('flowstate');
app.get('/login', flowstate(), function(req, res, next) {
// ...
});
```
The middleware will attempt to load any state intended for the endpoint, based
the `state` parameter in the query or body of the request. If state is loaded,
it will be set at `req.state` so that the handler can process it. The value set
at `req.state` is referred to as the "current state".
If state is not loaded, an "uninitialized" state will be set at `req.state`. A
state is uninitialized when it is new but not modified. If the request contains
a `return_to` and optional `state` parameter, those will be captured by the
uninitialized state as the location to return the user to when the current state
has been completely processed.
When a response is sent, any modifications to the current state will be saved
if the state is not complete. If the state is complete, any persisted state
will be removed. Note that an uninitialized state will never be saved since it
is not modified. However, the location to return the user to will be preserved
by propagating the `return_to` and optional `state` parameters on subsequent
requests.
#### Render a View
```js
app.get('/login', flowstate(), function(req, res, next) {
var msgs = req.state.messages || [];
res.locals.messages = msgs;
res.locals.hasMessages = !! msgs.length;
res.render('login');
});
```
When a response is sent by rendering a view, if there is state associated with
the request, `res.locals.state` will be set to the current state's handle.
Otherwise the `return_to` and `state` parameters, if any, will be propagated by
setting `res.locals.returnTo` and `res.locals.state`. The view is expected to
decorate links with these properties and add them as hidden input to forms, in
order to propagate state to subsequent requests.
For example, if the above `/login` endpoint is requested with a `return_to`
parameter:
```http
GET /login?return_to=%2Fdashboard HTTP/1.1
```
Then `res.locals.returnTo` will be set to `/dashboard`, making it available to
the view.
If the `/login` endpoint is requested with both a `return_to` and `state`
parameter:
```http
GET /login?return_to=%2Fauthorize%2Fcontinue&state=xyz HTTP/1.1
```
Then `res.locals.returnTo` will be set to `/authorize/continue` and `res.locals.state`
will be set to `xyz`, making them available to the view.
If the `/login` endpoint is requested with:
```http
GET /login?state=Zwu8y84x HTTP/1.1
```
Assuming the state was valid and intended for `/login`, `res.locals.state` will
be set to `Zwu8y84x` and made available to the view. `res.locals.returnTo` will
_not_ be set.
#### Redirect to a Location
```js
app.post('/login', flowstate(), authenticate(), function(req, res, next) {
if (mfaRequired(req.user)) {
return res.redirect('/stepup');
}
// ...
}, function(err, req, res, next) {
if (err.status !== 401) { return next(err); }
req.state.messages = req.state.messages || [];
req.state.messages.push('Invalid username or password.');
req.state.failureCount = req.state.failureCount ? req.state.failureCount + 1 : 1;
req.state.complete(false);
res.redirect('/login');
});
```
When a response redirects the browser, if the current state is complete, any
`return_to` and `state` parameters will be propagated by decorating the target
URL. If the current state is not complete, modifications will be saved and the
redirect will be decorated with the current state's handle.
For example, if the above `/login` endpoint is requested with a `return_to` and
`state` parameter:
```http
POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
username=alice&password=letmein&return_to=%2Fauthorize%2Fcontinue&state=xyz
```
Then the user will be redirected to `/stepup?return_to=%2Fauthorize%2Fcontinue&state=xyz`,
assuming the password is valid and MFA is required.
If the password is not valid, an uninitialized state is set at `req.state` that
captures the `return_to` and `state` parameters. It is then saved and the user
is redirected to `/login?state=Zwu8y84x` (where `'Zwu8y84x'` is the handle of
the newly saved state). The state data stored in the session is as follows:
```json
{
"state": {
"Zwu8y84x": {
"location": "https://www.example.com/login",
"messages": [ "Invalid username or password." ],
"failureCount": 1,
"returnTo": "/authorize/continue",
"state": "xyz"
}
}
}
```
This redirect will cause the browser to request the `GET /login` route above.
Since the request is made with a `state=Zwu8y84x` query parameter, the route will
load the state and make the handle (as well as messages) available to the view.
The view must add the handle to the login form as a hidden input field named
`state`. When submitted, the browser will then make a request with that `state`
parameter:
```http
POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
username=alice&password=letmeinnow&state=Zwu8y84x
```
This time, the `POST /login` route will load the state. If the password is
valid and MFA is required, the user will be will be redirected to
`/stepup?return_to=%2Fauthorize%2Fcontinue&state=xyz`, as before. This is
because the original `return_to` and `state` parameters were captured by the
loaded state object, and are propagated by decorating the redirect location.
If another invalid password is submitted, the cycle of redirecting, rendering
the login view, and prompting the user for a password will repeat, with the
`failureCount` incremented and saved each time.
#### Resume State
```js
app.post('/login', flowstate(), authenticate(), function(req, res, next) {
if (mfaRequired(req.user)) {
return res.redirect('/stepup');
}
res.resumeState(next);
}, function(req, res, next) {
res.redirect('/');
}, function(err, req, res, next) {
// ...
});
```
When a user has completed a given flow, they should be returned to the location
they were navigating prior to entering the flow. This is accomplished by
calling `resumeState()`, a function added to the response by this middleware.
If a current state was loaded, `resumeState()` will return the user to the
captured `return_to` and `state` parameters, if any. Otherwise, it will return
the user to the `return_to` and `state` parameters carried by the request. If
neither of these exist, `resumeState()` will call a callback, which will
typically be `next` to invoke the next middleware. This middleware can then
redirect the user to a default location.
For example, when `POST /login` is requested with a `state` parameter:
```http
POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
username=alice&password=letmeinnow&state=Zwu8y84x
```
Then the user will be redirected to `/authorize/continue&state=xyz`,
assuming the password is valid and MFA is not required.
If the `/login` endpoint is requested with a `return_to` parameter:
```http
POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
username=alice&password=letmein&return_to=%2Fdashboard
```
Then the user will be redirected to `/dashboard`, after logging in.
If the `/login` endpoint is requested without any state-related parameters:
```http
POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
username=alice&password=letmein
```
Then the user will be redirected to `/` by the next middleware in the stack.
#### Push State
## Authors
- [Jared Hanson](https://www.jaredhanson.me/) { [![WWW](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/globe-12x12.svg)](https://www.jaredhanson.me/) [![Facebook](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/facebook-12x12.svg)](https://www.facebook.com/jaredhanson) [![LinkedIn](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/linkedin-12x12.svg)](https://www.linkedin.com/in/jaredhanson) [![Twitter](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/twitter-12x12.svg)](https://twitter.com/jaredhanson) [![GitHub](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/github-12x12.svg)](https://github.com/jaredhanson) }
## License
[The MIT License](http://opensource.org/licenses/MIT)
Copyright (c) 2016-2023 Jared Hanson
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc