2018-04-26 15:54:53 -07:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
|
|
|
|
SCOPE_APPS: 'apps',
|
2018-05-01 11:32:12 -07:00
|
|
|
SCOPE_CLIENTS: 'clients',
|
2018-04-26 15:54:53 -07:00
|
|
|
SCOPE_CLOUDRON: 'cloudron',
|
2018-05-01 11:32:12 -07:00
|
|
|
SCOPE_DOMAINS: 'domains',
|
|
|
|
|
SCOPE_MAIL: 'mail',
|
|
|
|
|
SCOPE_PROFILE: 'profile',
|
2018-04-26 15:54:53 -07:00
|
|
|
SCOPE_SETTINGS: 'settings',
|
|
|
|
|
SCOPE_USERS: 'users',
|
2018-05-01 11:32:12 -07:00
|
|
|
VALID_SCOPES: [ 'apps', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ],
|
2018-04-26 15:54:53 -07:00
|
|
|
|
2018-04-27 21:47:11 -07:00
|
|
|
SCOPE_ANY: '*',
|
|
|
|
|
|
2018-05-01 13:34:46 -07:00
|
|
|
initialize: initialize,
|
|
|
|
|
uninitialize: uninitialize,
|
|
|
|
|
|
|
|
|
|
accessTokenAuth: accessTokenAuth,
|
|
|
|
|
|
2018-04-26 15:54:53 -07:00
|
|
|
validateScope: validateScope,
|
2018-04-30 21:21:18 -07:00
|
|
|
validateRequestedScopes: validateRequestedScopes,
|
2018-05-01 11:32:12 -07:00
|
|
|
normalizeScope: normalizeScope,
|
|
|
|
|
canonicalScope: canonicalScope
|
2018-04-26 15:54:53 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var assert = require('assert'),
|
2018-05-01 13:34:46 -07:00
|
|
|
BasicStrategy = require('passport-http').BasicStrategy,
|
|
|
|
|
BearerStrategy = require('passport-http-bearer').Strategy,
|
|
|
|
|
clients = require('./clients'),
|
|
|
|
|
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
|
|
|
|
ClientsError = clients.ClientsError,
|
|
|
|
|
DatabaseError = require('./databaseerror'),
|
2018-04-30 21:21:18 -07:00
|
|
|
debug = require('debug')('box:accesscontrol'),
|
2018-05-01 13:34:46 -07:00
|
|
|
LocalStrategy = require('passport-local').Strategy,
|
|
|
|
|
passport = require('passport'),
|
|
|
|
|
tokendb = require('./tokendb'),
|
|
|
|
|
users = require('./users.js'),
|
|
|
|
|
UsersError = users.UsersError,
|
2018-04-30 21:21:18 -07:00
|
|
|
_ = require('underscore');
|
2018-04-26 15:54:53 -07:00
|
|
|
|
2018-05-01 13:34:46 -07:00
|
|
|
function initialize(callback) {
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
passport.serializeUser(function (user, callback) {
|
|
|
|
|
callback(null, user.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
passport.deserializeUser(function(userId, callback) {
|
|
|
|
|
users.get(userId, function (error, result) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback(null, result);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
passport.use(new LocalStrategy(function (username, password, callback) {
|
|
|
|
|
if (username.indexOf('@') === -1) {
|
|
|
|
|
users.verifyWithUsername(username, password, function (error, result) {
|
|
|
|
|
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
|
|
|
|
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
if (!result) return callback(null, false);
|
|
|
|
|
callback(null, result);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
users.verifyWithEmail(username, password, function (error, result) {
|
|
|
|
|
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
|
|
|
|
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
if (!result) return callback(null, false);
|
|
|
|
|
callback(null, result);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
passport.use(new BasicStrategy(function (username, password, callback) {
|
|
|
|
|
if (username.indexOf('cid-') === 0) {
|
|
|
|
|
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
|
|
|
|
// username is actually client id here
|
|
|
|
|
// password is client secret
|
|
|
|
|
clients.get(username, function (error, client) {
|
|
|
|
|
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
if (client.clientSecret != password) return callback(null, false);
|
|
|
|
|
return callback(null, client);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
users.verifyWithUsername(username, password, function (error, result) {
|
|
|
|
|
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
|
|
|
|
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
if (!result) return callback(null, false);
|
|
|
|
|
callback(null, result);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
|
|
|
|
clients.get(clientId, function(error, client) {
|
|
|
|
|
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
|
|
|
|
if (error) { return callback(error); }
|
|
|
|
|
if (client.clientSecret != clientSecret) { return callback(null, false); }
|
|
|
|
|
return callback(null, client);
|
|
|
|
|
});
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
passport.use(new BearerStrategy(accessTokenAuth));
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function uninitialize(callback) {
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-02 12:36:35 -07:00
|
|
|
function canonicalScope(scope) {
|
|
|
|
|
return scope.replace(exports.SCOPE_ANY, exports.VALID_SCOPES.join(','));
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-01 13:34:46 -07:00
|
|
|
function normalizeScope(allowedScope, wantedScope) {
|
|
|
|
|
assert.strictEqual(typeof allowedScope, 'string');
|
|
|
|
|
assert.strictEqual(typeof wantedScope, 'string');
|
|
|
|
|
|
|
|
|
|
const allowedScopes = allowedScope.split(',');
|
|
|
|
|
const wantedScopes = wantedScope.split(',');
|
|
|
|
|
|
2018-05-02 12:36:35 -07:00
|
|
|
if (allowedScopes.indexOf(exports.SCOPE_ANY) !== -1) return canonicalScope(wantedScope);
|
|
|
|
|
if (wantedScopes.indexOf(exports.SCOPE_ANY) !== -1) return canonicalScope(allowedScope);
|
2018-05-01 13:34:46 -07:00
|
|
|
|
|
|
|
|
return _.intersection(allowedScopes, wantedScopes).join(',');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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, false);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
users.get(token.identifier, function (error, user) {
|
|
|
|
|
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
|
|
|
|
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
|
|
|
|
|
var scope = normalizeScope(user.scope, token.scope);
|
|
|
|
|
var info = { scope: scope, clientId: token.clientId };
|
|
|
|
|
|
|
|
|
|
callback(null, user, info);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-26 15:54:53 -07:00
|
|
|
function validateScope(scope) {
|
|
|
|
|
assert.strictEqual(typeof scope, 'string');
|
|
|
|
|
|
|
|
|
|
if (scope === '') return new Error('Empty scope not allowed');
|
|
|
|
|
|
2018-05-02 12:36:35 -07:00
|
|
|
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
|
|
|
|
|
// us not write a migration script every time we add a new scope
|
2018-05-01 11:32:12 -07:00
|
|
|
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s) !== -1; });
|
|
|
|
|
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
|
2018-04-26 15:54:53 -07:00
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tests if all requestedScopes are attached to the request
|
|
|
|
|
function validateRequestedScopes(authInfo, requestedScopes) {
|
|
|
|
|
assert.strictEqual(typeof authInfo, 'object');
|
|
|
|
|
assert(Array.isArray(requestedScopes));
|
|
|
|
|
|
|
|
|
|
if (!authInfo || !authInfo.scope) return new Error('No scope found');
|
|
|
|
|
|
|
|
|
|
var scopes = authInfo.scope.split(',');
|
|
|
|
|
|
2018-04-30 21:44:24 -07:00
|
|
|
if (scopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
|
2018-04-26 15:54:53 -07:00
|
|
|
|
|
|
|
|
for (var i = 0; i < requestedScopes.length; ++i) {
|
|
|
|
|
if (scopes.indexOf(requestedScopes[i]) === -1) {
|
|
|
|
|
debug('scope: missing scope "%s".', requestedScopes[i]);
|
|
|
|
|
return new Error('Missing required scope "' + requestedScopes[i] + '"');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|