0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

added password protection

closes #4993
- brings password protection to the frontend of blogs
- adds testing for password protection
- upgrades bcrypt-js to 2.1.0
This commit is contained in:
Austin Burdine 2015-03-26 02:01:39 -05:00
parent f4a9f0c82d
commit 2865662ee5
24 changed files with 697 additions and 9 deletions

View file

@ -24,6 +24,10 @@ var FeatureController = Ember.Controller.extend(Ember.PromiseProxyMixin, {
}
return value;
}),
passProtectUI: Ember.computed('config.passProtectUI', 'labs.passProtectUI', function () {
return this.get('config.passProtectUI') || this.get('labs.passProtectUI');
})
});

View file

@ -1,6 +1,8 @@
import Ember from 'ember';
var SettingsController = Ember.Controller.extend({
needs: ['feature'],
showGeneral: Ember.computed('session.user.name', function () {
return this.get('session.user.isAuthor') || this.get('session.user.isEditor') ? false : true;
}),
@ -21,6 +23,9 @@ var SettingsController = Ember.Controller.extend({
}),
showAbout: Ember.computed('session.user.name', function () {
return this.get('session.user.isAuthor') ? false : true;
}),
showPassProtection: Ember.computed('session.user.name', 'controllers.feature.passProtectUI', function () {
return this.get('session.user.isAuthor') || this.get('session.user.isEditor') || !this.get('controllers.feature.passProtectUI') ? false : true;
})
});

View file

@ -23,6 +23,16 @@ var LabsController = Ember.Controller.extend(Ember.Evented, {
});
},
usePassProtectUI: Ember.computed('controllers.feature.passProtectUI', function (key, value) {
// setter
if (arguments.length > 1) {
this.saveLabs('passProtectUI', value);
}
// getter
return this.get('controllers.feature.passProtectUI');
}),
actions: {
onUpload: function (file) {
var self = this,

View file

@ -0,0 +1,27 @@
import Ember from 'ember';
var SettingsPassProtectController = Ember.Controller.extend({
actions: {
save: function () {
var self = this;
if (this.get('model.isPrivate') && this.get('model.password') === '') {
self.notifications.closePassive();
self.notifications.showError('Password must have a value.');
return;
}
return this.get('model').save().then(function (model) {
self.notifications.closePassive();
self.notifications.showSuccess('Settings successfully saved.');
return model;
}).catch(function (errors) {
self.notifications.closePassive();
self.notifications.showErrors(errors);
});
}
}
});
export default SettingsPassProtectController;

View file

@ -19,7 +19,9 @@ var Setting = DS.Model.extend(NProgressSaveMixin, ValidationEngine, {
ghost_head: DS.attr('string'),
ghost_foot: DS.attr('string'),
labs: DS.attr('string'),
navigation: DS.attr('string')
navigation: DS.attr('string'),
isPrivate: DS.attr('boolean'),
password: DS.attr('string')
});
export default Setting;

View file

@ -43,6 +43,7 @@ Router.map(function () {
this.route('labs');
this.route('code-injection');
this.route('navigation');
this.route('pass-protect');
});
// Redirect debug to settings labs

View file

@ -0,0 +1,45 @@
import AuthenticatedRoute from 'ghost/routes/authenticated';
import loadingIndicator from 'ghost/mixins/loading-indicator';
import CurrentUserSettings from 'ghost/mixins/current-user-settings';
import styleBody from 'ghost/mixins/style-body';
var SettingsPassProtectRoute = AuthenticatedRoute.extend(styleBody, loadingIndicator, CurrentUserSettings, {
classNames: ['settings-view-pass'],
beforeModel: function () {
var feature = this.controllerFor('feature'),
self = this;
if (!feature) {
this.generateController('feature');
feature = this.controllerFor('feature');
}
return this.get('session.user')
.then(this.transitionAuthor())
.then(this.transitionEditor())
.then(function () {
return feature.then(function () {
if (!feature.get('passProtectUI')) {
return self.transitionTo('settings.general');
}
});
});
},
model: function () {
return this.store.find('setting', {type: 'blog,theme'}).then(function (records) {
return records.get('firstObject');
});
},
actions: {
save: function () {
this.get('controller').send('save');
}
}
});
export default SettingsPassProtectRoute;

View file

@ -27,6 +27,10 @@
{{gh-activating-list-item route="settings.code-injection" title="Code Injection" classNames="settings-nav-code icon-code"}}
{{/if}}
{{#if showPassProtection}}
{{gh-activating-list-item route="settings.pass-protect" title="Password Protection" classNames="settings-nav-pass icon-lock"}}
{{/if}}
{{#if showLabs}}
{{gh-activating-list-item route="settings.labs" title="Labs" classNames="settings-nav-labs icon-atom"}}
{{/if}}

View file

@ -44,4 +44,21 @@
</fieldset>
</form>
<hr>
<form>
<fieldset>
<div class="form-group for-checkbox">
<label for="labs-passProtectUI">Password Protection</label>
<label class="checkbox" for="labs-passProtectUI">
{{input id="labs-passProtectUI" name="labs[passProtectUI]" type="checkbox"
checked=usePassProtectUI}}
<span class="input-toggle-component"></span>
<p>Enable the password protection interface</p>
</label>
<p>A settings screen which enables you to add password protection to the front of your blog (work in progress)</p>
</div>
</fieldset>
</form>
</section>

View file

@ -0,0 +1,29 @@
<header class="settings-view-header">
{{#link-to "settings" class="btn btn-default btn-back"}}Back{{/link-to}}
<h2 class="page-title">Password protect your blog</h2>
<section class="page-actions">
<button type="button" class="btn btn-blue" {{action "save"}}>Save</button>
</section>
</header>
<section class="content settings-pass">
<form id="settings-pass" novalidate="novalidate">
<fieldset>
<div class="form-group for-checkbox">
<label for="blog-isPrivate">Make this blog private</label>
<label class="checkbox" for="blog-isPrivate">
{{input id="blog-isPrivate" name="labs[passProtectUI]" type="checkbox"
checked=model.isPrivate}}
<span class="input-toggle-component"></span>
<p>Enable password protection</p>
</label>
</div>
{{#if model.isPrivate}}
<div class="form-group">
{{input name="private[password]" type="text" value=model.password}}
<p>This password will be needed to access your blog. All search engine optimization and social features are now disabled.</p>
</div>
{{/if}}
</fieldset>
</form>
</section>

View file

@ -0,0 +1,5 @@
import BaseView from 'ghost/views/settings/content-base';
var SettingsGeneralView = BaseView.extend();
export default SettingsGeneralView;

View file

@ -10,6 +10,7 @@ var _ = require('lodash'),
function getValidKeys() {
var validKeys = {
fileStorage: config.fileStorage === false ? false : true,
passProtectUI: config.passProtectUI === true ? true : false,
apps: config.apps === true ? true : false,
version: config.ghostVersion,
environment: process.env.NODE_ENV,

View file

@ -94,7 +94,7 @@ function urlPathForPost(post, permalinks) {
// Usage:
// urlFor('home', true) -> http://my-ghost-blog.com/
// E.g. /blog/ subdir
// urlFor({relativeUrl: '/my-static-page/') -> /blog/my-static-page/
// urlFor({relativeUrl: '/my-static-page/'}) -> /blog/my-static-page/
// E.g. if post object represents welcome post, and slugs are set to standard
// urlFor('post', {...}) -> /welcome-to-ghost/
// E.g. if post object represents welcome post, and slugs are set to date

View file

@ -14,6 +14,7 @@ var moment = require('moment'),
template = require('../helpers/template'),
errors = require('../errors'),
routeMatch = require('path-match')(),
path = require('path'),
frontendControllers,
staticPostPermalink;
@ -451,7 +452,23 @@ frontendControllers = {
return handleError(next)(err);
});
},
rss: rss
rss: rss,
private: function (req, res) {
var defaultPage = path.resolve(config.paths.adminViews, 'password.hbs');
return getActiveThemePaths().then(function (paths) {
var data = {
forward: req.query.r
};
if (res.error) {
data.error = res.error;
}
if (paths.hasOwnProperty('password.hbs')) {
return res.render('password', data);
} else {
return res.render(defaultPage, data);
}
});
}
};
module.exports = frontendControllers;

View file

@ -73,6 +73,12 @@
},
"navigation": {
"defaultValue": "[{\"label\":\"Home\", \"url\":\"/\"}]"
},
"isPrivate": {
"defaultValue": "false"
},
"password": {
"defaultValue": ""
}
},
"theme": {

View file

@ -249,7 +249,6 @@ setupMiddleware = function (blogAppInstance, adminApp) {
// Favicon
blogApp.use(serveSharedFile('favicon.ico', 'image/x-icon', utils.ONE_DAY_S));
blogApp.use(serveSharedFile('sitemap.xsl', 'text/xsl', utils.ONE_DAY_S));
// Static assets
blogApp.use('/shared', express['static'](path.join(corePath, '/shared'), {maxAge: utils.ONE_HOUR_MS}));
@ -274,6 +273,13 @@ setupMiddleware = function (blogAppInstance, adminApp) {
// Theme only config
blogApp.use(middleware.staticTheme());
// Check if password protected blog
blogApp.use(middleware.checkIsPrivate); // check if the blog is protected
blogApp.use(middleware.filterPrivateRoutes);
// Serve sitemap.xsl file
blogApp.use(serveSharedFile('sitemap.xsl', 'text/xsl', utils.ONE_DAY_S));
// Serve robots.txt if not found in theme
blogApp.use(serveSharedFile('robots.txt', 'text/plain', utils.ONE_HOUR_S));
@ -315,7 +321,7 @@ setupMiddleware = function (blogAppInstance, adminApp) {
blogApp.use('/ghost', adminApp);
// Set up Frontend routes
blogApp.use(routes.frontend());
blogApp.use(routes.frontend(middleware));
// ### Error handling
// 404 Handler

View file

@ -3,13 +3,17 @@
// middleware_spec.js
var _ = require('lodash'),
fs = require('fs'),
express = require('express'),
bcrypt = require('bcryptjs'),
busboy = require('./ghost-busboy'),
config = require('../config'),
path = require('path'),
api = require('../api'),
passport = require('passport'),
Promise = require('bluebird'),
errors = require('../errors'),
session = require('cookie-session'),
url = require('url'),
utils = require('../utils'),
@ -17,7 +21,8 @@ var _ = require('lodash'),
blogApp,
oauthServer,
loginSecurity = [],
forgottenSecurity = [];
forgottenSecurity = [],
protectedSecurity = [];
function isBlackListedFileType(file) {
var blackListedFileTypes = ['.hbs', '.md', '.json'],
@ -76,6 +81,17 @@ function sslForbiddenOrRedirect(opt) {
return response;
}
function verifySessionHash(hash) {
if (!hash) {
return Promise.resolve(false);
}
var bcryptCompare = Promise.promisify(bcrypt.compare);
return api.settings.read({context: {internal: true}, key: 'password'}).then(function (response) {
var pass = response.settings[0].value;
return bcryptCompare(pass, hash);
});
}
middleware = {
// ### Authenticate Middleware
@ -325,6 +341,132 @@ middleware = {
next();
},
checkIsPrivate: function (req, res, next) {
return api.settings.read({context: {internal: true}, key: 'isPrivate'}).then(function (response) {
var pass = response.settings[0];
if (_.isEmpty(pass.value) || pass.value === 'false') {
res.isPrivateBlog = false;
return next();
}
res.isPrivateBlog = true;
return session({
maxAge: utils.ONE_MONTH_MS,
keys: ['isPrivateBlog']
})(req, res, next);
});
},
filterPrivateRoutes: function (req, res, next) {
if (res.isAdmin || !res.isPrivateBlog || req.url.lastIndexOf('/private/', 0) === 0) {
return next();
}
if (req.url.lastIndexOf('/rss', 0) === 0 || req.url.lastIndexOf('/sitemap', 0) === 0) { // take care of rss and sitemap 404's
return errors.error404(req, res, next);
} else if (req.url.lastIndexOf('/robots.txt', 0) === 0) {
fs.readFile(path.join(config.paths.corePath, 'shared', 'private-robots.txt'), function (err, buf) {
if (err) {
return next(err);
}
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Length': buf.length,
'Cache-Control': 'public, max-age=' + utils.ONE_HOUR_MS
});
res.end(buf);
});
} else {
return middleware.authenticatePrivateSession(req, res, next);
}
},
authenticatePrivateSession: function (req, res, next) {
var clientHash = req.session.token || '';
return verifySessionHash(clientHash).then(function (isVerified) {
if (isVerified) {
return next();
} else {
return res.redirect(config.urlFor({relativeUrl: '/private/'}) + '?r=' + encodeURI(req.url));
}
});
},
// This is here so a call to /private/ after a session is verified will redirect to home;
isPrivateSessionAuth: function (req, res, next) {
if (!res.isPrivateBlog) {
return res.redirect(config.urlFor('home', true));
}
var hash = req.session.token || '';
return verifySessionHash(hash).then(function (isVerified) {
if (isVerified) {
return res.redirect(config.urlFor('home', true)); // redirect to home if user is already authenticated
} else {
return next();
}
});
},
spamProtectedPrevention: function (req, res, next) {
var currentTime = process.hrtime()[0],
remoteAddress = req.connection.remoteAddress,
rateProtectedPeriod = config.rateProtectedPeriod || 3600,
rateProtectedAttempts = config.rateProtectedAttempts || 10,
ipCount = '',
message = 'Too many attempts.',
deniedRateLimit = '',
password = req.body.password;
if (password) {
protectedSecurity.push({ip: remoteAddress, time: currentTime});
} else {
res.error = {
message: 'No password entered'
};
return next();
}
// filter entries that are older than rateProtectedPeriod
protectedSecurity = _.filter(protectedSecurity, function (logTime) {
return (logTime.time + rateProtectedPeriod > currentTime);
});
ipCount = _.chain(protectedSecurity).countBy('ip').value();
deniedRateLimit = (ipCount[remoteAddress] > rateProtectedAttempts);
if (deniedRateLimit) {
errors.logError(
'Only ' + rateProtectedAttempts + ' tries per IP address every ' + rateProtectedPeriod + ' seconds.',
'Too many login attempts.'
);
message += rateProtectedPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later';
res.error = {
message: message
};
}
return next();
},
authenticateProtection: function (req, res, next) {
if (res.error) { // if errors have been generated from the previous call
return next();
}
var bodyPass = req.body.password,
bcryptHash = Promise.promisify(bcrypt.hash);
return api.settings.read({context: {internal: true}, key: 'password'}).then(function (response) {
var pass = response.settings[0];
if (pass.value === bodyPass) {
return bcryptHash(pass.value, 10).then(function (hash) {
req.session.token = hash;
return res.redirect(config.urlFor({relativeUrl: decodeURI(req.body.forward)}));
});
} else {
res.error = {
message: 'Wrong password'
};
return next();
}
});
},
busboy: busboy
};

View file

@ -5,7 +5,7 @@ var frontend = require('../controllers/frontend'),
frontendRoutes;
frontendRoutes = function () {
frontendRoutes = function (middleware) {
var router = express.Router(),
subdir = config.paths.subdir,
routeKeywords = config.routeKeywords;
@ -28,6 +28,18 @@ frontendRoutes = function () {
res.redirect(subdir + '/ghost/');
});
// password-protected frontend route
router.get('/private/',
middleware.isPrivateSessionAuth,
frontend.private
);
router.post('/private/',
middleware.isPrivateSessionAuth,
middleware.spamProtectedPrevention,
middleware.authenticateProtection,
frontend.private
);
// ### Frontend routes
router.get('/rss/', frontend.rss);
router.get('/rss/:page/', frontend.rss);

View file

@ -24,6 +24,7 @@ utils = {
ONE_YEAR_S: 31536000,
ONE_HOUR_MS: 3600000,
ONE_DAY_MS: 86400000,
ONE_MONTH_MS: 2628000000,
ONE_YEAR_MS: 31536000000,
/**

View file

@ -0,0 +1,56 @@
<!doctype html>
<!--[if (IE 8)&!(IEMobile)]><html class="no-js lt-ie9" lang="en"><![endif]-->
<!--[if (gte IE 9)| IEMobile |!(IE)]><!--><html class="no-js" lang="en"><!--<![endif]-->
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>Ghost - Private Blog Access</title>
<meta name="description" content="{{siteDescription}}">
<meta name="author" content="">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="shortcut icon" href="{{asset "favicon.ico"}}">
<meta http-equiv="cleartype" content="on">
<link rel="stylesheet" type='text/css' href='//fonts.googleapis.com/css?family=Open+Sans:400,300,700'>
<link rel="stylesheet" href="{{asset "ghost.css" ghost="true" minifyInProduction="true"}}" />
</head>
<body class="ghost-setup">
<main role="main" id="main" class="viewport">
<section class="setup-box js-setup-box fade-in">
<div class="vertical">
<form id="setup" class="setup-form" method="post" novalidate="novalidate">
<header>
<h1>This blog is private</h1>
</header>
<input type="hidden" name="forward" value="{{forward}}">
<div class="form-group">
<input type="text" name="password" autofocus="autofocus" class="icon-lock">
</div>
<footer>
<button type="submit" class="btn btn-green btn-lg">
Enter
</button>
</footer>
</form>
</div>
</section>
</main>
{{#if error}}
<aside class="notification bottom">
<div class="js-bb-notification">
<section class="js-notification notification-error">
<span class="notification-message">{{error.message}}</span>
</section>
</div>
</aside>
{{/if}}
<script src="{{asset "ghost.js" ghost="true" minifyInProduction="true"}}"></script>
</body>
</html>

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -1438,4 +1438,79 @@ describe('Frontend Controller', function () {
}).catch(done);
});
});
describe('private', function () {
var req, res, config, defaultPath;
defaultPath = '/core/server/views/password.hbs';
beforeEach(function () {
res = {
locals: {verson: ''},
render: sandbox.spy()
},
req = {
query: {
r: ''
}
},
config = {
paths: {
adminViews: '/core/server/views',
availableThemes: {
casper: {}
}
}
};
apiSettingsStub = sandbox.stub(api.settings, 'read');
apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(Promise.resolve({
settings: [{
key: 'activeTheme',
value: 'casper'
}]
}));
});
it('Should render default password page when theme has no password template', function (done) {
frontend.__set__('config', config);
frontend.private(req, res, done).then(function () {
res.render.calledWith(defaultPath).should.be.true;
done();
}).catch(done);
});
it('Should render theme password page when it exists', function (done) {
config.paths.availableThemes.casper = {
'password.hbs': '/content/themes/casper/password.hbs'
};
frontend.__set__('config', config);
frontend.private(req, res, done).then(function () {
res.render.calledWith('password').should.be.true;
done();
}).catch(done);
});
it('Should render with forward data when it is passed in', function (done) {
frontend.__set__('config', config);
req.query.r = '/test-redirect/';
frontend.private(req, res, done).then(function () {
res.render.calledWith(defaultPath, {forward: '/test-redirect/'}).should.be.true;
done();
}).catch(done);
});
it('Should render with error when error is passed in', function (done) {
frontend.__set__('config', config);
res.error = 'Test Error';
frontend.private(req, res, done).then(function () {
res.render.calledWith(defaultPath, {forward: '', error: 'Test Error'}).should.be.true;
done();
}).catch(done);
});
});
});

View file

@ -3,9 +3,24 @@
var assert = require('assert'),
should = require('should'),
sinon = require('sinon'),
middleware = require('../../server/middleware').middleware;
Promise = require('bluebird'),
middleware = require('../../server/middleware').middleware,
api = require('../../server/api'),
errors = require('../../server/errors'),
bcrypt = require('bcryptjs'),
fs = require('fs');
describe('Middleware', function () {
var sandbox,
apiSettingsStub;
beforeEach(function () {
sandbox = sinon.sandbox.create();
});
afterEach(function () {
sandbox.restore();
});
// TODO: needs new test for ember admin
// describe('redirectToDashboard', function () {
// var req, res;
@ -236,4 +251,209 @@ describe('Middleware', function () {
response.redirectUrl({a: 'b'}).should.equal('https://example.com/ssl/config/path/req/path?a=b');
});
});
describe('passProtect', function () {
var req, res, next;
beforeEach(function () {
req = {}, res = {};
apiSettingsStub = sandbox.stub(api.settings, 'read');
next = sinon.spy();
});
it('checkIsPrivate should call next if not private', function (done) {
apiSettingsStub.withArgs(sinon.match.has('key', 'isPrivate')).returns(Promise.resolve({
settings: [{
key: 'isPrivate',
value: 'false'
}]
}));
middleware.checkIsPrivate(req, res, next).then(function () {
next.called.should.be.true;
res.isPrivateBlog.should.be.false;
done();
});
});
it('checkIsPrivate should load session if private', function (done) {
apiSettingsStub.withArgs(sinon.match.has('key', 'isPrivate')).returns(Promise.resolve({
settings: [{
key: 'isPrivate',
value: 'true'
}]
}));
middleware.checkIsPrivate(req, res, next).then(function () {
res.isPrivateBlog.should.be.true;
done();
});
});
describe('not private', function () {
beforeEach(function () {
res.isPrivateBlog = false;
});
it('filterPrivateRoutes should call next if not private', function () {
middleware.filterPrivateRoutes(req, res, next);
next.called.should.be.true;
});
it('isPrivateSessionAuth should redirect if blog is not private', function () {
res = {
redirect: sinon.spy(),
isPrivateBlog: false
};
middleware.isPrivateSessionAuth(req, res, next);
res.redirect.called.should.be.true;
});
});
describe('private', function () {
var errorSpy;
beforeEach(function () {
res.isPrivateBlog = true;
errorSpy = sandbox.spy(errors, 'error404');
res = {
status: function () {
return this;
},
send: function () {},
set: function () {},
isPrivateBlog: true
};
});
it('filterPrivateRoutes should call next if admin', function () {
res.isAdmin = true;
middleware.filterPrivateRoutes(req, res, next);
next.called.should.be.true;
});
it('filterPrivateRoutes should call next if is the "private" route', function () {
req.url = '/private/';
middleware.filterPrivateRoutes(req, res, next);
next.called.should.be.true;
});
it('filterPrivateRoutes should throw 404 if url is sitemap', function () {
req.url = '/sitemap.xml';
middleware.filterPrivateRoutes(req, res, next);
errorSpy.called.should.be.true;
});
it('filterPrivateRoutes should throw 404 if url is rss', function () {
req.url = '/rss';
middleware.filterPrivateRoutes(req, res, next);
errorSpy.called.should.be.true;
});
it('filterPrivateRoutes should throw 404 if url is rss plus something', function () {
req.url = '/rss/sometag';
middleware.filterPrivateRoutes(req, res, next);
errorSpy.called.should.be.true;
});
it('filterPrivateRoutes should render custom robots.txt', function () {
req.url = '/robots.txt';
res.writeHead = sinon.spy();
res.end = sinon.spy();
sandbox.stub(fs, 'readFile', function (file, cb) {
cb(null, 'User-agent: * Disallow: /');
});
middleware.filterPrivateRoutes(req, res, next);
res.writeHead.called.should.be.true;
res.end.called.should.be.true;
});
it('authenticateProtection should call next if error', function () {
res.error = 'Test Error';
middleware.authenticateProtection(req, res, next);
next.called.should.be.true;
});
describe('with hash verification', function () {
beforeEach(function () {
sandbox.stub(bcrypt, 'compare', function (check, hash, cb) {
var isVerified = (check === hash) ? true : false;
cb(null, isVerified);
});
apiSettingsStub.withArgs(sinon.match.has('key', 'password')).returns(Promise.resolve({
settings: [{
key: 'password',
value: 'rightpassword'
}]
}));
});
it('authenticatePrivateSession should return next if hash is verified', function (done) {
req.session = {
token: 'rightpassword'
};
middleware.authenticatePrivateSession(req, res, next).then(function () {
next.called.should.be.true;
done();
});
});
it('authenticatePrivateSession should redirect if hash is not verified', function (done) {
req.url = '/welcome-to-ghost';
req.session = {
token: 'wrongpassword'
};
res.redirect = sinon.spy();
middleware.authenticatePrivateSession(req, res, next).then(function () {
res.redirect.called.should.be.true;
done();
});
});
it('isPrivateSessionAuth should redirect if hash is verified', function (done) {
req.session = {
token: 'rightpassword'
};
res.redirect = sandbox.spy();
middleware.isPrivateSessionAuth(req, res, next).then(function () {
res.redirect.called.should.be.true;
done();
});
});
it('isPrivateSessionAuth should return next if hash is not verified', function (done) {
req.session = {
token: 'wrongpassword'
};
middleware.isPrivateSessionAuth(req, res, next).then(function () {
next.called.should.be.true;
done();
});
});
it('authenticateProtection should return next if password is incorrect', function (done) {
req.body = {password:'wrongpassword'};
middleware.authenticateProtection(req, res, next).then(function () {
res.error.should.not.be.empty;
next.called.should.be.true;
done();
});
});
it('authenticateProtection should redirect if password is correct', function (done) {
req.body = {password:'rightpassword'};
req.session = {};
res.redirect = sandbox.spy();
sandbox.stub(bcrypt, 'hash', function (pass, salt, cb) {
cb(null, pass + 'hash');
});
middleware.authenticateProtection(req, res, next).then(function () {
req.session.token.should.equal('rightpasswordhash');
res.redirect.called.should.be.true;
done();
});
});
});
});
});
});

View file

@ -31,7 +31,7 @@
"iojs": "~1.2.0"
},
"dependencies": {
"bcryptjs": "2.0.2",
"bcryptjs": "2.1.0",
"bluebird": "2.4.2",
"body-parser": "1.10.0",
"bookshelf": "0.7.9",
@ -40,6 +40,7 @@
"colors": "0.6.2",
"compression": "1.2.2",
"connect-slashes": "1.3.0",
"cookie-session": "^1.1.0",
"downsize": "0.0.8",
"express": "4.12.0",
"express-hbs": "0.8.4",