Skip to content

Commit

Permalink
Rework server.auth(). Closes hapijs#1287
Browse files Browse the repository at this point in the history
  • Loading branch information
Eran Hammer committed Jan 3, 2014
1 parent 259db70 commit 8f0d49c
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 738 deletions.
56 changes: 1 addition & 55 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
- [`server.views(options)`](#serverviewsoptions)
- [`server.cache(name, options)`](#servercachename-options)
- [`server.auth(name, options)`](#serverauthname-options)
- [Basic authentication](#basic-authentication)
- [Cookie authentication](#cookie-authentication)
- [Hawk authentication](#hawk-authentication)
- [Bewit authentication](#bewit-authentication)
Expand Down Expand Up @@ -145,10 +144,6 @@ When creating a server instance, the following options configure the server's be
- `app` - application-specific configuration which can later be accessed via `server.settings.app`. Provides a safe place to store application configuration without potential conflicts with **hapi**. Should not be used by plugins which should use `plugins[name]`. Note the difference between
`server.settings.app` which is used to store configuration value and `server.app` which is meant for storing run-time state.

- `auth` - configures one or more authentication strategies. The `auth` key can be set to a single strategy object (the name will default to `'default'`),
or to an object with multiple strategies where the strategy name is the object key. The authentication strategies and their options are described in
[`server.auth()`](#serverauthname-options).

- <a name="server.config.cache"></a>`cache` - determines the type of server-side cache used. Every server includes a cache for storing and reusing request
responses and helper results. By default a simple memory-based cache is used which has limited capacity and limited production environment suitability.
In addition to the memory cache, a Redis, MongoDB, or Memcache cache can be configured. Actual caching is only utilized if helpers and plugins
Expand Down Expand Up @@ -950,7 +945,7 @@ var cache = server.cache('countries', { expiresIn: 60 * 60 * 1000 });

Registers an authentication strategy where:

- `name` - is the strategy name (`'default'` is automatically assigned if a single strategy is registered via the server `auth` config).
- `name` - is the strategy name.
- `options` - required strategy options. Each scheme comes with its own set of required options, in addition to the options shared by all schemes:
- `scheme` - (required, except when `implementation` is used) the built-in scheme name. Available values:
- `'basic'` - [HTTP Basic authentication](#basic-authentication) ([RFC 2617](http://tools.ietf.org/html/rfc2617))
Expand All @@ -961,55 +956,6 @@ Registers an authentication strategy where:
- `defaultMode` - if `true`, the scheme is automatically assigned as a required strategy to any route without an `auth` config. Can only be assigned to a single
server strategy. Value must be `true` (which is the same as `'required'`) or a valid authentication mode (`'required'`, `'optional'`, `'try'`). Defaults to `false`.

##### Basic authentication

Basic authentication requires validating a username and password combination. The `'basic'` scheme takes the following options:

- `scheme` - (required) set to `'basic'`.
- `validateFunc` - (required) a user lookup and password validation function with the signature `function(username, password, callback)` where:
- `username` - the username received from the client.
- `password` - the password received from the client.
- `callback` - a callback function with the signature `function(err, isValid, credentials)` where:
- `err` - an internal error.
- `isValid` - `true` if both the username was found and the password matched, otherwise `false`.
- `credentials` - a credentials object passed back to the application in `request.auth.credentials`. Typically, `credentials` are only
included when `isValid` is `true`, but there are cases when the application needs to know who tried to authenticate even when it fails
(e.g. with authentication mode `'try'`).
- `allowEmptyUsername` - (optional) if `true`, allows making requests with an empty username. Defaults to `false`.

```javascript
var Bcrypt = require('bcrypt');

var users = {
john: {
username: 'john',
password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm', // 'secret'
name: 'John Doe',
id: '2133d32a'
}
};

var validate = function (username, password, callback) {

var user = users[username];
if (!user) {
return callback(null, false);
}

Bcrypt.compare(password, user.password, function (err, isValid) {

callback(err, isValid, { id: user.id, name: user.name });
});
};

server.auth('simple', {
scheme: 'basic',
validateFunc: validate
});

server.route({ method: 'GET', path: '/', config: { auth: 'simple' } });
```

##### Cookie authentication

Cookie authentication provides a simple cookie-based session management. The user has to be authenticated via other means, typically a web
Expand Down
1 change: 0 additions & 1 deletion lib/auth/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ var internals = {};
exports = module.exports = internals.Scheme = function (server, options) {

Utils.assert(options, 'Invalid options');
Utils.assert(options.scheme === 'basic', 'Wrong scheme');
Utils.assert(options.validateFunc, 'Missing required validateFunc method in configuration');
Utils.assert(server, 'Server is required');

Expand Down
1 change: 0 additions & 1 deletion lib/auth/bewit.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ var internals = {};
exports = module.exports = internals.Scheme = function (server, options) {

Utils.assert(options, 'Invalid options');
Utils.assert(options.scheme === 'bewit', 'Wrong scheme');
Utils.assert(options.getCredentialsFunc, 'Missing required getCredentialsFunc method in configuration');
Utils.assert(server, 'Server is required');

Expand Down
1 change: 0 additions & 1 deletion lib/auth/cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ exports = module.exports = internals.Scheme = function (server, options) {

Utils.assert(server, 'Server is required');
Utils.assert(options, 'Invalid options');
Utils.assert(options.scheme === 'cookie', 'Wrong scheme');
Utils.assert(!options.validateFunc || typeof options.validateFunc === 'function', 'Invalid validateFunc method in configuration');
Utils.assert(options.password, 'Missing required password in configuration');
Utils.assert(!options.appendNext || options.redirectTo, 'Cannot set \'appendNext\' without \'redirectTo\'');
Expand Down
1 change: 0 additions & 1 deletion lib/auth/hawk.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ var internals = {};
exports = module.exports = internals.Scheme = function (server, options) {

Utils.assert(options, 'Invalid options');
Utils.assert(options.scheme === 'hawk', 'Wrong scheme');
Utils.assert(options.getCredentialsFunc, 'Missing required getCredentialsFunc method in configuration');
Utils.assert(server, 'Server is required');

Expand Down
87 changes: 37 additions & 50 deletions lib/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,71 +18,58 @@ var internals = {};
exports = module.exports = internals.Auth = function (server) {

this.server = server;

// Load strategies

this._schemes = {};
this._strategies = {};
this._extensions = [];
this._defaultStrategy = { // Strategy used as default if route has no auth settings
name: null,
mode: 'required'
};

this.scheme('hawk', function (server, options) { return new Hawk(server, options); });
this.scheme('basic', function (server, options) { return new Basic(server, options); });
this.scheme('cookie', function (server, options) { return new Cookie(server, options); });
this.scheme('bewit', function (server, options) { return new Bewit(server, options); });
};


internals.Auth.prototype.scheme = function (name, scheme) {

Utils.assert(name, 'Authentication scheme must have a name');
Utils.assert(!this._schemes[name], 'Authentication scheme name already exists:', name);
Utils.assert(typeof scheme === 'function', 'scheme must be a function:', name);

this._schemes[name] = scheme;
};


internals.Auth.prototype.add = function (name, options) {
internals.Auth.prototype.strategy = function (name, scheme /*, mode, options */) {

var mode = (arguments.length === 4 ? arguments[2] : null);
var options = (arguments.length === 4 ? arguments[3] : arguments[2]);

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);

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;
}
Utils.assert(scheme && this._schemes[scheme], name, 'has an unknown scheme:', scheme);
Utils.assert(!mode || !this._defaultStrategy.name, 'Cannot set default required strategy more than once:', name, '- already set to:', this._defaultStrategy);

this._strategies[name] = this._schemes[scheme](this.server, options);

if (this._strategies[name].extend &&
typeof this._strategies[name].extend === 'function') {

this._extensions.push(this._strategies[name]);
}

if (options.defaultMode) {
if (mode) {
this._defaultStrategy.name = name;
this._defaultStrategy.mode = (typeof options.defaultMode === 'string' ? options.defaultMode : 'required');
this._defaultStrategy.mode = (typeof mode === 'string' ? mode : 'required');
Utils.assert(['required', 'optional', 'try'].indexOf(this._defaultStrategy.mode) !== -1, 'Unknown default authentication mode:', this._defaultStrategy.mode);
}
};


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) {
internals.Auth.prototype._setupRoute = function (options) {

var self = this;

Expand Down Expand Up @@ -127,7 +114,7 @@ internals.Auth.prototype.setupRoute = function (options) {
};


internals.Auth.prototype.routeConfig = function (request) {
internals.Auth.prototype._routeConfig = function (request) {

if (request.route.auth) {
return request.route.auth;
Expand All @@ -152,8 +139,8 @@ internals.Auth.prototype.routeConfig = function (request) {

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

var auth = request.server._auth;
var config = auth.routeConfig(request);
var auth = request.server.auth;
var config = auth._routeConfig(request);
if (!config) {
return next();
}
Expand All @@ -164,15 +151,15 @@ internals.Auth.authenticate = function (request, next) {
auth._extensions[i].extend(request);
}

return auth.authenticate(request, next);
return auth._authenticate(request, next);
};


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

var self = this;

var config = this.routeConfig(request);
var config = this._routeConfig(request);

var authErrors = [];
var strategyPos = 0;
Expand Down Expand Up @@ -336,8 +323,8 @@ internals.Auth.prototype.authenticate = function (request, next) {

internals.Auth.authenticatePayload = function (request, next) {

var auth = request.server._auth;
var config = auth.routeConfig(request);
var auth = request.server.auth;
var config = auth._routeConfig(request);

if (!config ||
!config.payload ||
Expand All @@ -362,8 +349,8 @@ internals.Auth.authenticatePayload = function (request, next) {

internals.Auth.responseHeader = function (request, response, next) {

var auth = request.server._auth;
var config = auth.routeConfig(request);
var auth = request.server.auth;
var config = auth._routeConfig(request);

if (!config ||
!request.auth.isAuthenticated) {
Expand Down
3 changes: 1 addition & 2 deletions lib/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,7 @@ exports.server = {
// Optional components

cors: false, // CORS headers on responses and OPTIONS requests (defaults: exports.cors): false -> null, true -> defaults, {} -> override defaults
views: null, // Views engine
auth: {} // Authentication
views: null // Views engine
};


Expand Down
19 changes: 17 additions & 2 deletions lib/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,15 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen

self._applySync(selection.servers, Server.prototype.state, arguments);
},
auth: function () {
auth: {
scheme: function () {

self._applySync(selection.servers, Server.prototype.auth, arguments);
self._applyChildSync(selection.servers, 'auth', 'scheme', arguments);
},
strategy: function () {

self._applyChildSync(selection.servers, 'auth', 'strategy', arguments);
}
},
ext: function () {

Expand Down Expand Up @@ -589,6 +595,15 @@ internals.Pack.prototype._applySync = function (servers, func, args) {
};


internals.Pack.prototype._applyChildSync = function (servers, child, func, args) {

for (var i = 0, il = servers.length; i < il; ++i) {
var obj = servers[i][child];
obj[func].apply(obj, args);
}
};


internals.Pack.prototype._provisionCache = function (options, type, name, segment) {

Utils.assert(options && typeof options === 'object', 'Invalid cache policy options');
Expand Down
2 changes: 1 addition & 1 deletion lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ exports = module.exports = internals.Route = function (options, server, env) {

// Authentication configuration

this.settings.auth = this.server._auth.setupRoute(this.settings.auth);
this.settings.auth = this.server.auth._setupRoute(this.settings.auth);

// Cache

Expand Down
1 change: 0 additions & 1 deletion lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ internals.cache = Joi.object({

internals.serverSchema = {
app: Joi.object().allow(null),
auth: Joi.object().allow(false, true),
cache: [
Joi.string().allow(null),
internals.cache,
Expand Down
14 changes: 1 addition & 13 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ module.exports = internals.Server = function (/* host, port, options */) {
// Server facilities

this._started = false;
this._auth = new Auth(this); // Required before _router
this.auth = new Auth(this); // Required before _router
this._router = new Router(this);

// Server load
Expand Down Expand Up @@ -192,12 +192,6 @@ module.exports = internals.Server = function (/* host, port, options */) {
Http.globalAgent.maxSockets = this.settings.maxSockets;
}

// Authentication

if (this.settings.auth) {
this._auth.addBatch(this.settings.auth);
}

// Server information

this.info = {
Expand Down Expand Up @@ -448,12 +442,6 @@ internals.Server.prototype.state = function (name, options) {
};


internals.Server.prototype.auth = function (name, options) {

this._auth.add(name, options);
};


internals.Server.prototype.views = function (options) {

Utils.assert(!this._views, 'Cannot set server views manager more than once');
Expand Down
Loading

0 comments on commit 8f0d49c

Please sign in to comment.