hapi
Version:
HTTP Server framework
360 lines (248 loc) • 11.8 kB
JavaScript
// Load modules
var Boom = require('boom');
var Semver = require('semver');
var Hawk = require('./hawk');
var Bewit = require('./bewit');
var Basic = require('./basic');
var Cookie = require('./cookie');
var Utils = require('../utils');
// Declare internals
var internals = {};
exports = module.exports = internals.Auth = function (server) {
Utils.assert(this.constructor === internals.Auth, 'Auth must be instantiated using new');
this.server = server;
// Load strategies
this._strategies = {};
this._extensions = [];
this._defaultStrategy = { // Strategy used as default if route has no auth settings
name: null,
mode: 'required'
};
return this;
};
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 || ['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.defaultMode || !this._defaultStrategy.name, 'Cannot set default required strategy more than once:', name, '- already set to:', this._defaultStrategy);
options.scheme = options.scheme || 'ext';
switch (options.scheme) {
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;
}
if (this._strategies[name].extend &&
typeof this._strategies[name].extend === 'function') {
this._extensions.push(this._strategies[name]);
}
if (options.defaultMode) {
this._defaultStrategy.name = name;
this._defaultStrategy.mode = (typeof options.defaultMode === 'string' ? options.defaultMode : 'required');
}
};
internals.Auth.prototype.addBatch = function (options) {
var self = this;
Utils.assert(options && typeof options === 'object', 'Invalid auth options');
if (!Object.keys(options).length) {
return;
}
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) {
return options; // Preseve the difference between undefined and false
}
if (typeof options === 'string') {
options = { strategies: [options] };
}
else if (typeof options === 'boolean') {
options = { strategies: ['default'] };
}
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) {
if (request.route.auth) {
return request.route.auth;
}
if (request.route.auth === false ||
request.route.auth === null) {
return false;
}
if (this._defaultStrategy.name) {
return {
mode: this._defaultStrategy.mode,
strategies: [this._defaultStrategy.name]
};
}
return false;
};
internals.Auth.authenticate = function (request, next) {
var auth = request.server._auth;
var config = auth.routeConfig(request);
if (!config) {
return 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);
};
internals.Auth.prototype.authenticate = function (request, next) {
var self = this;
var config = this.routeConfig(request);
var authErrors = [];
var strategyPos = 0;
var authenticate = function () {
// Injection
if (request.auth.credentials) {
return validate(null, request.auth.credentials);
}
// Authenticate
if (strategyPos >= config.strategies.length) {
if (config.mode === 'optional' ||
config.mode === 'try') {
request.auth.isAuthenticated = false;
request.auth.credentials = null;
request._log(['auth', 'unauthenticated']);
return next();
}
return next(Boom.unauthorized('Missing authentication', authErrors));
}
var strategy = self._strategies[config.strategies[strategyPos++]]; // Increments counter after fetching current strategy
return strategy.authenticate(request, validate);
};
var validate = function (err, credentials, options) {
options = options || {};
// Unauthenticated
if (!err && !credentials) {
return next(Boom.internal('Authentication response missing both error and credentials'));
}
if (err) {
if (options.log) {
request._log(['auth', 'error', config.strategies[strategyPos]].concat(options.log.tags), options.log.data);
}
else {
request._log(['auth', 'unauthenticated'], err);
}
if (err instanceof Error === false || // Not an actual error (e.g. redirect, custom response)
!err.isMissing || // Missing authentication (did not fail)
err.response.code !== 401) { // An actual error (not just missing authentication)
if (config.mode === 'try') {
request.auth.isAuthenticated = false;
request.auth.credentials = credentials;
request.auth.artifacts = options.artifacts;
request._log(['auth', 'unauthenticated', 'try'], err);
return next();
}
return next(err);
}
// Try next strategy
if (err.response.headers['WWW-Authenticate']) {
authErrors.push(err.response.headers['WWW-Authenticate']);
}
return authenticate();
}
// Authenticated
request.auth.credentials = credentials;
request.auth.artifacts = options.artifacts;
request.auth._strategy = self._strategies[config.strategies[strategyPos - 1]];
// Check scope
if (config.scope &&
(!credentials || !credentials.scope || credentials.scope.indexOf(config.scope) === -1)) {
request._log(['auth', 'scope', 'error'], { got: credentials && credentials.scope, need: config.scope });
return next(Boom.forbidden('Insufficient scope (\'' + config.scope + '\' expected)'));
}
// Check TOS
var tos = (config.hasOwnProperty('tos') ? config.tos : false);
if (tos &&
(!credentials || !credentials.tos || !Semver.satisfies(credentials.tos, tos))) {
request._log(['auth', 'tos', 'error'], { min: tos, received: credentials && credentials.tos });
return next(Boom.forbidden('Insufficient TOS accepted'));
}
// Check entity
var entity = config.entity || 'any';
// Entity: 'any'
if (entity === 'any') {
request._log(['auth']);
request.auth.isAuthenticated = true;
return next();
}
// Entity: 'user'
if (entity === 'user') {
if (!credentials || !credentials.user) {
request._log(['auth', 'error'], 'User credentials required');
return next(Boom.forbidden('Application credentials cannot be used on a user endpoint'));
}
request._log(['auth']);
request.auth.isAuthenticated = true;
return next();
}
// Entity: 'app'
if (credentials && credentials.user) {
request._log(['auth', 'error'], 'App credentials required');
return next(Boom.forbidden('User credentials cannot be used on an application endpoint'));
}
request._log(['auth']);
request.auth.isAuthenticated = true;
return next();
};
authenticate();
};
internals.Auth.authenticatePayload = function (request, next) {
var auth = request.server._auth;
var config = auth.routeConfig(request);
if (!config ||
!config.payload ||
!request.auth.isAuthenticated) {
return next();
}
if (config.payload === 'optional' &&
(!request.auth.artifacts.hash ||
typeof request.auth._strategy.authenticatePayload !== 'function')) {
return next();
}
request.auth._strategy.authenticatePayload(request, function (err) {
return next(err);
});
};
internals.Auth.responseHeader = function (request, response, next) {
var auth = request.server._auth;
var config = auth.routeConfig(request);
if (!config ||
!request.auth.isAuthenticated) {
return next();
}
if (!request.auth.credentials ||
!request.auth._strategy ||
typeof request.auth._strategy.responseHeader !== 'function') {
return next();
}
request.auth._strategy.responseHeader(request, response, next);
};