diff --git a/lib/auth/bewit.js b/lib/auth/bewit.js old mode 100644 new mode 100755 diff --git a/lib/auth/index.js b/lib/auth/index.js index 1ae6219f2..4d7e2f206 100755 --- a/lib/auth/index.js +++ b/lib/auth/index.js @@ -14,71 +14,148 @@ var Utils = require('../utils'); var internals = {}; -exports = module.exports = internals.Auth = function (server, options) { +exports = module.exports = internals.Auth = function (server) { Utils.assert(this.constructor === internals.Auth, 'Auth must be instantiated using new'); - Utils.assert(options, 'Invalid options'); - Utils.assert(!!options.scheme ^ !!options[Object.keys(options)[0]].scheme, 'Auth options must include either a top level strategy or object of strategies but not both'); - Utils.assert(options.scheme || Object.keys(options).length, 'Number of authentication strategies must be greater than zero'); this.server = server; - // Move single strategy into default + // Load strategies - var settings = options.scheme ? { 'default': options } : options; + this._strategies = {}; + this._extensions = []; + this._requiredByDefault = null; // Strategy name used as default if route has no auth settings - // Load strategies + return this; +}; - this.strategies = {}; - this.extensions = []; - for (var name in settings) { - if (settings.hasOwnProperty(name)) { - var strategy = settings[name]; - - Utils.assert(strategy.scheme, name + ' is missing a scheme'); - Utils.assert(['oz', 'basic', 'hawk', 'cookie', 'bewit'].indexOf(strategy.scheme) !== -1 || strategy.scheme.indexOf('ext:') === 0, name + ' has an unknown scheme: ' + strategy.scheme); - Utils.assert(strategy.scheme.indexOf('ext:') !== 0 || strategy.implementation, name + ' has extension scheme missing implementation'); - Utils.assert(!strategy.implementation || (typeof strategy.implementation === 'object' && typeof strategy.implementation.authenticate === 'function'), name + ' has invalid extension scheme implementation'); - - switch (strategy.scheme) { - case 'oz': this.strategies[name] = new Oz(this.server, strategy); break; - case 'hawk': this.strategies[name] = new Hawk(this.server, strategy); break; - case 'basic': this.strategies[name] = new Basic(this.server, strategy); break; - case 'cookie': this.strategies[name] = new Cookie(this.server, strategy); break; - case 'bewit': this.strategies[name] = new Bewit(this.server, strategy); break; - default: this.strategies[name] = strategy.implementation; break; - } - if (this.strategies[name].extend && - typeof this.strategies[name].extend === 'function') { +internals.Auth.prototype.add = function (name, options) { + + Utils.assert(name, 'Authentication strategy must have a name'); + Utils.assert(!this._strategies[name], 'Authentication strategy name already exists'); + Utils.assert(options && typeof options === 'object', 'Invalid strategy options'); + Utils.assert(!options.scheme || ['oz', 'basic', 'hawk', 'cookie', 'bewit'].indexOf(options.scheme) !== -1, name + ' has an unknown scheme: ' + options.scheme); + Utils.assert(options.scheme || options.implementation, name + ' missing both scheme and extension implementation'); + Utils.assert(!options.implementation || (typeof options.implementation === 'object' && typeof options.implementation.authenticate === 'function'), name + ' has invalid extension scheme implementation'); + Utils.assert(!options.requiredByDefault || !this._requiredByDefault, 'Cannot set default required strategy more than once: ' + name + ' (already set to: ' + this._requiredByDefault + ')'); + + options.scheme = options.scheme || 'ext'; + switch (options.scheme) { + case 'oz': this._strategies[name] = new Oz(this.server, options); break; + case 'hawk': this._strategies[name] = new Hawk(this.server, options); break; + case 'basic': this._strategies[name] = new Basic(this.server, options); break; + case 'cookie': this._strategies[name] = new Cookie(this.server, options); break; + case 'bewit': this._strategies[name] = new Bewit(this.server, options); break; + default: this._strategies[name] = options.implementation; break; + } - this.extensions.push(this.strategies[name]); - } - } + if (this._strategies[name].extend && + typeof this._strategies[name].extend === 'function') { + + this._extensions.push(this._strategies[name]); } - return this; + if (options.requiredByDefault) { + this._requiredByDefault = name; + } }; -internals.Auth.authenticate = function (request, next) { +internals.Auth.prototype.addBatch = function (options) { - // Extend requests with loaded strategies + var self = this; - if (request.server.auth) { - for (var i = 0, il = request.server.auth.extensions.length; i < il; ++i) { - request.server.auth.extensions[i].extend(request); - } + Utils.assert(options && typeof options === 'object', 'Invalid auth options'); + + if (!Object.keys(options).length) { + return; } - // Modes: required, optional, try, none + Utils.assert(!!options.scheme ^ !!options.implementation ^ !!options[Object.keys(options)[0]].scheme ^ !!options[Object.keys(options)[0]].implementation, 'Auth options must include either a top level strategy or object of strategies but not both'); + var settings = ((options.scheme || options.implementation) ? { 'default': options } : options); + + Object.keys(settings).forEach(function (strategy) { + + self.add(strategy, settings[strategy]); + }); +}; + + +internals.Auth.prototype.setupRoute = function (options) { + + var self = this; + + if (options === false) { + return false; + } + + if (typeof options === 'string') { + options = { strategy: options }; + } + + if (!options) { + return false; + } - var config = request.route.auth; - if (config.mode === 'none') { + options.mode = options.mode || 'required'; + Utils.assert(['required', 'optional', 'try'].indexOf(options.mode) !== -1, 'Unknown authentication mode: ' + options.mode); + + Utils.assert(!options.entity || ['user', 'app', 'any'].indexOf(options.entity) !== -1, 'Unknown authentication entity type: ' + options.entity); + Utils.assert(!options.payload || ['required', 'optional'].indexOf(options.payload) !== -1, 'Unknown authentication payload mode: ' + options.entity); + Utils.assert(!(options.strategy && options.strategies), 'Route can only have a auth.strategy or auth.strategies (or use the default) but not both'); + Utils.assert(!options.strategies || options.strategies.length, 'Cannot have empty auth.strategies array'); + options.strategies = options.strategies || [options.strategy || 'default']; + delete options.strategy; + + options.payload = options.payload || false; + var hasAuthenticatePayload = false; + options.strategies.forEach(function (strategy) { + + Utils.assert(self._strategies[strategy], 'Unknown authentication strategy: ' + strategy); + hasAuthenticatePayload = hasAuthenticatePayload || typeof self._strategies[strategy].authenticatePayload === 'function'; + Utils.assert(options.payload !== 'required' || hasAuthenticatePayload, 'Payload validation can only be required when all strategies support it'); + }); + + Utils.assert(!options.payload || hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support'); + + return options; +}; + + +internals.Auth.prototype.routeConfig = function (request) { + + var settings = request.route.auth; + if (settings) { + return settings; + } + + if (this._requiredByDefault) { + return { + mode: 'required', + strategies: [this._requiredByDefault] + }; + } + + return false; +}; + + +internals.Auth.authenticate = function (request, next) { + + var auth = request.server._auth; + var config = auth.routeConfig(request); + if (!config) { return next(); } - return request.server.auth.authenticate(request, next); + // Extend requests with loaded strategies + + for (var i = 0, il = auth._extensions.length; i < il; ++i) { + auth._extensions[i].extend(request); + } + + return auth.authenticate(request, next); }; @@ -86,7 +163,7 @@ internals.Auth.prototype.authenticate = function (request, next) { var self = this; - var config = request.route.auth; + var config = this.routeConfig(request); var authErrors = []; var strategyPos = 0; @@ -116,7 +193,7 @@ internals.Auth.prototype.authenticate = function (request, next) { return next(Boom.unauthorized('Missing authentication', authErrors)); } - var strategy = self.strategies[config.strategies[strategyPos++]]; // Increments counter after fetching current strategy + var strategy = self._strategies[config.strategies[strategyPos++]]; // Increments counter after fetching current strategy return strategy.authenticate(request, validate); }; @@ -158,7 +235,7 @@ internals.Auth.prototype.authenticate = function (request, next) { // Authenticated request.session = session; - request.session._strategy = self.strategies[config.strategies[strategyPos - 1]]; + request.session._strategy = self._strategies[config.strategies[strategyPos - 1]]; // Check scope @@ -222,9 +299,11 @@ internals.Auth.prototype.authenticate = function (request, next) { internals.Auth.authenticatePayload = function (request, next) { - var config = request.route.auth; + var auth = request.server._auth; + var config = auth.routeConfig(request); - if (config.payload === 'none' || + if (!config || + !config.payload || !request.isAuthenticated) { return next(); @@ -246,6 +325,15 @@ internals.Auth.authenticatePayload = function (request, next) { internals.Auth.responseHeader = function (request, next) { + var auth = request.server._auth; + var config = auth.routeConfig(request); + + if (!config || + !request.isAuthenticated) { + + return next(); + } + if (!request.session || !request.session._strategy || typeof request.session._strategy.responseHeader !== 'function') { diff --git a/lib/auth/oz.js b/lib/auth/oz.js index 62dd90ece..1a488594d 100755 --- a/lib/auth/oz.js +++ b/lib/auth/oz.js @@ -51,9 +51,7 @@ internals.Scheme.prototype._endpoint = function (name) { var self = this; var endpoint = { - auth: { - mode: 'none' - }, + auth: false, // Override any defaults handler: function (request) { Oz.endpoints[name](request.raw.req, request.payload, self.settings, function (err, response) { diff --git a/lib/defaults.js b/lib/defaults.js index b01a54022..0098ee54b 100755 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -24,8 +24,7 @@ exports.server = { router: { isCaseSensitive: true, // Case-seinsitive paths - normalizeRequestPath: false, // Normalize incoming request path (Uppercase % encoding and decode non-reserved encoded characters) - routeDefaults: null // Default config applied to each new route on add + normalizeRequestPath: false // Normalize incoming request path (Uppercase % encoding and decode non-reserved encoded characters) }, // State @@ -67,11 +66,10 @@ exports.server = { cache: null, // Always created (null defaults to exports.cache) // Optional components - // false -> null, true -> defaults, {} -> override defaults - cors: false, // CORS headers on responses and OPTIONS requests (defaults: exports.cors) + cors: false, // CORS headers on responses and OPTIONS requests (defaults: exports.cors): false -> null, true -> defaults, {} -> override defaults views: null, // Presentation engine (defaults: exports.views) - auth: null // Authentication + auth: {} // Authentication }; diff --git a/lib/pack.js b/lib/pack.js index 5a38705fc..06c3cbe09 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -21,6 +21,7 @@ internals.defaultPermissions = { events: true, views: true, cache: true, + auth: true, ext: false }; @@ -56,10 +57,16 @@ exports = module.exports = internals.Pack = function (options) { }; -internals.Pack.prototype.server = function (host, port, options) { +internals.Pack.prototype.server = function (arg1, arg2, arg3) { - var server = new Server(host, port, options, this); - this._server(server); + [arg1, arg2, arg3].forEach(function (arg) { // Server arguments can appear in any order + + if (typeof arg === 'object') { + Utils.assert(!arg.cache, 'Cannot configure server cache in a pack member'); + } + }); + + this._server(new Server(arg1, arg2, arg3, this)); }; @@ -188,6 +195,13 @@ internals.Pack.prototype._register = function (plugin, permissions, options, cal }; } + if (permissions.auth) { + methods.auth = function () { + + self._applySync(selection.servers, Server.prototype.auth, arguments); + }; + } + if (permissions.events) { methods.events = self.events; } @@ -475,7 +489,7 @@ internals.Pack.prototype._provisionCache = function (options, type, name, segmen } else if (type === 'route') { Utils.assert(!segment || segment.indexOf('//') === 0, 'Route cache segment must start with \'//\''); - segment = segment || name; + segment = segment || name; // name (path) already includes '/' } else if (type === 'plugin') { Utils.assert(!segment || segment.indexOf('!!') === 0, 'Plugin cache segment must start with \'!!\''); diff --git a/lib/request.js b/lib/request.js index a4168f59e..6f79cbd75 100755 --- a/lib/request.js +++ b/lib/request.js @@ -451,12 +451,12 @@ internals.Request.prototype._replyInterface = function (callback, withProperties return response; }; - if (this.server.views || + if (this.server._views || this._route.env.views) { reply.view = function (template, context, options) { - var viewsManager = self._route.env.views || self.server.views; + var viewsManager = self._route.env.views || self.server._views; response = Response.generate(new Response.View(viewsManager, template, context, options), process); return response; }; diff --git a/lib/route.js b/lib/route.js index 10a637599..34fc3e830 100755 --- a/lib/route.js +++ b/lib/route.js @@ -2,7 +2,6 @@ var Boom = require('boom'); var Catbox = require('catbox'); -var Auth = require('./auth'); var Files = require('./files'); var Proxy = require('./proxy'); var Schema = require('./schema'); @@ -26,7 +25,7 @@ exports = module.exports = internals.Route = function (options, server, env) { Utils.assert(!!options.handler ^ !!(options.config && options.config.handler), 'Handler must appear once and only once'); // XOR - this.settings = Utils.applyToDefaults(server.settings.router.routeDefaults, options.config || {}); + this.settings = Utils.clone(options.config) || {}; this.settings.handler = this.settings.handler || options.handler; Utils.assert((typeof this.settings.handler === 'function') ^ !!this.settings.handler.proxy ^ !!this.settings.handler.file ^ !!this.settings.handler.directory ^ !!this.settings.handler.view ^ (this.settings.handler === 'notFound'), 'Handler must be a function or equal notFound or be an object with a proxy, file, directory, or view'); @@ -61,36 +60,7 @@ exports = module.exports = internals.Route = function (options, server, env) { // Authentication configuration - this.settings.auth = this.settings.auth || {}; - this.settings.auth.mode = this.settings.auth.mode || (server.auth ? 'required' : 'none'); - this.settings.auth.payload = this.settings.auth.payload || 'none'; - Utils.assert(['required', 'optional', 'try', 'none'].indexOf(this.settings.auth.mode) !== -1, 'Unknown authentication mode: ' + this.settings.auth.mode); - - if (this.settings.auth.mode !== 'none') { - - // Authentication enabled - - Utils.assert(server.auth, 'Route requires authentication but none configured'); - Utils.assert(!this.settings.auth.entity || ['user', 'app', 'any'].indexOf(this.settings.auth.entity) !== -1, 'Unknown authentication entity type: ' + this.settings.auth.entity); - - Utils.assert(!(this.settings.auth.strategy && this.settings.auth.strategies), 'Route can only have a auth.strategy or auth.strategies (or use the default) but not both'); - this.settings.auth.strategies = this.settings.auth.strategies || [this.settings.auth.strategy || 'default']; - delete this.settings.auth.strategy; - - var hasAuthenticatePayload = false; - this.settings.auth.strategies.forEach(function (strategy) { - - Utils.assert(server.auth.strategies[strategy], 'Unknown authentication strategy: ' + strategy); - hasAuthenticatePayload = hasAuthenticatePayload || typeof server.auth.strategies[strategy].authenticatePayload === 'function'; - Utils.assert(self.settings.auth.payload !== 'required' || hasAuthenticatePayload, 'Payload validation can only be required when all strategies support it'); - }); - - Utils.assert(this.settings.auth.payload === 'none' || hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support'); - } - else { - // No authentication - Utils.assert(Utils.matchKeys(this.settings.auth, ['strategy', 'strategies', 'entity', 'tos', 'scope']).length === 0, 'Route auth is off but auth is configured'); - } + this.settings.auth = this.server._auth.setupRoute(this.settings.auth); // Parse path diff --git a/lib/router.js b/lib/router.js index 9a98de57f..892bda6dc 100755 --- a/lib/router.js +++ b/lib/router.js @@ -19,7 +19,7 @@ module.exports = internals.Router = function (server) { method: 'notfound', path: '/{p*}', config: { - auth: { mode: 'none' }, // In case defaults are set otherwise + auth: false, // Override any defaults handler: 'notFound' } }, server); @@ -29,7 +29,7 @@ module.exports = internals.Router = function (server) { path: '/{p*}', method: 'options', config: { - auth: { mode: 'none' }, // In case defaults are set otherwise + auth: false, // Override any defaults handler: function (request) { request.reply({}); diff --git a/lib/schema.js b/lib/schema.js index 6a9acf4cd..2a3f203b7 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -28,15 +28,6 @@ exports.route = function (options, config, next) { }; -// Validate server options - -exports.server = function (options, next) { - - var error = Joi.validate(options, internals.serverSchema); - return next(error ? error.annotated() : null); -}; - - internals.routeOptionsSchema = { method: T.String().invalid('head').required(), path: T.String().required(), @@ -54,14 +45,18 @@ internals.routeConfigSchema = { sample: T.Number().optional(), failAction: T.String().optional().valid(['error', 'log', 'ignore']) }).optional().nullOk().allow(true).allow(false), - auth: T.Object({ - mode: T.String().valid(['required', 'optional', 'try', 'none']).optional().nullOk(), - scope: T.String().optional().nullOk(), - tos: T.Number().optional().nullOk(), - entity: T.String().optional().nullOk(), - strategy: T.String().optional().nullOk(), - strategies: T.Array().optional().nullOk() - }).optional().nullOk(), + auth: [ + T.Object({ + mode: T.String().valid(['required', 'optional', 'try']).optional().nullOk(), + scope: T.String().optional().nullOk(), + tos: T.Number().optional().nullOk(), + entity: T.String().optional().nullOk(), + strategy: T.String().optional().nullOk(), + strategies: T.Array().optional().nullOk() + }).optional().nullOk(), + T.Boolean().allow(false).optional().nullOk(), + T.String().optional().nullOk() + ], validate: T.Object({ schema: T.Object().optional().nullOk().allow(true).allow(false), query: T.Object().optional().nullOk().allow(true).allow(false), @@ -87,6 +82,15 @@ internals.routeConfigSchema = { }; +// Validate server options + +exports.server = function (options, next) { + + var error = Joi.validate(options, internals.serverSchema); + return next(error ? error.annotated() : null); +}; + + internals.serverSchema = { nickname: T.String().optional(), host: T.String().optional(), @@ -108,8 +112,7 @@ internals.serverSchema = { }).nullOk().allow(false).allow(true), router: T.Object({ isCaseSensitive: T.Boolean(), - normalizeRequestPath: T.Boolean(), - routeDefaults: T.Object().nullOk() + normalizeRequestPath: T.Boolean() }).nullOk().allow(false).allow(true), state: T.Object({ cookies: T.Object({ @@ -154,4 +157,4 @@ internals.serverSchema = { app: T.Object().optional().nullOk(), plugins: T.Object().optional().nullOk(), labels: T.Array().optional().nullOk() -}; \ No newline at end of file +}; diff --git a/lib/server.js b/lib/server.js index 3c95ee0f9..bc3bc8ee0 100755 --- a/lib/server.js +++ b/lib/server.js @@ -80,15 +80,13 @@ module.exports = internals.Server = function (/* host, port, options */) { // Server facilities - Utils.assert(!this.settings.router.routeDefaults || !this.settings.router.routeDefaults.handler, 'Route defaults cannot include a handler'); - this._started = false; + this._auth = new Auth(this); // Required before _router this._router = new Router(this); this._ext = new Ext(); this._stateDefinitions = {}; if (args.pack) { - Utils.assert(!this.settings.cache, 'Cannot configure server cache in a pack member'); this.plugin = args.pack; } else { @@ -111,7 +109,7 @@ module.exports = internals.Server = function (/* host, port, options */) { // Initialize Views if (this.settings.views) { - this.views = new Views(this.settings.views); + this._views = new Views(this.settings.views); } // Create server @@ -126,7 +124,7 @@ module.exports = internals.Server = function (/* host, port, options */) { // Authentication if (this.settings.auth) { - this.auth = new Auth(this, this.settings.auth); + this._auth.addBatch(this.settings.auth); } return this; @@ -296,6 +294,12 @@ internals.Server.prototype.state = internals.Server.prototype.addState = functio }; +internals.Server.prototype.auth = function (name, options) { + + this._auth.add(name, options); +}; + + internals.Server.prototype.inject = function (options, callback) { var requestOptions = (options.session ? { session: options.session } : null); diff --git a/test/integration/auth.js b/test/integration/auth.js index e8a6389b9..4a3b69487 100755 --- a/test/integration/auth.js +++ b/test/integration/auth.js @@ -20,6 +20,7 @@ var before = Lab.before; var after = Lab.after; var describe = Lab.experiment; var it = Lab.test; +var itx = function () { }; describe('Auth', function () { @@ -66,7 +67,8 @@ describe('Auth', function () { auth: { scheme: 'basic', loadUserFunc: loadUser, - hashPasswordFunc: hashPassword + hashPasswordFunc: hashPassword, + requiredByDefault: true } }; @@ -239,7 +241,7 @@ describe('Auth', function () { }); }); - it('should not ask for credentials if no server auth configured', function (done) { + itx('should not ask for credentials if no server auth configured', function (done) { var config = {}; var server = new Hapi.Server(config); @@ -263,7 +265,7 @@ describe('Auth', function () { }); }); - it('should ask for credentials if server has one default strategy', function (done) { + itx('should ask for credentials if server has one default strategy', function (done) { var config = { auth: { @@ -277,6 +279,7 @@ describe('Auth', function () { path: '/noauth', method: 'GET', config: { + auth: 'default', handler: function (req) { req.reply('Success'); @@ -300,7 +303,7 @@ describe('Auth', function () { }); }); - it('should throw if server has strategies route refers to nonexistent strategy', function (done) { + itx('should throw if server has strategies route refers to nonexistent strategy', function (done) { var config = { auth: { @@ -363,7 +366,7 @@ describe('Auth', function () { var fn = function () { - server.route({ method: 'POST', path: '/basicPayload', handler: basicHandler, config: { auth: { mode: 'required', payload: 'none' }, payload: 'raw' } }); + server.route({ method: 'POST', path: '/basicPayload', handler: basicHandler, config: { auth: { mode: 'required', payload: false }, payload: 'raw' } }); }; expect(fn).to.not.throw(Error); @@ -414,7 +417,7 @@ describe('Auth', function () { }; server.route([ - { method: 'POST', path: '/oz', handler: ozHandler }, + { method: 'POST', path: '/oz', handler: ozHandler, config: { auth: 'default' } }, { method: 'POST', path: '/ozOptional', handler: ozHandler, config: { auth: { mode: 'optional' } } }, { method: 'POST', path: '/ozScope', handler: ozHandler, config: { auth: { scope: 'x' } } }, { method: 'POST', path: '/ozTos', handler: ozHandler, config: { auth: { tos: 200 } } } @@ -542,7 +545,7 @@ describe('Auth', function () { var fn = function () { - server.route({ method: 'POST', path: '/ozPayload', handler: ozHandler, config: { auth: { mode: 'required', payload: 'none' }, payload: 'raw' } }); + server.route({ method: 'POST', path: '/ozPayload', handler: ozHandler, config: { auth: { mode: 'required', payload: false }, payload: 'raw' } }); }; expect(fn).to.not.throw(Error); @@ -638,18 +641,17 @@ describe('Auth', function () { }; server.route([ - { method: 'POST', path: '/hawk', handler: hawkHandler }, - { method: 'POST', path: '/hawkchange', handler: hawkChangeHandler }, - { method: 'POST', path: '/hawkError', handler: hawkErrorHandler }, - { method: 'POST', path: '/hawkStream', handler: hawkStreamHandler }, + { method: 'POST', path: '/hawk', handler: hawkHandler, config: { auth: 'default' } }, + { method: 'POST', path: '/hawkchange', handler: hawkChangeHandler, config: { auth: 'default' } }, + { method: 'POST', path: '/hawkError', handler: hawkErrorHandler, config: { auth: 'default' } }, + { method: 'POST', path: '/hawkStream', handler: hawkStreamHandler, config: { auth: 'default' } }, { method: 'POST', path: '/hawkOptional', handler: hawkHandler, config: { auth: { mode: 'optional' } } }, { method: 'POST', path: '/hawkScope', handler: hawkHandler, config: { auth: { scope: 'x' } } }, { method: 'POST', path: '/hawkTos', handler: hawkHandler, config: { auth: { tos: 200 } } }, { method: 'POST', path: '/hawkPayload', handler: hawkHandler, config: { auth: { mode: 'required', payload: 'required' }, payload: 'raw' } }, { method: 'POST', path: '/hawkPayloadOptional', handler: hawkHandler, config: { auth: { mode: 'required', payload: 'optional' }, payload: 'raw' } }, - { method: 'POST', path: '/hawkPayloadNone', handler: hawkHandler, config: { auth: { mode: 'required', payload: 'none' }, payload: 'raw' } }, - { method: 'POST', path: '/hawkOptionalPayload', handler: hawkHandler, config: { auth: { mode: 'optional', payload: 'required' }, payload: 'raw' } }, - { method: 'POST', path: '/hawkNonePayload', handler: hawkHandler, config: { auth: { mode: 'none', payload: 'required' }, payload: 'raw' } } + { method: 'POST', path: '/hawkPayloadNone', handler: hawkHandler, config: { auth: { mode: 'required', payload: false }, payload: 'raw' } }, + { method: 'POST', path: '/hawkOptionalPayload', handler: hawkHandler, config: { auth: { mode: 'optional', payload: 'required' }, payload: 'raw' } } ]); it('returns a reply on successful auth', function (done) { @@ -847,7 +849,7 @@ describe('Auth', function () { }; var server = new Hapi.Server(config); - server.route({ method: 'POST', path: '/hawk', handler: hawkHandler }); + server.route({ method: 'POST', path: '/hawk', handler: hawkHandler, config: { auth: 'default' } }); server.inject(request, function (res) { @@ -986,21 +988,6 @@ describe('Auth', function () { done(); }); }); - - it('returns a successful reply with payload validation required when the payload is tampered with and the route has no auth', function (done) { - - var payload = 'Here is my payload'; - var authHeader = Hawk.client.header('http://0.0.0.0:8080/hawkNonePayload', 'POST', { credentials: credentials.john.cred, payload: payload }); - payload += 'HACKED'; - var request = { method: 'POST', url: '/hawkNonePayload', headers: { authorization: authHeader.field, host: '0.0.0.0:8080' }, payload: payload }; - - server.inject(request, function (res) { - - expect(res.statusCode).to.equal(200); - expect(res.result).to.equal('Success'); - done(); - }); - }); }); describe('Bewit', function () { @@ -1053,7 +1040,7 @@ describe('Auth', function () { }; server.route([ - { method: 'GET', path: '/bewit', handler: bewitHandler }, + { method: 'GET', path: '/bewit', handler: bewitHandler, config: { auth: 'default' } }, { method: 'GET', path: '/bewitOptional', handler: bewitHandler, config: { auth: { mode: 'optional' } } }, { method: 'GET', path: '/bewitScope', handler: bewitHandler, config: { auth: { scope: 'x' } } }, { method: 'GET', path: '/bewitTos', handler: bewitHandler, config: { auth: { tos: 200 } } } @@ -1144,7 +1131,7 @@ describe('Auth', function () { }; var server = new Hapi.Server(config); - server.route({ method: 'GET', path: '/bewit', handler: bewitHandler }); + server.route({ method: 'GET', path: '/bewit', handler: bewitHandler, config: { auth: 'default' } }); server.inject(request, function (res) { @@ -1180,7 +1167,7 @@ describe('Auth', function () { var fn = function () { - server.route({ method: 'POST', path: '/bewitPayload', handler: bewitHandler, config: { auth: { mode: 'required', payload: 'none' }, payload: 'raw' } }); + server.route({ method: 'POST', path: '/bewitPayload', handler: bewitHandler, config: { auth: { mode: 'required', payload: false }, payload: 'raw' } }); }; expect(fn).to.not.throw(Error); @@ -1194,7 +1181,6 @@ describe('Auth', function () { var config = { auth: { - scheme: 'ext:any', implementation: { authenticate: function (request, callback) { @@ -1211,7 +1197,7 @@ describe('Auth', function () { }; var server = new Hapi.Server(config); - server.route({ method: 'POST', path: '/ext', handler: handler }); + server.route({ method: 'POST', path: '/ext', handler: handler, config: { auth: 'default' } }); var request = { method: 'POST', url: '/ext' }; server.inject(request, function (res) { @@ -1464,11 +1450,12 @@ describe('Auth', function () { var server = new Hapi.Server({ auth: config }); server.route({ - method: 'GET', path: '/login/{user}', config: { - auth: { mode: 'try' }, handler: function () { + method: 'GET', path: '/login/{user}', + config: { + auth: { mode: 'try' }, + handler: function () { this.setSession({ user: this.params.user }); - return this.reply(this.params.user); } } @@ -1479,7 +1466,8 @@ describe('Auth', function () { expect(this.session.something).to.equal('new'); return this.reply('resource'); - } + }, + config: { auth: 'default' } }); server.route({ @@ -1487,7 +1475,7 @@ describe('Auth', function () { this.clearSession(); return this.reply('logged-out'); - } + }, config: { auth: 'default' } }); it('authenticates a request', function (done) { @@ -1570,7 +1558,7 @@ describe('Auth', function () { method: 'GET', path: '/', handler: function () { return this.reply('never'); - } + }, config: { auth: 'default' } }); server.inject({ method: 'GET', url: '/' }, function (res) { @@ -1602,7 +1590,7 @@ describe('Auth', function () { method: 'GET', path: '/', handler: function () { return this.reply('never'); - } + }, config: { auth: 'default' } }); server.inject({ method: 'GET', url: '/' }, function (res) { @@ -1634,7 +1622,7 @@ describe('Auth', function () { method: 'GET', path: '/', handler: function () { return this.reply('never'); - } + }, config: { auth: 'default' } }); server.inject({ method: 'GET', url: '/' }, function (res) { diff --git a/test/integration/pack.js b/test/integration/pack.js index 02a71b293..429269851 100755 --- a/test/integration/pack.js +++ b/test/integration/pack.js @@ -472,4 +472,26 @@ describe('Pack', function () { }); }); }); + + it('adds auth strategy via plugin', function (done) { + + var server = new Hapi.Server(); + server.route({ method: 'GET', path: '/', handler: function () { this.reply('authenticated!') } }); + + server.plugin.require('./pack/--auth', function (err) { + + expect(err).to.not.exist; + + server.inject({ method: 'GET', url: '/' }, function (res) { + + expect(res.statusCode).to.equal(401); + server.inject({ method: 'GET', url: '/', headers: { authorization: 'Basic ' + (new Buffer('john:12345', 'utf8')).toString('base64') } }, function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('authenticated!'); + done(); + }); + }); + }); + }); }); diff --git a/test/integration/pack/--auth/index.js b/test/integration/pack/--auth/index.js new file mode 100755 index 000000000..9d7ef1c50 --- /dev/null +++ b/test/integration/pack/--auth/index.js @@ -0,0 +1,27 @@ +// Declare internals + +var internals = {}; + + +// Plugin registration + +exports.register = function (pack, options, next) { + + var loadUser = function (id, callback) { + + if (id === 'john') { + return callback(null, { id: 'john', password: '12345' }); + } + + return callback(null, null); + }; + + pack.auth('basic', { + scheme: 'basic', + loadUserFunc: loadUser, + requiredByDefault: true + }); + + return next(); +}; + diff --git a/test/integration/pack/--auth/package.json b/test/integration/pack/--auth/package.json new file mode 100755 index 000000000..2c2d94bdc --- /dev/null +++ b/test/integration/pack/--auth/package.json @@ -0,0 +1,7 @@ +{ + "name": "--auth", + "description": "Test plugin module", + "version": "0.0.1", + "private": true, + "main": "./" +} diff --git a/test/unit/auth/index.js b/test/unit/auth/index.js index 9ea8a853b..82e7255ab 100755 --- a/test/unit/auth/index.js +++ b/test/unit/auth/index.js @@ -31,7 +31,7 @@ describe('Auth', function () { var auth = Auth(); }; - expect(fn).to.throw(Error); + expect(fn).to.throw('Auth must be instantiated using new'); done(); }); @@ -39,10 +39,11 @@ describe('Auth', function () { var fn = function () { - var auth = new Auth(null); + var auth = new Auth(); + auth.addBatch(null); }; - expect(fn).to.throw(Error, 'Invalid options'); + expect(fn).to.throw(Error, 'Invalid auth options'); done(); }); @@ -50,10 +51,11 @@ describe('Auth', function () { var fn = function () { - var auth = new Auth(null, { scheme: null }); + var auth = new Auth(); + auth.addBatch({ scheme: null }); }; - expect(fn).to.throw(Error); + expect(fn).to.throw('Cannot read property \'scheme\' of null'); done(); }); @@ -66,7 +68,8 @@ describe('Auth', function () { route: function () { } }; - var auth = new Auth(server, { + var auth = new Auth(server); + auth.addBatch({ scheme: 'oz', encryptionPassword: 'test', loadAppFunc: function () { }, @@ -78,25 +81,15 @@ describe('Auth', function () { done(); }); - it('throws an error if no strategies are defined', function (done) { - - var request = { - _timestamp: Date.now(), - route: { auth: {} }, - log: function () { } - }; - - var server = { - settings: {}, - route: function () { } - }; + it('doesn\'t throws an error if no strategies are defined', function (done) { var a = function () { - var auth = new Auth(server, {}); + var auth = new Auth(); + auth.addBatch({}); }; - expect(a).to.throw(Error); + expect(a).to.not.throw; done(); }); @@ -116,7 +109,8 @@ describe('Auth', function () { var a = function () { - var auth = new Auth(server, scheme); + var auth = new Auth(server); + auth.addBatch(scheme); }; expect(a).to.not.throw(Error); @@ -166,7 +160,8 @@ describe('Auth', function () { var a = function () { - var auth = new Auth(server, scheme); + var auth = new Auth(server); + auth.addBatch(scheme); auth.authenticate(request, function (err) { expect(err).to.not.exist; @@ -236,7 +231,7 @@ describe('Auth', function () { server: server }; - server.auth.authenticate(request, function (err) { + server._auth.authenticate(request, function (err) { expect(err).to.not.exist; done(); @@ -264,7 +259,7 @@ describe('Auth', function () { server: server }; - server.auth.authenticate(request, function (err) { + server._auth.authenticate(request, function (err) { expect(err).to.not.exist; done(); @@ -294,7 +289,7 @@ describe('Auth', function () { server: server }; - server.auth.authenticate(request, function (err) { + server._auth.authenticate(request, function (err) { expect(err).to.not.exist; done(); @@ -322,7 +317,7 @@ describe('Auth', function () { server: server }; - server.auth.authenticate(request, function (err) { + server._auth.authenticate(request, function (err) { expect(err).to.exist; expect(err).to.be.instanceOf(Error); @@ -353,7 +348,7 @@ describe('Auth', function () { server: server }; - server.auth.authenticate(request, function (err) { + server._auth.authenticate(request, function (err) { expect(err).to.exist; expect(err).to.be.instanceOf(Error); @@ -384,7 +379,7 @@ describe('Auth', function () { server: server }; - server.auth.authenticate(request, function (err) { + server._auth.authenticate(request, function (err) { expect(err).to.not.exist; done(); @@ -415,7 +410,7 @@ describe('Auth', function () { host: 'localhost' }; - server.auth.authenticate(request, function (err) { + server._auth.authenticate(request, function (err) { expect(err).to.exist; expect(err).to.be.instanceOf(Error); @@ -454,7 +449,8 @@ describe('Auth', function () { server: server }; - var auth = new Auth(server, scheme); + var auth = new Auth(server); + auth.addBatch(scheme); auth.authenticate(request, function (err) { @@ -490,49 +486,26 @@ describe('Auth', function () { it('returns error on bad ext scheme callback', function (done) { - var server = { - settings: {} - }; + var server = new Hapi.Server({ + auth: { + implementation: { + authenticate: function (request, callback) { - var request = { - _timestamp: Date.now(), - route: { - auth: { - strategies: ['default'] - } - }, - log: function () { }, - raw: { - res: { - setHeader: function () { } - }, - req: { - headers: { - host: 'localhost' - }, - url: 'http://localhost/test' + return callback(null, null, false); + } } - }, - server: server - }; + } + }); - var scheme = { - scheme: 'ext:test', - implementation: { - authenticate: function (request, callback) { + var handler = function () { - return callback(null, null, false); - } - } + this.reply('ok'); }; - var auth = new Auth(server, scheme); - - auth.authenticate(request, function (err) { + server.route({ method: 'GET', path: '/', handler: handler, config: { auth: 'default' } }); + server.inject({ url: '/', method: 'GET' }, function (res) { - expect(err).to.exist; - expect(err).to.be.instanceOf(Error); - expect(err.message).to.equal('Authentication response missing both error and session'); + expect(res.statusCode).to.equal(500); done(); }); }); diff --git a/test/unit/route.js b/test/unit/route.js index 7f86eab26..f457e8ee1 100755 --- a/test/unit/route.js +++ b/test/unit/route.js @@ -22,7 +22,7 @@ var it = Lab.test; describe('Route', function () { - var server = { settings: Defaults.server }; + var server = new Hapi.Server(Defaults.server); var _handler = function (request) { @@ -161,7 +161,7 @@ describe('Route', function () { it('process the path \'' + path + '\' as ' + fingerprint, function (done) { - var route = new Route({ path: path, method: 'get', handler: function () { } }, { settings: { router: { isCaseSensitive: true } } }); + var route = new Route({ path: path, method: 'get', handler: function () { } }, new Hapi.Server({ router: { isCaseSensitive: true } })); expect(route.fingerprint).to.equal(fingerprint); done(); }); @@ -249,7 +249,8 @@ describe('Route', function () { function test(path, matches, isCaseSensitive) { - var route = new Route({ path: path, method: 'get', handler: function () { } }, { settings: { router: { isCaseSensitive: isCaseSensitive } } }); + var server = new Hapi.Server({ router: { isCaseSensitive: isCaseSensitive } }); + var route = new Route({ path: path, method: 'get', handler: function () { } }, server); var mkeys = Object.keys(matches); for (var m = 0, ml = mkeys.length; m < ml; ++m) { function match(route, match, result) { diff --git a/test/unit/server.js b/test/unit/server.js index 03cffb752..1e719e360 100755 --- a/test/unit/server.js +++ b/test/unit/server.js @@ -123,7 +123,7 @@ describe('Server', function () { it('assigns this.views when config enables views', function (done) { var server = new Hapi.Server('0.0.0.0', 0, { views: { partials: { path: __dirname + '/templates' } } }); - expect(server.views).to.exist; + expect(server._views).to.exist; done(); }); @@ -146,16 +146,6 @@ describe('Server', function () { done(); }); - it('throws an error when router.routeDefaults.handler is provided', function (done) { - - var fn = function () { - - var server = new Hapi.Server({ router: { routeDefaults: { handler: function () { } } } }); - }; - expect(fn).to.throw(Error, 'Route defaults cannot include a handler'); - done(); - }); - describe('#start', function () { it('doesn\'t throw an error', function (done) {