heroku-bouncer
Advanced tools
Comparing version 2.1.0 to 3.0.0
12
index.js
@@ -21,5 +21,10 @@ 'use strict'; | ||
* @param {String} options.herokuOAuthSecret a secret for a Heroku OAuth client | ||
* @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.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 | ||
@@ -69,3 +74,4 @@ * authentication URL used | ||
options.herokuAuthURL = options.herokuAuthURL || 'https://id.heroku.com'; | ||
options.herokaiOnlyRedirect = options.herokaiOnlyRedirect || 'https://www.heroku.com'; | ||
options.herokaiOnly = options.herokaiOnly || false; | ||
} |
'use strict'; | ||
var encryptor = require('encryptor'); | ||
var request = require('request'); | ||
@@ -26,2 +27,3 @@ /** | ||
return function(req, res, next) { | ||
var currentSession = getCurrentSession(req, options.sessionSyncNonce); | ||
var i, route; | ||
@@ -32,3 +34,3 @@ | ||
if (!req.session.userSession && req.url.match(route)) { | ||
if (!currentSession && req.url.match(route)) { | ||
return next(); | ||
@@ -38,16 +40,12 @@ } | ||
if (req.session.userSession || isOAuthPath(req.path)) { | ||
if (req.session.userSession) { | ||
var userSession = JSON.parse(cipher.decrypt(req.session.userSession)); | ||
var token = userSession.accessToken; | ||
if (currentSession || isOAuthPath(req.path)) { | ||
if (currentSession) { | ||
var userSession = JSON.parse(cipher.decrypt(currentSession)); | ||
var isHerokai = /@heroku\.com$/.test(userSession.user.email); | ||
var isJSON = /json/.test(req.get('content-type')); | ||
if (options.herokaiOnly === true && !isHerokai) { | ||
if (isJSON || req.method !== 'GET') { | ||
res.statusCode = 401; | ||
return res.json({ id: 'unauthorized', message: 'This app is limited to Herokai only.' }); | ||
} else { | ||
return res.redirect('https://www.heroku.com'); | ||
} | ||
return reauthenticate(req, res, { | ||
redirectTo: options.herokaiOnlyRedirect, | ||
message : 'This app is limited to Herokai only.' | ||
}); | ||
} else if (typeof options.herokaiOnly === 'function' && !isHerokai) { | ||
@@ -57,18 +55,99 @@ return options.herokaiOnly(req, res, next); | ||
req['heroku-bouncer'] = { | ||
token: token, | ||
email: userSession.user.email, | ||
name : userSession.user.name, | ||
id : userSession.user.id | ||
}; | ||
ensureValidToken(userSession, function(err) { | ||
if (err) { | ||
return reauthenticate(req, res); | ||
} | ||
req.session.userSession = cipher.encrypt(JSON.stringify(userSession)); | ||
req['heroku-bouncer'] = { | ||
token: userSession.accessToken, | ||
email: userSession.user.email, | ||
name : userSession.user.name, | ||
id : userSession.user.id | ||
}; | ||
next(); | ||
}); | ||
} else { | ||
next(); | ||
} | ||
} else { | ||
reauthenticate(req, res); | ||
} | ||
}; | ||
next(); | ||
function ensureValidToken(userSession, cb) { | ||
var then = new Date(userSession.createdAt); | ||
var now = new Date(); | ||
var remaining = (now - then) / 1000; // Remaining until token expires. | ||
remaining += 600; // Add 10 minutes | ||
if (remaining > userSession.expiresIn) { | ||
request.post({ | ||
url : options.herokuAuthURL + '/oauth/token', | ||
json: true, | ||
form: { | ||
grant_type : 'refresh_token', | ||
refresh_token: userSession.refreshToken, | ||
client_secret: options.herokuOAuthSecret | ||
} | ||
}, function(err, res, body) { | ||
if (err) { | ||
return cb(err); | ||
} | ||
if (res.statusCode === 200) { | ||
userSession.accessToken = body.access_token; | ||
userSession.refreshToken = body.refresh_token; | ||
userSession.createdAt = (new Date()).toISOString(); | ||
userSession.expiresIn = body.expires_in; | ||
cb(); | ||
} else { | ||
cb(new Error('Expected 200 from Heroku Identity, got ' + res.statusCode)); | ||
} | ||
}); | ||
} else { | ||
req.session.redirectPath = req.url; | ||
res.redirect('/auth/heroku'); | ||
cb(); | ||
} | ||
}; | ||
} | ||
function reauthenticate(req, res, options) { | ||
var isJSON = /json/.test(req.get('accept')); | ||
options = options || {}; | ||
var redirectTo = options.redirectTo || '/auth/heroku'; | ||
var message = options.message || 'Please authenticate.'; | ||
req.session.reset(); | ||
if (req.method.toLowerCase() === 'get' && !isJSON) { | ||
if (redirectTo === '/auth/heroku') { | ||
req.session.redirectPath = req.url; | ||
} | ||
res.redirect(redirectTo); | ||
} else { | ||
res.statusCode = 401; | ||
res.json({ id: 'unauthorized', message: message }); | ||
} | ||
} | ||
}; | ||
function getCurrentSession(req, checkNonce) { | ||
var session = req.session.userSession; | ||
if (checkNonce) { | ||
return nonceMatch(req, checkNonce) ? session : null; | ||
} else { | ||
return session; | ||
} | ||
} | ||
function nonceMatch(req, checkNonce) { | ||
return req.session.herokuBouncerSessionNonce === req.cookies[checkNonce]; | ||
} | ||
function isOAuthPath(path) { | ||
@@ -75,0 +154,0 @@ return [ |
@@ -11,3 +11,2 @@ 'use strict'; | ||
var heroku = require('heroku-client'); | ||
var router = new express.Router(); | ||
@@ -26,2 +25,3 @@ /** | ||
var oauth = getOAuth(); | ||
var router = new express.Router(); | ||
@@ -33,3 +33,3 @@ router.get('/auth/heroku', function(req, res) { | ||
router.get('/auth/heroku/callback', function(req, res) { | ||
oauth.getOAuthAccessToken(req.query.code, null, function(err, accessToken) { | ||
oauth.getOAuthAccessToken(req.query.code, null, function(err, accessToken, refreshToken, results) { | ||
if (err) throw err; | ||
@@ -46,3 +46,6 @@ | ||
var userSession = JSON.stringify({ | ||
accessToken: accessToken, | ||
accessToken : accessToken, | ||
refreshToken: refreshToken, | ||
createdAt : (new Date()).toISOString(), | ||
expiresIn : results.expires_in, | ||
@@ -58,2 +61,7 @@ user: { | ||
if (options.sessionSyncNonce) { | ||
var nonceName = options.sessionSyncNonce; | ||
req.session.herokuBouncerSessionNonce = req.cookies[nonceName]; | ||
} | ||
req.session.userSession = cipher.encrypt(userSession); | ||
@@ -74,3 +82,3 @@ | ||
router.get('/auth/heroku/logout', function(req, res) { | ||
req.session = {}; | ||
req.session.reset(); | ||
res.redirect(options.herokuAuthURL + '/logout'); | ||
@@ -77,0 +85,0 @@ }); |
{ | ||
"name": "heroku-bouncer", | ||
"version": "2.1.0", | ||
"version": "3.0.0", | ||
"description": "heroku bouncer middleware for express", | ||
@@ -23,3 +23,5 @@ "main": "index.js", | ||
"express": "~4.1.1", | ||
"heroku-client": "^1.2.0" | ||
"heroku-client": "^1.2.0", | ||
"bluebird": "^2.2.2", | ||
"request": "^2.37.0" | ||
}, | ||
@@ -30,32 +32,35 @@ "devDependencies": { | ||
"mocha": "~1.18.2", | ||
"request": "~2.34.0", | ||
"should": "~3.2.0-beta1" | ||
"should": "~3.2.0-beta1", | ||
"tough-cookie": "^0.12.1", | ||
"bluebird": "^2.2.2", | ||
"body-parser": "^1.4.3", | ||
"node-uuid": "^1.4.1" | ||
}, | ||
"jshintConfig": { | ||
"eqeqeq" : true, | ||
"forin" : true, | ||
"eqeqeq": true, | ||
"forin": true, | ||
"globalstrict": true, | ||
"immed" : true, | ||
"indent" : 2, | ||
"latedef" : "nofunc", | ||
"newcap" : true, | ||
"noarg" : true, | ||
"quotmark" : "single", | ||
"strict" : true, | ||
"trailing" : true, | ||
"undef" : true, | ||
"unused" : true, | ||
"globals" : { | ||
"afterEach" : false, | ||
"before" : false, | ||
"immed": true, | ||
"indent": 2, | ||
"latedef": "nofunc", | ||
"newcap": true, | ||
"noarg": true, | ||
"quotmark": "single", | ||
"strict": true, | ||
"trailing": true, | ||
"undef": true, | ||
"unused": true, | ||
"globals": { | ||
"afterEach": false, | ||
"before": false, | ||
"beforeEach": false, | ||
"context" : false, | ||
"describe" : false, | ||
"exports" : false, | ||
"it" : false, | ||
"module" : false, | ||
"process" : false, | ||
"require" : false | ||
"context": false, | ||
"describe": false, | ||
"exports": false, | ||
"it": false, | ||
"module": false, | ||
"process": false, | ||
"require": false | ||
} | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
# node-heroku-bouncer | ||
# node-heroku-bouncer [![Build Status](https://travis-ci.org/jclem/node-heroku-bouncer.svg?branch=master)](https://travis-ci.org/jclem/node-heroku-bouncer) | ||
@@ -14,6 +14,6 @@ node-heroku-bouncer is an easy-to-use module for adding Heroku OAuth | ||
node-heroku-bouncer assumes you've already added the express | ||
[cookieParser][cookieParser] and [cookieSession][cookieSession] 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. | ||
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. | ||
@@ -28,4 +28,4 @@ Use the `bouncer.middleware` object to set up middleware that will ensure that | ||
app.use(express.cookieParser('your cookie secret')); | ||
app.use(express.cookieSession({ | ||
app.use(require('cookie-parser')('your cookie secret')); | ||
app.use(require('client-sessions')({ | ||
secret: 'your session secret', | ||
@@ -50,8 +50,33 @@ cookie: { | ||
app.get('/', function(req, res) { | ||
res.end('you must be logged in!'); | ||
res.end('you are clearly logged in!'); | ||
}); | ||
``` | ||
After requests pass through `bouncer.middleware`, they'll have a | ||
`heroku-bouncer` property on them: | ||
```javascript | ||
{ | ||
token: 'user-api-token', | ||
id : 'user-id', | ||
name : 'user-name', | ||
email: 'user-email' | ||
} | ||
``` | ||
To log a user out, send them to `/auth/heroku/logout`. | ||
### Options | ||
| Options | Required? | Default | Description | | ||
|---------|-----------|---------|-------------| | ||
| `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 | | ||
| `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` | | ||
## Test | ||
@@ -63,3 +88,3 @@ | ||
[cookieParser]: http://expressjs.com/3x/api.html#cookieParser | ||
[cookieSession]: http://expressjs.com/3x/api.html#cookieSession | ||
[cookieParser]: https://github.com/expressjs/cookie-parser | ||
[clientSessions]: https://github.com/mozilla/node-client-sessions |
'use strict'; | ||
var Promise = require('bluebird'); | ||
var request = require('request'); | ||
var get = Promise.promisify(request.get); | ||
exports.shouldNotRedirect = function(client, done) { | ||
exports.shouldNotRedirect = function() { | ||
var jar = request.jar(); | ||
request({ | ||
return get({ | ||
jar: jar, | ||
url: 'http://localhost:' + client.address().port | ||
}, function(err) { | ||
if (err) throw err; | ||
request({ | ||
jar: jar, | ||
url: 'http://localhost:' + client.address().port + '/hello', | ||
url: this.url | ||
}).then(function() { | ||
return get({ | ||
jar : jar, | ||
url : this.url + '/hello', | ||
followRedirect: false | ||
}, function(err, res) { | ||
if (err) throw err; | ||
res.body.should.eql('hello world'); | ||
done(); | ||
}); | ||
}.bind(this)).spread(function(res, body) { | ||
body.should.eql('hello world'); | ||
}); | ||
}; | ||
exports.shouldRedirect = function(client, done) { | ||
exports.shouldRedirect = function() { | ||
var jar = request.jar(); | ||
request({ | ||
return get({ | ||
jar: jar, | ||
url: 'http://localhost:' + client.address().port | ||
}, function(err) { | ||
if (err) throw err; | ||
request({ | ||
jar: jar, | ||
url: 'http://localhost:' + client.address().port + '/helloo', | ||
url: this.url | ||
}).then(function() { | ||
return get({ | ||
jar : jar, | ||
url : this.url + '/hello', | ||
followRedirect: false | ||
}, function(err, res) { | ||
if (err) throw err; | ||
res.headers.location.should.eql('https://www.heroku.com'); | ||
done(); | ||
}); | ||
}.bind(this)).spread(function(res) { | ||
res.headers.location.should.eql('https://www.heroku.com'); | ||
}); | ||
}; |
@@ -0,1 +1,3 @@ | ||
// jshint -W068 | ||
'use strict'; | ||
@@ -5,19 +7,15 @@ | ||
it('throws an error when not given a `herokuBouncerSecret`', function() { | ||
function requireIndex() { | ||
(function() { | ||
require('../index')(); | ||
} | ||
requireIndex.should.throw('No `herokuBouncerSecret` provided to heroku-bouncer'); | ||
}).should.throw('No `herokuBouncerSecret` provided to heroku-bouncer'); | ||
}); | ||
it('throws an error when not given a `herokuOAuthID`', function() { | ||
function requireIndex() { | ||
(function() { | ||
require('../index')({ herokuBouncerSecret: 'foo' }); | ||
} | ||
requireIndex.should.throw('No `herokuOAuthID` provided to heroku-bouncer'); | ||
}).should.throw('No `herokuOAuthID` provided to heroku-bouncer'); | ||
}); | ||
it('throws an error when not given a `herokuOAuthSecret`', function() { | ||
function requireIndex() { | ||
(function() { | ||
require('../index')({ | ||
@@ -27,6 +25,4 @@ herokuBouncerSecret: 'foo', | ||
}); | ||
} | ||
requireIndex.should.throw('No `herokuOAuthSecret` provided to heroku-bouncer'); | ||
}).should.throw('No `herokuOAuthSecret` provided to heroku-bouncer'); | ||
}); | ||
}); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
31759
14
773
87
6
8
3
+ Addedbluebird@^2.2.2
+ Addedrequest@^2.37.0
+ Addedajv@6.12.6(transitive)
+ Addedasn1@0.2.6(transitive)
+ Addedassert-plus@1.0.0(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedaws-sign2@0.7.0(transitive)
+ Addedaws4@1.13.2(transitive)
+ Addedbcrypt-pbkdf@1.0.2(transitive)
+ Addedbluebird@2.11.0(transitive)
+ Addedcaseless@0.12.0(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcore-util-is@1.0.2(transitive)
+ Addeddashdash@1.14.1(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedecc-jsbn@0.1.2(transitive)
+ Addedextend@3.0.2(transitive)
+ Addedextsprintf@1.3.0(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedforever-agent@0.6.1(transitive)
+ Addedform-data@2.3.3(transitive)
+ Addedgetpass@0.1.7(transitive)
+ Addedhar-schema@2.0.0(transitive)
+ Addedhar-validator@5.1.5(transitive)
+ Addedhttp-signature@1.2.0(transitive)
+ Addedis-typedarray@1.0.0(transitive)
+ Addedisstream@0.1.2(transitive)
+ Addedjsbn@0.1.1(transitive)
+ Addedjson-schema@0.4.0(transitive)
+ Addedjson-schema-traverse@0.4.1(transitive)
+ Addedjson-stringify-safe@5.0.1(transitive)
+ Addedjsprim@1.4.2(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedoauth-sign@0.9.0(transitive)
+ Addedperformance-now@2.1.0(transitive)
+ Addedpsl@1.15.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedqs@6.5.3(transitive)
+ Addedrequest@2.88.2(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsshpk@1.18.0(transitive)
+ Addedtough-cookie@2.5.0(transitive)
+ Addedtunnel-agent@0.6.0(transitive)
+ Addedtweetnacl@0.14.5(transitive)
+ Addeduri-js@4.4.1(transitive)
+ Addeduuid@3.4.0(transitive)
+ Addedverror@1.10.0(transitive)