Skip to content

Commit

Permalink
Auth cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
Eran Hammer committed Jan 25, 2013
1 parent 0c4e0e5 commit 9408922
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 246 deletions.
183 changes: 69 additions & 114 deletions lib/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,77 +18,84 @@ exports = module.exports = internals.Auth = function (server, options) {
Utils.assert(this.constructor === internals.Auth, 'Auth must be instantiated using new');
Utils.assert(options, 'Invalid options');
Utils.assert(!!options.scheme ^ !!options.strategies, 'Auth options must include one of scheme or strategies but not both');
Utils.assert(options.scheme || Object.keys(options.strategies).length, 'Number of authentication strategies must be greater than zero');

this.server = server;
this.options = Utils.clone(options);
this.strategies = {};

if (options.scheme) {
this.options = {
'strategies': {
// Move single strategy into default

var settings = options;
if (!settings.strategies) {
settings = {
strategies: {
'default': options
}
};
}

this.setStrategies(this.options.strategies);
// Load strategies

this.strategies = {};
for (var name in settings.strategies) {
if (settings.strategies.hasOwnProperty(name)) {
var strategy = settings.strategies[name];

Utils.assert(strategy.scheme, name + ' is missing a scheme');
Utils.assert(['oz', 'basic', 'hawk'].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;
default: this.strategies[name] = strategy.implementation; break;
}
}
}

Log.event(['info', 'config', 'auth'], server.settings.nickname + ': Authentication enabled');

return this;
};


internals.Auth.prototype.setStrategies = function (strategies) {

Utils.assert(Object.keys(strategies).length > 0, 'Number of Authentication Strategies must be greater than zero');
internals.Auth.prototype.authenticate = function (request, next) {

for (var strategy in strategies) {
if (strategies.hasOwnProperty(strategy)) {
var strat = strategies[strategy];
Utils.assert(strat.scheme, strategy + ' is missing a scheme');
Utils.assert(['oz', 'basic', 'hawk'].indexOf(strat.scheme) !== -1 || strat.scheme.indexOf('ext:') === 0, strategy + ' has an unknown scheme: ' + strat.scheme);
Utils.assert(strat.scheme.indexOf('ext:') !== 0 || strat.implementation, strategy + ' has extension scheme missing implementation');
Utils.assert(!strat.implementation || (typeof strat.implementation === 'object' && typeof strat.implementation.authenticate === 'function'), strategy + ' has invalid extension scheme implementation');
var self = this;

this.strategies[strategy] = this.loadScheme(strat);
}
}
var config = request._route.config.auth;

return this;
};
var authErrors = [];
var strategyPos = 0;

var authenticate = function () {

internals.Auth.prototype.loadScheme = function (options) {
// Injection

if (options.scheme === 'oz') {
return new Oz(this.server, options);
}
else if (options.scheme === 'hawk') {
return new Hawk(this.server, options);
}
else if (options.scheme === 'basic') {
return new Basic(this.server, options);
}
else {
return options.implementation;
}
};
if (request.session) {
return validate(null, request.session);
}

// Authenticate

internals.Auth.prototype.authenticate = function (request, next) {

var self = this;
if (strategyPos >= config.strategies.length) {
return next(Err.unauthorized('Missing authentication', authErrors));
}

var config = request._route.config.auth;
var authResults = [];
var strategyName = config.strategies[0];
var strategy = this.strategies[strategyName];
var strategy = self.strategies[config.strategies[strategyPos++]]; // Increments counter after fetching current strategy
return strategy.authenticate(request, validate);
};

var validate = function (err, session, wasLogged) {

// Unauthenticated

if (err || !session) {
if (!err && !session) {
return next(Err.internal('Authentication response missing both error and session'));
}

if (err) {
if (config.mode === 'optional' &&
!request.raw.req.headers.authorization) {

Expand All @@ -101,7 +108,20 @@ internals.Auth.prototype.authenticate = function (request, next) {
request.log(['auth', 'unauthenticated'], err);
}

return nextStrategy(err);
if (!err.isMissing ||
err.code !== 401) { // An actual error (not just missing authentication)

return next(err);
}

// Try next strategy

var response = err.toResponse();
if (response.headers['WWW-Authenticate']) {
authErrors.push(response.headers['WWW-Authenticate']);
}

return authenticate();
}

// Authenticated
Expand All @@ -114,7 +134,7 @@ internals.Auth.prototype.authenticate = function (request, next) {
(!session.scope || session.scope.indexOf(config.scope) === -1)) {

request.log(['auth', 'error', 'scope'], { got: session.scope, need: config.scope });
return nextStrategy(Err.forbidden('Insufficient scope (\'' + config.scope + '\' expected)'));
return next(Err.forbidden('Insufficient scope (\'' + config.scope + '\' expected)'));
}

// Check TOS
Expand All @@ -124,7 +144,7 @@ internals.Auth.prototype.authenticate = function (request, next) {
(!session.ext || !session.ext.tos || session.ext.tos < tos)) {

request.log(['auth', 'error', 'tos'], { min: tos, received: session.ext && session.ext.tos });
return nextStrategy(Err.forbidden('Insufficient TOS accepted'));
return next(Err.forbidden('Insufficient TOS accepted'));
}

// Check entity
Expand Down Expand Up @@ -154,77 +174,12 @@ internals.Auth.prototype.authenticate = function (request, next) {

if (session.user) {
request.log(['auth', 'error'], 'App session required');
return nextStrategy(Err.forbidden('User session cannot be used on an application endpoint'));
return next(Err.forbidden('User session cannot be used on an application endpoint'));
}

request.log(['auth']);
return next();
};

var nextStrategy = function (err) {

if (err) {
authResults.push(err);
}

if (config.strategies.length === authResults.length) {
return next(combineUnauthorizedErrors());
}

strategyName = config.strategies[authResults.length];
strategy = self.strategies[strategyName];

return strategy ? strategy.authenticate(request, validate) : next(err);
};

var combineUnauthorizedErrors = function () {

var wwwAuthenticate = '';
var message = '';

while (authResults.length > 0) {

var currentError = authResults.shift();
if (currentError.code !== 401) {
return currentError;
}

if (currentError.isMissing === false) {
return currentError;
}

var response = currentError.toResponse();
if (message.length > 0 && response.payload.message) {
message += ', ';
}

if (wwwAuthenticate.length > 0 && response.headers['WWW-Authenticate']) {
wwwAuthenticate += ', ';
}

wwwAuthenticate += response.headers['WWW-Authenticate'];
message += response.payload.message ? response.payload.message : '';
}

var outError = new Err(401, message);
var errResponse = outError.toResponse();

outError.toResponse = function () {

errResponse.headers = { 'WWW-Authenticate': wwwAuthenticate };
return errResponse;
};

return outError;
};

// Injection

if (request.session) {
return validate(null, request.session);
}

// Authenticate

return strategy.authenticate(request, validate);
};
authenticate();
};
24 changes: 16 additions & 8 deletions lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,29 @@ exports = module.exports = internals.Route = function (options, server) {
// Authentication configuration

this.config.auth = this.config.auth || {};
this.config.auth.mode = this.config.auth.mode || (server.settings.auth ? 'required' : 'none');
this.config.auth.strategies = this.config.auth.strategies || [this.config.auth.strategy || 'default'];
this.config.auth.mode = this.config.auth.mode || (server.auth ? 'required' : 'none');
Utils.assert(['required', 'optional', 'none'].indexOf(this.config.auth.mode) !== -1, 'Unknown authentication mode: ' + this.config.auth.mode);
Utils.assert(this.config.auth.mode === 'none' || server.settings.auth, 'Route requires authentication but none configured');

if (server.auth) {
Utils.assert(this.config.auth.mode !== 'none' || !!server.auth.strategies.default, "Route has no default authentication strategy to fallback on");
if (this.config.auth.mode !== 'none') {

// Authentication enabled

Utils.assert(server.auth, 'Route requires authentication but none configured');
Utils.assert(!this.config.auth.entity || ['user', 'app', 'any'].indexOf(this.config.auth.entity) !== -1, 'Unknown authentication entity type: ' + this.config.auth.entity);

Utils.assert(!(this.config.auth.strategy && this.config.auth.strategies), 'Route can only have a auth.strategy or auth.strategies (or use the default) but not both')
this.config.auth.strategies = this.config.auth.strategies || [this.config.auth.strategy || 'default'];
delete this.config.auth.strategy;

this.config.auth.strategies.forEach(function (strategy) {

Utils.assert(server.auth.strategies.hasOwnProperty(strategy), 'Unknown authentication strategy: ' + strategy);
Utils.assert(server.auth.strategies[strategy], 'Unknown authentication strategy: ' + strategy);
});
}

Utils.assert(this.config.auth.mode === 'none' || !this.config.auth.entity || ['user', 'app', 'any'].indexOf(this.config.auth.entity) !== -1, 'Unknown authentication entity type: ' + this.config.auth.entity);
else {
// No authentication
Utils.assert(Utils.matchKeys(this.config.auth, ['strategy', 'strategies', 'entity', 'tos', 'scope']).length === 0, 'Route auth is off but auth is configured');
}

// Parse path

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"node": ">=0.8.0"
},
"dependencies": {
"hoek": "0.0.x",
"boom": "0.1.x",
"hoek": "0.1.x",
"boom": "0.2.x",
"joi": "0.0.x",
"lout": "0.0.x",
"hapi-helmet": "0.0.x",
Expand Down
Loading

0 comments on commit 9408922

Please sign in to comment.