Skip to content

Commit ae0634d

Browse files
author
Eran Hammer
committed
Merge pull request hapijs#2532 from hapijs/dynamic_scope
Dynamic authentication scopes
2 parents db4bcd0 + 9531443 commit ae0634d

File tree

3 files changed

+103
-3
lines changed

3 files changed

+103
-3
lines changed

API.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,6 +2016,8 @@ following options:
20162016
- `scope` - the application scope required to access the route. Value can be a scope
20172017
string or an array of scope strings. The authenticated credentials object `scope`
20182018
property must contain at least one of the scopes defined to access the route.
2019+
You may also access properties on the request object to populate a dynamic scope
2020+
by using `{}` characters around the property name, such as `'user-{params.id}'`.
20192021
Set to `false` to remove scope requirements. Defaults to no scope required.
20202022
- `entity` - the required authenticated entity type. If set, must match the `entity`
20212023
value of the authentication credentials. Available values:

lib/auth.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ internals.Auth.prototype._setupRoute = function (options, path) {
125125
options = Hoek.applyToDefaults(this.settings.default, options);
126126
}
127127

128+
if (options.scope) {
129+
if (typeof options.scope === 'string') {
130+
options.scope = [options.scope];
131+
}
132+
133+
for (var i = 0, il = options.scope.length; i < il; ++i) {
134+
if (/{([^}]+)}/g.test(options.scope[i])) {
135+
options.hasScopeParameters = true;
136+
break;
137+
}
138+
}
139+
}
140+
128141
Hoek.assert(options.strategies.length, 'Route missing authentication strategy:', path);
129142

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

272285
if (config.scope) {
286+
if (config.hasScopeParameters) {
287+
var expandScope = function ($0, context) {
288+
289+
return Hoek.reach({
290+
params: request.params,
291+
query: request.query,
292+
payload: request.payload
293+
}, context);
294+
};
295+
296+
for (var i = 0, il = config.scope.length; i < il; ++i) {
297+
config.scope[i] = config.scope[i].replace(/{([^}]+)}/g, expandScope);
298+
}
299+
}
300+
273301
if (!credentials.scope ||
274-
(typeof config.scope === 'string' ?
275-
(typeof credentials.scope === 'string' ? config.scope !== credentials.scope : credentials.scope.indexOf(config.scope) === -1) :
276-
(typeof credentials.scope === 'string' ? config.scope.indexOf(credentials.scope) === -1 : !Hoek.intersect(config.scope, credentials.scope).length))) {
302+
(typeof credentials.scope === 'string' ? config.scope.indexOf(credentials.scope) === -1 : !Hoek.intersect(config.scope, credentials.scope).length)) {
277303

278304
request._log(['auth', 'scope', 'error', name], { got: credentials.scope, need: config.scope });
279305
return next(Boom.forbidden('Insufficient scope, expected any of: ' + config.scope));

test/auth.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,78 @@ describe('authentication', function () {
730730
});
731731
});
732732

733+
it('matches dynamic scope (single to single)', function (done) {
734+
735+
var server = new Hapi.Server();
736+
server.connection();
737+
server.auth.scheme('custom', internals.implementation);
738+
server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test' } } });
739+
server.route({
740+
method: 'GET',
741+
path: '/{id}',
742+
config: {
743+
handler: function (request, reply) { return reply(request.auth.credentials.user); },
744+
auth: {
745+
scope: 'one-{params.id}'
746+
}
747+
}
748+
});
749+
750+
server.inject({ url: '/test', headers: { authorization: 'Custom steve' } }, function (res) {
751+
752+
expect(res.statusCode).to.equal(200);
753+
done();
754+
});
755+
});
756+
757+
it('matches dynamic scope with multiple parts (single to single)', function (done) {
758+
759+
var server = new Hapi.Server();
760+
server.connection();
761+
server.auth.scheme('custom', internals.implementation);
762+
server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test-admin' } } });
763+
server.route({
764+
method: 'GET',
765+
path: '/{id}/{role}',
766+
config: {
767+
handler: function (request, reply) { return reply(request.auth.credentials.user); },
768+
auth: {
769+
scope: 'one-{params.id}-{params.role}'
770+
}
771+
}
772+
});
773+
774+
server.inject({ url: '/test/admin', headers: { authorization: 'Custom steve' } }, function (res) {
775+
776+
expect(res.statusCode).to.equal(200);
777+
done();
778+
});
779+
});
780+
781+
it('does not match broken dynamic scope (single to single)', function (done) {
782+
783+
var server = new Hapi.Server();
784+
server.connection();
785+
server.auth.scheme('custom', internals.implementation);
786+
server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test' } } });
787+
server.route({
788+
method: 'GET',
789+
path: '/{id}',
790+
config: {
791+
handler: function (request, reply) { return reply(request.auth.credentials.user); },
792+
auth: {
793+
scope: 'one-params.id}'
794+
}
795+
}
796+
});
797+
798+
server.inject({ url: '/test', headers: { authorization: 'Custom steve' } }, function (res) {
799+
800+
expect(res.statusCode).to.equal(403);
801+
done();
802+
});
803+
});
804+
733805
it('does not match scope (single to single)', function (done) {
734806

735807
var handler = function (request, reply) {

0 commit comments

Comments
 (0)