diff --git a/migrations/20180827211107-tokens-add-name.js b/migrations/20180827211107-tokens-add-name.js new file mode 100644 index 000000000..845848fa8 --- /dev/null +++ b/migrations/20180827211107-tokens-add-name.js @@ -0,0 +1,12 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE tokens ADD COLUMN name VARCHAR(64) DEFAULT ""', [], callback); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE tokens DROP COLUMN name', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index b05b4d3e0..13e1d4405 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -41,6 +41,7 @@ CREATE TABLE IF NOT EXISTS groupMembers( FOREIGN KEY(userId) REFERENCES users(id)); CREATE TABLE IF NOT EXISTS tokens( + name VARCHAR(64) DEFAULT "", // description accessToken VARCHAR(128) NOT NULL UNIQUE, identifier VARCHAR(128) NOT NULL, clientId VARCHAR(128), diff --git a/src/clients.js b/src/clients.js index 8378c0b0d..5c2fa6741 100644 --- a/src/clients.js +++ b/src/clients.js @@ -79,6 +79,14 @@ function validateClientName(name) { return null; } +function validateTokenName(name) { + assert.strictEqual(typeof name, 'string'); + + if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long'); + + return null; +} + function add(appId, type, redirectURI, scope, callback) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof type, 'string'); @@ -244,12 +252,17 @@ function delByAppIdAndType(appId, type, callback) { }); } -function addTokenByUserId(clientId, userId, expiresAt, callback) { +function addTokenByUserId(clientId, userId, expiresAt, options, callback) { assert.strictEqual(typeof clientId, 'string'); assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof expiresAt, 'number'); + assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); + const name = options.name || ''; + let error = validateTokenName(name); + if (error) return callback(error); + get(clientId, function (error, result) { if (error) return callback(error); @@ -265,7 +278,7 @@ function addTokenByUserId(clientId, userId, expiresAt, callback) { var token = tokendb.generateToken(); - tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), function (error) { + tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) { if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error)); callback(null, { @@ -282,17 +295,17 @@ function addTokenByUserId(clientId, userId, expiresAt, callback) { } // this issues a cid-cli token that does not require a password in various routes -function issueDeveloperToken(userObject, ip, callback) { +function issueDeveloperToken(userObject, auditSource, callback) { assert.strictEqual(typeof userObject, 'object'); - assert.strictEqual(typeof ip, 'string'); + assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION; - addTokenByUserId('cid-cli', userObject.id, expiresAt, function (error, result) { + addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) { if (error) return callback(error); - eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: users.removePrivateFields(userObject) }); + eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) }); callback(null, result); }); diff --git a/src/routes/clients.js b/src/routes/clients.js index 4285256e2..36f70432b 100644 --- a/src/routes/clients.js +++ b/src/routes/clients.js @@ -80,8 +80,9 @@ function addToken(req, res, next) { var data = req.body; var expiresAt = data.expiresAt ? parseInt(data.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION; if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future')); + if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string')); - clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, function (error, result) { + clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, { name: req.body.name || '' }, function (error, result) { if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message)); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(201, { token: result })); diff --git a/src/routes/developer.js b/src/routes/developer.js index 0269d579f..4e9ccc90f 100644 --- a/src/routes/developer.js +++ b/src/routes/developer.js @@ -24,7 +24,8 @@ function login(req, res, next) { if (!verified) return next(new HttpError(401, 'Invalid totpToken')); } - clients.issueDeveloperToken(user, ip, function (error, result) { + const auditSource = { authType: 'cli', ip: ip }; + clients.issueDeveloperToken(user, auditSource, function (error, result) { if (error) return next(new HttpError(500, error)); next(new HttpSuccess(200, result)); diff --git a/src/routes/oauth2.js b/src/routes/oauth2.js index 54955505f..625ec6326 100644 --- a/src/routes/oauth2.js +++ b/src/routes/oauth2.js @@ -91,7 +91,7 @@ function initialize() { authcodedb.del(code, function (error) { if(error) return callback(error); - clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) { + 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 user %s token %s', client.id, authCode.userId, result.accessToken.slice(0, 6)); // partial token for security @@ -104,7 +104,7 @@ 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) { - clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) { + 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 user %s token %s', client.id, user.id, result.accessToken.slice(0, 6)); // partial token for security @@ -364,7 +364,7 @@ function accountSetup(req, res, next) { if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message); if (error) return next(new HttpError(500, error)); - clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) { + clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { if (error) return next(new HttpError(500, error)); res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`); @@ -412,7 +412,7 @@ function passwordReset(req, res, next) { if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(406, error.message)); if (error) return next(new HttpError(500, error)); - clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) { + clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { if (error) return next(new HttpError(500, error)); res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`); diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index 79182aa04..43f7525ae 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -216,7 +216,7 @@ function startBox(done) { token_1 = tokendb.generateToken(); // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, accesscontrol.SCOPE_ANY, callback); + tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, accesscontrol.SCOPE_ANY, '', callback); }, function (callback) { diff --git a/src/routes/test/cloudron-test.js b/src/routes/test/cloudron-test.js index 623c1ddb4..940b734ab 100644 --- a/src/routes/test/cloudron-test.js +++ b/src/routes/test/cloudron-test.js @@ -167,7 +167,7 @@ describe('Cloudron', function () { userId_1 = result.body.id; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', callback); + tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', '', callback); }); } ], done); diff --git a/src/routes/test/eventlog-test.js b/src/routes/test/eventlog-test.js index 15b15c027..a1f67abbc 100644 --- a/src/routes/test/eventlog-test.js +++ b/src/routes/test/eventlog-test.js @@ -63,7 +63,7 @@ function setup(done) { token_1 = tokendb.generateToken(); // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, callback); + tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback); } ], done); diff --git a/src/routes/test/groups-test.js b/src/routes/test/groups-test.js index e2ad2a523..5021a3de0 100644 --- a/src/routes/test/groups-test.js +++ b/src/routes/test/groups-test.js @@ -70,7 +70,7 @@ function setup(done) { userId_1 = result.body.id; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, callback); + tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback); }); } ], done); diff --git a/src/routes/test/profile-test.js b/src/routes/test/profile-test.js index 6df5ca5fb..c24572dba 100644 --- a/src/routes/test/profile-test.js +++ b/src/routes/test/profile-test.js @@ -115,7 +115,7 @@ describe('Profile API', function () { var token = tokendb.generateToken(); var expires = Date.now() - 2000; // 1 sec - tokendb.add(token, user_0.id, null, expires, 'profile', function (error) { + tokendb.add(token, user_0.id, null, expires, 'profile', 'tokenname', function (error) { expect(error).to.not.be.ok(); superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) { diff --git a/src/routes/test/users-test.js b/src/routes/test/users-test.js index 888b90808..20aa3ea50 100644 --- a/src/routes/test/users-test.js +++ b/src/routes/test/users-test.js @@ -176,7 +176,7 @@ describe('Users API', function () { var token = tokendb.generateToken(); var expires = Date.now() + 2000; // 1 sec - tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_PROFILE, function (error) { + tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_PROFILE, 'tokenname', function (error) { expect(error).to.not.be.ok(); setTimeout(function () { @@ -287,7 +287,7 @@ describe('Users API', function () { user_1 = result.body; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, accesscontrol.SCOPE_PROFILE, done); + tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, accesscontrol.SCOPE_PROFILE, 'fromtest', done); }); }); @@ -701,7 +701,7 @@ describe('Users API', function () { token = tokendb.generateToken(); var expires = Date.now() + 2000; // 1 sec - tokendb.add(token, user_4.id, null, expires, accesscontrol.SCOPE_PROFILE, done); + tokendb.add(token, user_4.id, null, expires, accesscontrol.SCOPE_PROFILE, '', done); }); }); diff --git a/src/setup.js b/src/setup.js index 32ab0932e..c65df1fbc 100644 --- a/src/setup.js +++ b/src/setup.js @@ -247,7 +247,7 @@ 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.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, 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)); eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { }); diff --git a/src/test/database-test.js b/src/test/database-test.js index ee4893d01..08516616a 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -553,6 +553,7 @@ describe('database', function () { describe('token', function () { var TOKEN_0 = { + name: 'token0', accessToken: tokendb.generateToken(), identifier: '0', clientId: 'clientid-0', @@ -560,6 +561,7 @@ describe('database', function () { scope: 'clients' }; var TOKEN_1 = { + name: 'token1', accessToken: tokendb.generateToken(), identifier: '1', clientId: 'clientid-1', @@ -567,6 +569,7 @@ describe('database', function () { scope: 'settings' }; var TOKEN_2 = { + name: 'token2', accessToken: tokendb.generateToken(), identifier: '2', clientId: 'clientid-2', @@ -582,14 +585,14 @@ describe('database', function () { }); it('add succeeds', function (done) { - tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) { + tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) { expect(error).to.be(null); done(); }); }); it('add of same token fails', function (done) { - tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) { + tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) { expect(error).to.be.a(DatabaseError); expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS); done(); @@ -642,7 +645,7 @@ describe('database', function () { }); it('delByIdentifier succeeds', function (done) { - tokendb.add(TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, function (error) { + tokendb.add(TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, '', function (error) { expect(error).to.be(null); tokendb.delByIdentifier(TOKEN_1.identifier, function (error) { @@ -661,7 +664,7 @@ describe('database', function () { }); it('getByIdentifierAndClientId succeeds', function (done) { - tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) { + tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) { expect(error).to.be(null); tokendb.getByIdentifierAndClientId(TOKEN_0.identifier, TOKEN_0.clientId, function (error, result) { @@ -675,7 +678,7 @@ describe('database', function () { }); it('delExpired succeeds', function (done) { - tokendb.add(TOKEN_2.accessToken, TOKEN_2.identifier, TOKEN_2.clientId, TOKEN_2.expires, TOKEN_2.scope, function (error) { + tokendb.add(TOKEN_2.accessToken, TOKEN_2.identifier, TOKEN_2.clientId, TOKEN_2.expires, TOKEN_2.scope, TOKEN_2.name, function (error) { expect(error).to.be(null); tokendb.delExpired(function (error, result) { @@ -706,7 +709,7 @@ describe('database', function () { }); it('delByClientId succeeds', function (done) { - tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) { + tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) { expect(error).to.be(null); tokendb.delByClientId(TOKEN_0.clientId, function (error) { diff --git a/src/test/janitor-test.js b/src/test/janitor-test.js index 19cbfb21a..dce9aa0a8 100644 --- a/src/test/janitor-test.js +++ b/src/test/janitor-test.js @@ -33,7 +33,8 @@ describe('janitor', function () { identifier: '0', clientId: 'clientid-0', expires: Date.now() + 60 * 60 * 1000, - scope: 'settings' + scope: 'settings', + name: 'clientid0' }; var TOKEN_1 = { accessToken: tokendb.generateToken(), @@ -41,6 +42,7 @@ describe('janitor', function () { clientId: 'clientid-1', expires: Date.now() - 1000, scope: 'apps', + name: 'clientid1' }; before(function (done) { @@ -49,8 +51,8 @@ describe('janitor', function () { database._clear, authcodedb.add.bind(null, AUTHCODE_0.authCode, AUTHCODE_0.clientId, AUTHCODE_0.userId, AUTHCODE_0.expiresAt), authcodedb.add.bind(null, AUTHCODE_1.authCode, AUTHCODE_1.clientId, AUTHCODE_1.userId, AUTHCODE_1.expiresAt), - tokendb.add.bind(null, TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope), - tokendb.add.bind(null, TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope) + tokendb.add.bind(null, TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name), + tokendb.add.bind(null, TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, TOKEN_1.name) ], done); }); diff --git a/src/tokendb.js b/src/tokendb.js index 7bdf7c574..05e584dba 100644 --- a/src/tokendb.js +++ b/src/tokendb.js @@ -22,7 +22,7 @@ var assert = require('assert'), DatabaseError = require('./databaseerror'), hat = require('./hat.js'); -var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires' ].join(','); +var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires', 'name' ].join(','); function generateToken() { return hat(8 * 32); // TODO: make this stronger @@ -40,16 +40,17 @@ function get(accessToken, callback) { }); } -function add(accessToken, identifier, clientId, expires, scope, callback) { +function add(accessToken, identifier, clientId, expires, scope, name, callback) { assert.strictEqual(typeof accessToken, 'string'); assert.strictEqual(typeof identifier, 'string'); assert(typeof clientId === 'string' || clientId === null); assert.strictEqual(typeof expires, 'number'); assert.strictEqual(typeof scope, 'string'); + assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('INSERT INTO tokens (accessToken, identifier, clientId, expires, scope) VALUES (?, ?, ?, ?, ?)', - [ accessToken, identifier, clientId, expires, scope ], function (error, result) { + database.query('INSERT INTO tokens (accessToken, identifier, clientId, expires, scope, name) VALUES (?, ?, ?, ?, ?, ?)', + [ accessToken, identifier, clientId, expires, scope, name ], function (error, result) { if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS)); if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));