New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

heroku-bouncer

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

heroku-bouncer - npm Package Compare versions

Comparing version 3.1.2 to 4.0.1

CHANGELOG.md

106

index.js
'use strict';
var express = require('express');
var middleware = require('./lib/middleware');
var router = require('./lib/router');
var routes = require('./lib/router');
/**
* `heroku-bouncer` is a function which exposes two things: A piece of
* middleware to handle Heroku OAuth for a node app, and a router which exposes
* the required OAuth endpoints (such as a callback URL), and a logout path.
* `heroku-bouncer` provides a router and a piece of middleware for handling
* Heroku OAuth sessions in a web app.
*
* var bouncer = require('heroku-bouncer')({
* encryptionSecret : process.env.USER_SESSION_SECRET,
* oAuthClientID : process.env.HEROKU_OAUTH_ID,
* oAuthClientSecret: process.env.HEROKU_OAUTH_SECRET
* });
*
* app.use(bouncer);
*
* @class Main

@@ -17,60 +25,64 @@ */

* @param {Object} options
* @param {String} options.herokuBouncerSecret a secret used to encrypt
* information in the session
* @param {String} options.herokuOAuthID an ID for a Heroku OAuth client
* @param {String} options.herokuOAuthSecret a secret for a Heroku OAuth client
* @param {String} [options.herokaiOnlyRedirect='https://www.heroku.com'] a URL
* to redirect to when a user is not a Herokai and `herokaiOnly` is `true`
* @param {String} [options.sessionSyncNonce] if present, determines the name of
* a cookie shared across properties under the same domain in order to keep
* their sessions synchronized
* @param {Array} [options.ignoreRoutes=[]] an array of route regular
* expressions to match request routes again. If a request route matches one
* of these, it passes through the authentication stack instantly.
* @param {String} [options.herokuAuthURL='https://id.heroku.com'] the
* authentication URL used
* @param {Boolean} [options.herokaiOnly=false] whether or not to restrict this
* app to Herokai (users with @heroku.com email addresses)
* @example
* ```javascript
* var bouncer = require('heroku-bouncer')({
* herokuBouncerSecret: process.env.HEROKU_BOUNCER_SECRET,
* herokuOAuthID : process.env.HEROKU_OAUTH_ID,
* herokuOAuthSecret : process.env.HEROKU_OAUTH_SECRET
* });
*
* app.use(bouncer.middleware);
* app.use(bouncer.router);
* @param {String} options.encryptionSecret a user information encryption secret
* @param {String} options.oAuthClientID a Heroku OAuth client ID
* @param {String} options.oAuthClientSecret a Heroku OAuth client secret
* @param {String} [options.herokuAPIHost=null] optionally override the host
* that API requests are sent to (defaults in the Node Heorku client to
* 'api.heroku.com').
* @param {String} [options.sessionSyncNonce=null] the name of a cookie shared
* across different apps on the same domain to keep sessions synchronized
* @param {Array} [options.ignoredRoutes=[] an array of regular expressions
* against which routes are tested to determine if they skip the
* authentication stack. Only used when there is no current session.
* @param {String} [options.oAuthServerURL='https://id.heroku.com'] the URL of
* the Heroku OAuth server app
* @param {Function} [options.herokaiOnlyHandler=null] if provided, this route
* handler will be called on requests by non-Herokai
* ```
*/
module.exports = function(options) {
var router = new express.Router();
options = options || {};
enforceOptions(options);
setOptions(options);
return {
middleware: middleware(options),
router : router(options)
};
router.middleware = middleware(options);
router.router = routes(options);
router.use(router.middleware);
router.use(router.router);
return router;
};
function enforceOptions(options) {
if (!options.herokuBouncerSecret) {
throw new Error('No `herokuBouncerSecret` provided to heroku-bouncer');
function setOptions(options) {
if (!options.encryptionSecret) {
throw new Error('No `encryptionSecret` provided to heroku-bouncer');
}
if (!options.herokuOAuthID) {
throw new Error('No `herokuOAuthID` provided to heroku-bouncer');
if (!options.oAuthClientID) {
throw new Error('No `oAuthClientID` provided to heroku-bouncer');
}
if (!options.herokuOAuthSecret) {
throw new Error('No `herokuOAuthSecret` provided to heroku-bouncer');
if (!options.oAuthClientSecret) {
throw new Error('No `oAuthClientSecret` provided to heroku-bouncer');
}
if (!options.hasOwnProperty('ignoreRoutes')) {
options.ignoreRoutes = [];
if (options.herokaiOnlyHandler && typeof(options.herokaiOnlyHandler) !== 'function') {
throw new Error('`herokaiOnlyHandler` must be a handler function');
}
options.herokuAuthURL = options.herokuAuthURL || 'https://id.heroku.com';
options.herokaiOnlyRedirect = options.herokaiOnlyRedirect || 'https://www.heroku.com';
options.herokaiOnly = options.herokaiOnly || false;
var defaults = {
herokaiOnlyHandler: null,
herokuAPIHost : null,
ignoredRoutes : [],
oAuthServerURL : 'https://id.heroku.com',
oAuthScope : 'identity',
sessionSyncNonce : null,
};
for (var key in defaults) {
if (defaults.hasOwnProperty(key)) {
options[key] = options[key] || defaults[key];
}
}
}

@@ -13,60 +13,48 @@ 'use strict';

/**
* Create a middleware function for using Heroku OAuth. If the user is
* authenticated, it will add an API token, email, name and ID to the user's
* request and session.
* Create a piece of middleware for using Heroku OAuth. If the user is
* authenticated, it will add appropriate account information and a token for
* making API requests to the session.
*
* @method main
* @private
* @param {Object} options options for configuring the middleware. See
* {{#crossLink "Main/main"}}Main#main{{/crossLink}} for configuration
* details.
* @return {Function} a middleware function
* @return {Function} a piece of middleware
*/
module.exports = function(options) {
var cipher = encryptor(options.herokuBouncerSecret);
var cipher = encryptor(options.encryptionSecret);
return function(req, res, next) {
var currentSession = getCurrentSession(req, options.sessionSyncNonce);
var i, route;
var userSession = getUserSession(req, options.sessionSyncNonce);
for (i = 0; i < options.ignoreRoutes.length; i++) {
route = options.ignoreRoutes[i];
if (!userSession && isIgnoredRoute(req.path)) {
return next();
}
if (!currentSession && req.url.match(route)) {
return next();
if (userSession) {
var isHerokai = /@heroku\.com$/.test(userSession.user.email);
if (options.herokaiOnlyHandler && !isHerokai) {
return options.herokaiOnlyHandler(req, res, next);
}
}
if (currentSession || isOAuthPath(req.path)) {
if (currentSession) {
var userSession = cipher.decrypt(currentSession);
var isHerokai = /@heroku\.com$/.test(userSession.user.email);
if (options.herokaiOnly === true && !isHerokai) {
return reauthenticate(req, res, {
redirectTo: options.herokaiOnlyRedirect,
message : 'This app is limited to Herokai only.'
});
} else if (typeof options.herokaiOnly === 'function' && !isHerokai) {
return options.herokaiOnly(req, res, next);
ensureValidToken(userSession, function(err) {
if (err) {
return reauthenticate(req, res);
}
ensureValidToken(userSession, function(err) {
if (err) {
return reauthenticate(req, res);
}
req.session.userSession = cipher.encrypt(userSession);
req.session.userSession = cipher.encrypt(userSession);
req['heroku-bouncer'] = {
token: userSession.accessToken,
email: userSession.user.email,
name : userSession.user.name,
id : userSession.user.id
};
req['heroku-bouncer'] = {
token: userSession.accessToken,
email: userSession.user.email,
name : userSession.user.name,
id : userSession.user.id
};
next();
});
} else {
next();
}
});
} else if (isOAuthPath(req.path)) {
next();
} else {

@@ -85,3 +73,3 @@ reauthenticate(req, res);

request.post({
url : options.herokuAuthURL + '/oauth/token',
url : options.oAuthServerURL + '/oauth/token',
json: true,

@@ -91,3 +79,3 @@ form: {

refresh_token: userSession.refreshToken,
client_secret: options.herokuOAuthSecret
client_secret: options.oAuthClientSecret
}

@@ -115,21 +103,35 @@ }, function(err, res, body) {

function reauthenticate(req, res, options) {
var isJSON = /json/.test(req.get('accept'));
function getUserSession(req, checkNonce) {
var session = cipher.decrypt(req.session.userSession);
options = options || {};
if (checkNonce) {
return nonceMatch(req, checkNonce) ? session : null;
} else {
return (session && session.user && session.user.email) ? session : null;
}
}
var redirectTo = options.redirectTo || '/auth/heroku';
var message = options.message || 'Please authenticate.';
function isIgnoredRoute(route) {
var pattern;
for (var i = 0; i < options.ignoredRoutes.length; i++) {
pattern = options.ignoredRoutes[i];
if (pattern.test(route)) {
return true;
}
}
}
function reauthenticate(req, res) {
var isJSON = /json/.test(req.get('accept'));
req.session.reset();
if (req.method.toLowerCase() === 'get' && !isJSON) {
if (redirectTo === '/auth/heroku') {
req.session.redirectPath = req.url;
}
res.redirect(redirectTo);
req.session.redirectPath = req.url;
res.redirect('/auth/heroku');
} else {
res.statusCode = 401;
res.json({ id: 'unauthorized', message: message });
res.json({ id: 'unauthorized', message: 'Please authenticate.' });
}

@@ -139,12 +141,2 @@ }

function getCurrentSession(req, checkNonce) {
var session = req.session.userSession;
if (checkNonce) {
return nonceMatch(req, checkNonce) ? session : null;
} else {
return session;
}
}
function nonceMatch(req, checkNonce) {

@@ -151,0 +143,0 @@ return req.session.herokuBouncerSessionNonce === req.cookies[checkNonce];

@@ -22,3 +22,3 @@ 'use strict';

module.exports = function(options) {
var cipher = encryptor(options.herokuBouncerSecret);
var cipher = encryptor(options.encryptionSecret);
var oauth = getOAuth();

@@ -35,3 +35,3 @@ var router = new express.Router();

res.redirect(oauth.getAuthorizeUrl({ response_type: 'code' }));
res.redirect(oauth.getAuthorizeUrl({ response_type: 'code', scope: options.oAuthScope }));
});

@@ -45,3 +45,3 @@

token: accessToken,
host : options.hostname
host : options.herokuAPIHost
});

@@ -88,3 +88,3 @@

req.session.reset();
res.redirect(options.herokuAuthURL + '/logout');
res.redirect(options.oAuthServerURL + '/logout');
});

@@ -94,5 +94,5 @@

return new OAuth(
options.herokuOAuthID,
options.herokuOAuthSecret,
options.herokuAuthURL,
options.oAuthClientID,
options.oAuthClientSecret,
options.oAuthServerURL,
'/oauth/authorize',

@@ -99,0 +99,0 @@ '/oauth/token'

{
"name": "heroku-bouncer",
"version": "3.1.2",
"version": "4.0.1",
"description": "heroku bouncer middleware for express",

@@ -5,0 +5,0 @@ "main": "index.js",

# node-heroku-bouncer [![Build Status](https://travis-ci.org/jclem/node-heroku-bouncer.svg?branch=master)](https://travis-ci.org/jclem/node-heroku-bouncer)
node-heroku-bouncer is an easy-to-use module for adding Heroku OAuth
authentication to express 4 apps.
authentication to Express 4 apps.

@@ -12,43 +12,49 @@ ## Install

## Requirements
- Node 0.10.x
- Express 4.x
## Use
node-heroku-bouncer assumes you've already added [cookie-parser][cookieParser]
and [client-sessions][clientSessions] middlewares to your app. To set it up,
pass it your OAuth client ID and secret and another secret used to encrypt your
user's OAuth session data.
Ensure your app is using the [cookie-parser][cookieParser] and
[client-sessions][clientSessions] middlewares. This module is not guaranteed to
work with any other session middleware.
Use the `bouncer.middleware` object to set up middleware that will ensure that
your users are logged in (and redirect otherwise), and the `bouncer.routes`
object to add the OAuth-specific routes to your app:
```javascript
var express = require('express');
var app = express();
var express = require('express');
var cookieParser = require('cookie-parser');
var sessions = require('client-sessions');
var bouncer = require('heroku-bouncer');
var app = express();
app.use(require('cookie-parser')('your cookie secret'));
app.use(require('client-sessions')({
app.use(cookieParser('your cookie secret'));
// NOTE: These options are good general options for use in a Heroku app, but
// carefully review your own environment's needs before just copying these.
app.use(sessions({
cookieName : 'session',
secret : 'your session secret',
duration : 24 * 60 * 60 * 1000,
activeDuration: 1000 * 60 * 5,
cookie : {
path : '/',
ephemeral: false,
httpOnly : true
httpOnly : true,
secure : false
}
}));
var bouncer = require('heroku-bouncer')({
herokuOAuthID : 'client-id',
herokuOAuthSecret : 'client-secret',
herokuBouncerSecret: 'abcd1234abcd1234'
});
app.use(bouncer({
oAuthClientID : 'client-id',
oAuthClientSecret : 'client-secret',
encryptionSecret : 'abcd1234abcd1234'
}));
app.use(bouncer.middleware);
app.use(bouncer.router);
app.get('/', function(req, res) {
res.end('you are clearly logged in!');
res.end('You must be logged in.');
});
```
After requests pass through `bouncer.middleware`, they'll have a
After requests pass through the bouncer middleware, they'll have the
`heroku-bouncer` property on them:

@@ -71,10 +77,10 @@

|---------|-----------|---------|-------------|
| `herokuOAuthID` | Yes | n/a | The ID of your Heroku OAuth client |
| `herokuOAuthSecret` | Yes | n/a | The secret of your Heroku OAuth client |
| `herokuBouncerSecret` | Yes | n/a | A random string used to encrypt your user session data |
| `encryptionSecret` | Yes | n/a | A random string used to encrypt your user session data |
| `oAuthClientID` | Yes | n/a | The ID of your Heroku OAuth client |
| `oAuthClientSecret` | Yes | n/a | The secret of your Heroku OAuth client |
| `herokuAPIHost` | No | n/a | An optional override host to send Heroku API requests to |
| `sessionSyncNonce` | No | `null` | The name of a nonce cookie to validate sessions against |
| `ignoreRoutes` | No | `[]` | An array of regular expressions to match routes to be ignored |
| `herokuAuthURL` | No | `"https://id.heroku.com"` | The location of the Heroku OAuth server |
| `herokaiOnly` | No | `false` | Whether or not to restrict the app to Herokai only |
| `herokaiOnlyRedirect` | No | `"https://www.heroku.com"` | Where to redirect non-Herokai to when using `herokaiOnly` |
| `ignoredRoutes` | No | `[]` | An array of regular expressions to match routes to be ignored when there is no session active |
| `oAuthServerURL` | No | `"https://id.heroku.com"` | The location of the Heroku OAuth server |
| `herokaiOnlyHandler` | No | `null` | A route handler that will be called on requests by non-Herokai |

@@ -81,0 +87,0 @@ ## Test

@@ -138,2 +138,16 @@ // jshint -W030

});
context('and userSession does not contain user info', function() {
beforeEach(function(){
herokuStubber.stubUser({ email: undefined });
});
it('forces to reauthenticate', function() {
return withClient().spread(function(client, url) {
return get(url, { followRedirect: false });
}).spread(function(res) {
res.headers.location.should.eql('/auth/heroku');
});
});
});
});

@@ -241,7 +255,9 @@

context('and herokaiOnly is set to `true`', function() {
context('and herokaiOnly is set', function() {
var clientOptions;
beforeEach(function() {
clientOptions = { herokaiOnly: true };
clientOptions = { herokaiOnlyHandler: function(req, res) {
res.end('You are not a Herokai.');
} };
});

@@ -264,7 +280,7 @@

context('and it is a non-JSON GET request', function() {
it('redirects to the non-Herokai URL', function() {
return withClient(clientOptions).spread(function(client, url) {
return get(url + '/hello-world', { jar: true });
it('uses the custom request handler', function() {
return authenticate(clientOptions).spread(function(client, url, jar) {
return get(url, { jar: jar });
}).spread(function(res, body) {
body.should.eql('herokai only');
body.should.eql('You are not a Herokai.');
});

@@ -275,29 +291,2 @@ });

});
context('and herokaiOnly is a function', function() {
var clientOptions;
beforeEach(function() {
clientOptions = { herokaiOnly: function(req, res) {
res.end('You are not a Herokai.');
} };
});
context('and the user is a Herokai', function() {
it('performs the request like normal', function() {
herokuStubber.stubUser({ email: 'user@heroku.com' });
return itBehavesLikeANormalRequest(clientOptions);
});
});
context('and the user is not a Herokai', function() {
it('uses the custom request handler', function() {
return authenticate(clientOptions).spread(function(client, url, jar) {
return get(url, { jar: jar });
}).spread(function(res, body) {
body.should.eql('You are not a Herokai.');
});
});
});
});
});

@@ -304,0 +293,0 @@

@@ -24,4 +24,3 @@ 'use strict';

app.use(bouncer.middleware);
app.use(bouncer.router);
app.use(bouncer);

@@ -28,0 +27,0 @@ app.use(function(req, res, next) {

@@ -11,9 +11,7 @@ 'use strict';

var defaultOptions = {
herokuOAuthID : 'client-id',
herokuOAuthSecret : 'client-secret',
herokuBouncerSecret: 'abcd1234abcd1234',
herokuAuthURL : 'http://localhost:' + oAuthServer.address().port,
herokaiOnlyRedirect: '/herokai-only',
ignoreRoutes : [/^\/ignore/, /^\/herokai-only/],
herokaiOnly : false
oAuthClientID : 'client-id',
oAuthClientSecret : 'client-secret',
encryptionSecret : 'abcd1234abcd1234',
oAuthServerURL : 'http://localhost:' + oAuthServer.address().port,
ignoredRoutes : [/^\/ignore/, /^\/herokai-only/]
};

@@ -20,0 +18,0 @@

@@ -6,22 +6,33 @@ // jshint -W068

describe('setup', function() {
it('throws an error when not given a `herokuBouncerSecret`', function() {
it('throws an error when not given a `encryptionSecret`', function() {
(function() {
require('../index')();
}).should.throw('No `herokuBouncerSecret` provided to heroku-bouncer');
}).should.throw('No `encryptionSecret` provided to heroku-bouncer');
});
it('throws an error when not given a `herokuOAuthID`', function() {
it('throws an error when not given a `oAuthClientID`', function() {
(function() {
require('../index')({ herokuBouncerSecret: 'foo' });
}).should.throw('No `herokuOAuthID` provided to heroku-bouncer');
require('../index')({ encryptionSecret: 'foo' });
}).should.throw('No `oAuthClientID` provided to heroku-bouncer');
});
it('throws an error when not given a `herokuOAuthSecret`', function() {
it('throws an error when not given a `oAuthClientSecret`', function() {
(function() {
require('../index')({
herokuBouncerSecret: 'foo',
herokuOAuthID: '123'
encryptionSecret: 'foo',
oAuthClientID : '123'
});
}).should.throw('No `herokuOAuthSecret` provided to heroku-bouncer');
}).should.throw('No `oAuthClientSecret` provided to heroku-bouncer');
});
it('throws an error if `herokaiOnlyHandler` is not a function', function() {
(function() {
require('../index')({
encryptionSecret : 'foo',
oAuthClientID : '123',
oAuthClientSecret : '123',
herokaiOnlyHandler: true
});
}).should.throw('`herokaiOnlyHandler` must be a handler function');
});
});
SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc