Skip to content

Commit

Permalink
Merge pull request #2532 from hapijs/dynamic_scope
Browse files Browse the repository at this point in the history
Dynamic authentication scopes
  • Loading branch information
Eran Hammer committed May 21, 2015
2 parents db4bcd0 + 9531443 commit ae0634d
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 3 deletions.
2 changes: 2 additions & 0 deletions API.md
Expand Up @@ -2016,6 +2016,8 @@ following options:
- `scope` - the application scope required to access the route. Value can be a scope
string or an array of scope strings. The authenticated credentials object `scope`
property must contain at least one of the scopes defined to access the route.
You may also access properties on the request object to populate a dynamic scope
by using `{}` characters around the property name, such as `'user-{params.id}'`.
Set to `false` to remove scope requirements. Defaults to no scope required.
- `entity` - the required authenticated entity type. If set, must match the `entity`
value of the authentication credentials. Available values:
Expand Down
32 changes: 29 additions & 3 deletions lib/auth.js
Expand Up @@ -125,6 +125,19 @@ internals.Auth.prototype._setupRoute = function (options, path) {
options = Hoek.applyToDefaults(this.settings.default, options);
}

if (options.scope) {
if (typeof options.scope === 'string') {
options.scope = [options.scope];
}

for (var i = 0, il = options.scope.length; i < il; ++i) {
if (/{([^}]+)}/g.test(options.scope[i])) {
options.hasScopeParameters = true;
break;
}
}
}

Hoek.assert(options.strategies.length, 'Route missing authentication strategy:', path);

options.mode = options.mode || 'required';
Expand Down Expand Up @@ -270,10 +283,23 @@ internals.Auth.prototype._authenticate = function (request, next) {
// Check scope

if (config.scope) {
if (config.hasScopeParameters) {
var expandScope = function ($0, context) {

return Hoek.reach({
params: request.params,
query: request.query,
payload: request.payload
}, context);
};

for (var i = 0, il = config.scope.length; i < il; ++i) {
config.scope[i] = config.scope[i].replace(/{([^}]+)}/g, expandScope);
}
}

if (!credentials.scope ||
(typeof config.scope === 'string' ?
(typeof credentials.scope === 'string' ? config.scope !== credentials.scope : credentials.scope.indexOf(config.scope) === -1) :
(typeof credentials.scope === 'string' ? config.scope.indexOf(credentials.scope) === -1 : !Hoek.intersect(config.scope, credentials.scope).length))) {
(typeof credentials.scope === 'string' ? config.scope.indexOf(credentials.scope) === -1 : !Hoek.intersect(config.scope, credentials.scope).length)) {

request._log(['auth', 'scope', 'error', name], { got: credentials.scope, need: config.scope });
return next(Boom.forbidden('Insufficient scope, expected any of: ' + config.scope));
Expand Down
72 changes: 72 additions & 0 deletions test/auth.js
Expand Up @@ -730,6 +730,78 @@ describe('authentication', function () {
});
});

it('matches dynamic scope (single to single)', function (done) {

var server = new Hapi.Server();
server.connection();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test' } } });
server.route({
method: 'GET',
path: '/{id}',
config: {
handler: function (request, reply) { return reply(request.auth.credentials.user); },
auth: {
scope: 'one-{params.id}'
}
}
});

server.inject({ url: '/test', headers: { authorization: 'Custom steve' } }, function (res) {

expect(res.statusCode).to.equal(200);
done();
});
});

it('matches dynamic scope with multiple parts (single to single)', function (done) {

var server = new Hapi.Server();
server.connection();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test-admin' } } });
server.route({
method: 'GET',
path: '/{id}/{role}',
config: {
handler: function (request, reply) { return reply(request.auth.credentials.user); },
auth: {
scope: 'one-{params.id}-{params.role}'
}
}
});

server.inject({ url: '/test/admin', headers: { authorization: 'Custom steve' } }, function (res) {

expect(res.statusCode).to.equal(200);
done();
});
});

it('does not match broken dynamic scope (single to single)', function (done) {

var server = new Hapi.Server();
server.connection();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test' } } });
server.route({
method: 'GET',
path: '/{id}',
config: {
handler: function (request, reply) { return reply(request.auth.credentials.user); },
auth: {
scope: 'one-params.id}'
}
}
});

server.inject({ url: '/test', headers: { authorization: 'Custom steve' } }, function (res) {

expect(res.statusCode).to.equal(403);
done();
});
});

it('does not match scope (single to single)', function (done) {

var handler = function (request, reply) {
Expand Down

0 comments on commit ae0634d

Please sign in to comment.