When issuing token intersect with the existing user roles
Also: * Move token validation to accesscontrol.js * Use clients.addTokenByUserId everywhere
This commit is contained in:
+41
-5
@@ -19,14 +19,14 @@ exports = module.exports = {
|
||||
|
||||
ROLE_OWNER: 'owner',
|
||||
|
||||
scopesForRoles: scopesForRoles,
|
||||
|
||||
validateRoles: validateRoles,
|
||||
|
||||
validateScopeString: validateScopeString,
|
||||
hasScopes: hasScopes,
|
||||
canonicalScopeString: canonicalScopeString,
|
||||
intersectScopes: intersectScopes,
|
||||
canonicalScopeString: canonicalScopeString
|
||||
validateToken: validateToken,
|
||||
scopesForRoles: scopesForRoles
|
||||
};
|
||||
|
||||
// https://docs.microsoft.com/en-us/azure/role-based-access-control/role-definitions
|
||||
@@ -46,7 +46,11 @@ const ROLE_DEFINITIONS = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:accesscontrol'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
_ = require('underscore');
|
||||
|
||||
// returns scopes that does not have wildcards and is sorted
|
||||
@@ -60,6 +64,8 @@ function intersectScopes(allowedScopes, wantedScopes) {
|
||||
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
|
||||
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
|
||||
|
||||
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
|
||||
|
||||
let wantedScopesMap = new Map();
|
||||
let results = [];
|
||||
|
||||
@@ -134,7 +140,7 @@ function hasScopes(authorizedScopes, requiredScopes) {
|
||||
function scopesForRoles(roles) {
|
||||
assert(Array.isArray(roles), 'Expecting array');
|
||||
|
||||
var scopes = [ 'profile', 'apps:read' ];
|
||||
let scopes = [ 'profile', 'apps:read' ]; // the minimum scopes
|
||||
|
||||
for (let r of roles) {
|
||||
if (!ROLE_DEFINITIONS[r]) continue; // unknown or some legacy role
|
||||
@@ -142,5 +148,35 @@ function scopesForRoles(roles) {
|
||||
scopes = scopes.concat(ROLE_DEFINITIONS[r].scopes);
|
||||
}
|
||||
|
||||
return _.uniq(scopes.sort(), true /* isSorted */);
|
||||
// fold scopes so we don't have duplicate scopes
|
||||
let sortedScopes = scopes.sort();
|
||||
let set = new Set();
|
||||
for (let s of sortedScopes) {
|
||||
var parts = s.split(':');
|
||||
if (set.has(parts[0])) continue;
|
||||
set.add(s);
|
||||
}
|
||||
return Array.from(set);
|
||||
}
|
||||
|
||||
function validateToken(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error); // this triggers 'internal error' in passport
|
||||
|
||||
users.getWithRoles(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error);
|
||||
|
||||
const userScopes = scopesForRoles(user.roles);
|
||||
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
|
||||
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
|
||||
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+19
-10
@@ -37,6 +37,7 @@ var apps = require('./apps.js'),
|
||||
accesscontrol = require('./accesscontrol.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
util = require('util'),
|
||||
uuid = require('uuid');
|
||||
|
||||
@@ -252,18 +253,26 @@ function addTokenByUserId(clientId, userId, expiresAt, callback) {
|
||||
get(clientId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var scope = accesscontrol.canonicalScopeString(result.scope);
|
||||
|
||||
tokendb.add(token, userId, result.id, expiresAt, scope, function (error) {
|
||||
users.getWithRoles(userId, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
accessToken: token,
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
scope: result.scope,
|
||||
expires: expiresAt
|
||||
const userScopes = accesscontrol.scopesForRoles(user.roles);
|
||||
var scope = accesscontrol.canonicalScopeString(result.scope);
|
||||
var authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(',')).join(',');
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
|
||||
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes, function (error) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
accessToken: token,
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
scope: authorizedScopes,
|
||||
expires: expiresAt
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,12 +14,9 @@ var accesscontrol = require('../accesscontrol.js'),
|
||||
clients = require('../clients.js'),
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
ClientsError = clients.ClientsError,
|
||||
constants = require('../constants.js'),
|
||||
DatabaseError = require('../databaseerror.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
passport = require('passport'),
|
||||
tokendb = require('../tokendb'),
|
||||
users = require('../users.js'),
|
||||
UsersError = users.UsersError;
|
||||
|
||||
@@ -82,7 +79,9 @@ function initialize(callback) {
|
||||
}));
|
||||
|
||||
// used for "Authorization: Bearer token" or access_token query param authentication
|
||||
passport.use(new BearerStrategy(accessTokenAuth));
|
||||
passport.use(new BearerStrategy(function (token, callback) {
|
||||
accesscontrol.validateToken(token, callback);
|
||||
}));
|
||||
|
||||
callback(null);
|
||||
}
|
||||
@@ -93,31 +92,6 @@ function uninitialize(callback) {
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function accessTokenAuth(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error); // this triggers 'internal error' in passport
|
||||
|
||||
users.getWithRoles(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error);
|
||||
|
||||
// scopes here can define what capabilities that token carries
|
||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||
const userScopes = accesscontrol.scopesForRoles(user.roles);
|
||||
var authorizedScopes = accesscontrol.intersectScopes(userScopes, token.scope.split(','));
|
||||
// these clients do not require password checks unlike UI
|
||||
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli';
|
||||
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification };
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// The scope middleware provides an auth middleware for routes.
|
||||
//
|
||||
// It is used for API routes, which are authenticated using accesstokens.
|
||||
@@ -151,7 +125,7 @@ function websocketAuth(requiredScopes, req, res, next) {
|
||||
|
||||
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
accessTokenAuth(req.query.access_token, function (error, user, info) {
|
||||
accesscontrol.validateToken(req.query.access_token, function (error, user, info) {
|
||||
if (error) return next(new HttpError(500, error.message));
|
||||
if (!user) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
|
||||
+7
-21
@@ -19,8 +19,7 @@ exports = module.exports = {
|
||||
csrf: csrf
|
||||
};
|
||||
|
||||
var accesscontrol = require('../accesscontrol.js'),
|
||||
apps = require('../apps.js'),
|
||||
var apps = require('../apps.js'),
|
||||
assert = require('assert'),
|
||||
authcodedb = require('../authcodedb.js'),
|
||||
clients = require('../clients'),
|
||||
@@ -39,7 +38,6 @@ var accesscontrol = require('../accesscontrol.js'),
|
||||
session = require('connect-ensure-login'),
|
||||
settings = require('../settings.js'),
|
||||
speakeasy = require('speakeasy'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
url = require('url'),
|
||||
users = require('../users.js'),
|
||||
UsersError = users.UsersError,
|
||||
@@ -85,8 +83,6 @@ function initialize() {
|
||||
|
||||
// exchange authorization codes for access tokens. this is used by external oauth clients
|
||||
gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
|
||||
debug('exchange:', client, code, redirectURI);
|
||||
|
||||
authcodedb.get(code, function (error, authCode) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
@@ -95,16 +91,12 @@ function initialize() {
|
||||
authcodedb.del(code, function (error) {
|
||||
if(error) return callback(error);
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
var scope = accesscontrol.canonicalScopeString(client.scope);
|
||||
|
||||
tokendb.add(token, authCode.userId, authCode.clientId, expires, scope, function (error) {
|
||||
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('exchange: new access token for client %s token %s (scope: %s)', client.id, token.slice(0, 6), scope); // partial token for security
|
||||
debug('exchange: new access token for client %s user %s token %s', client.id, authCode.userId, result.accessToken.slice(0, 6)); // partial token for security
|
||||
|
||||
callback(null, token);
|
||||
callback(null, result.accessToken);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -112,18 +104,12 @@ function initialize() {
|
||||
|
||||
// implicit token grant that skips issuing auth codes. this is used by our webadmin
|
||||
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
|
||||
debug('grant token:', client.id, user.id, ares);
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
var scope = accesscontrol.canonicalScopeString(client.scope);
|
||||
|
||||
tokendb.add(token, user.id, client.id, expires, scope, function (error) {
|
||||
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('grant token: new access token for client %s token %s (scope: %s)', client.id, token.slice(0, 6), scope); // partial token for security
|
||||
debug('grant token: new access token for client %s user %s token %s', client.id, user.id, result.accessToken.slice(0, 6)); // partial token for security
|
||||
|
||||
callback(null, token);
|
||||
callback(null, result.accessToken);
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
+4
-12
@@ -246,22 +246,14 @@ function activate(username, password, email, displayName, ip, auditSource, callb
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
clients.get('cid-webadmin', function (error, result) {
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
// Also generate a token so the admin creation can also act as a login
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||
|
||||
tokendb.add(token, userObject.id, result.id, expires, accesscontrol.canonicalScopeString(result.scope), function (error) {
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
callback(null, { token: result.accessToken, expires: result.expires });
|
||||
|
||||
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||
|
||||
callback(null, { token: token, expires: expires });
|
||||
|
||||
setTimeout(cloudron.onActivated, 3000); // hack for now to not block the above http response
|
||||
});
|
||||
setTimeout(cloudron.onActivated, 3000); // hack for now to not block the above http response
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,16 +22,16 @@ describe('access control', function () {
|
||||
|
||||
describe('intersectScopes', function () { // args: allowed, wanted
|
||||
it('both are same', function () {
|
||||
expect(accesscontrol.intersectScopes([ 'apps', 'clients' ], [ 'clients', 'apps' ])).to.eql([ 'apps', 'clients' ]);
|
||||
expect(accesscontrol.intersectScopes([ 'apps', 'clients' ], [ 'apps', 'clients' ])).to.eql([ 'apps', 'clients' ]);
|
||||
});
|
||||
|
||||
it('some are different', function () {
|
||||
expect(accesscontrol.intersectScopes([ 'apps' ], [ 'clients', 'apps' ])).to.eql(['apps']);
|
||||
expect(accesscontrol.intersectScopes([ 'apps' ], [ 'apps', 'clients' ])).to.eql(['apps']);
|
||||
expect(accesscontrol.intersectScopes([ 'clients', 'domains', 'mail' ], [ 'mail' ])).to.eql(['mail']);
|
||||
});
|
||||
|
||||
it('everything is different', function () {
|
||||
expect(accesscontrol.intersectScopes(['cloudron', 'domains' ], ['clients', 'apps'])).to.eql([]);
|
||||
expect(accesscontrol.intersectScopes(['cloudron', 'domains' ], ['apps','clients'])).to.eql([]);
|
||||
});
|
||||
|
||||
it('subscopes', function () {
|
||||
|
||||
Reference in New Issue
Block a user