diff --git a/src/oidcclients.js b/src/oidcclients.js index a5ca42191..0536474e2 100644 --- a/src/oidcclients.js +++ b/src/oidcclients.js @@ -6,6 +6,14 @@ exports = module.exports = { del, update, list, + + validateId, + + // token client ids. we categorize them so we can have different restrictions based on the client + ID_WEBADMIN: 'cid-webadmin', // dashboard + ID_DEVELOPMENT: 'cid-development', // dashboard development + ID_CLI: 'cid-cli', // cloudron cli + ID_SDK: 'cid-sdk', // created by user via dashboard }; const assert = require('assert'), @@ -13,13 +21,21 @@ const assert = require('assert'), dashboard = require('./dashboard.js'), database = require('./database.js'), hat = require('./hat.js'), - safe = require('safetydance'), - tokens = require('./tokens.js'); + safe = require('safetydance'); const OIDC_CLIENTS_TABLE_NAME = 'oidcClients'; const OIDC_CLIENTS_FIELDS = [ 'id', 'secret', 'name', 'appId', 'loginRedirectUri', 'tokenSignatureAlgorithm' ]; const DEFAULT_TOKEN_SIGNATURE_ALGORITHM='RS256'; +function validateId(type) { + assert.strictEqual(typeof type, 'string'); + + const types = [ exports.ID_WEBADMIN, exports.ID_SDK, exports.ID_DEVELOPMENT, exports.ID_CLI ]; + if (types.indexOf(type) === -1) return new BoxError(BoxError.BAD_FIELD, `type must be one of ${types.join(',')}`); + + return null; +} + function postProcess(result) { assert.strictEqual(typeof result, 'object'); @@ -50,19 +66,19 @@ async function add(data) { async function get(id) { assert.strictEqual(typeof id, 'string'); - if (id === tokens.ID_WEBADMIN) { + if (id === exports.ID_WEBADMIN) { const { fqdn:dashboardFqdn } = await dashboard.getLocation(); return { - id: tokens.ID_WEBADMIN, + id: exports.ID_WEBADMIN, secret: 'notused', application_type: 'web', response_types: ['code', 'code token'], grant_types: ['authorization_code', 'implicit'], loginRedirectUri: `https://${dashboardFqdn}/authcallback.html` }; - } else if (id === tokens.ID_DEVELOPMENT) { + } else if (id === exports.ID_DEVELOPMENT) { return { - id: tokens.ID_DEVELOPMENT, + id: exports.ID_DEVELOPMENT, secret: 'notused', application_type: 'native', // have to use native here to support plaintext http, this however makes it impossible to skip consent screen response_types: ['code', 'code token'], diff --git a/src/oidcserver.js b/src/oidcserver.js index 97fe18e21..3405697f9 100644 --- a/src/oidcserver.js +++ b/src/oidcserver.js @@ -154,7 +154,7 @@ class StorageAdapter { if (this.name === 'Client') { debug('upsert: this should not happen as it is stored in our db'); - } else if (this.name === 'AccessToken' && (payload.clientId === tokens.ID_WEBADMIN || payload.clientId === tokens.ID_DEVELOPMENT)) { + } else if (this.name === 'AccessToken' && (payload.clientId === oidcClients.ID_WEBADMIN || payload.clientId === oidcClients.ID_DEVELOPMENT)) { const expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS; const [error] = await safe(tokens.add({ clientId: payload.clientId, identifier: payload.accountId, expires, accessToken: id, allowedIpRanges: '' })); @@ -313,7 +313,7 @@ async function renderInteractionPage(req, res) { ICON_URL: '/api/v1/cloudron/avatar', NAME: client?.name || await branding.getCloudronName(), FOOTER: marked.parse(await branding.renderFooter()), - NOTE: (client.id === tokens.ID_WEBADMIN && constants.DEMO) ? '
This is a demo. Username and password is "cloudron"
' : '' + NOTE: (client.id === oidcClients.ID_WEBADMIN && constants.DEMO) ? '
This is a demo. Username and password is "cloudron"
' : '' }; if (app) { @@ -691,7 +691,7 @@ async function start() { if (grantId) { return await ctx.oidc.provider.Grant.find(grantId); - } else if (ctx.oidc.client.clientId === tokens.ID_WEBADMIN || ctx.oidc.client.clientId === tokens.ID_DEVELOPMENT) { + } else if (ctx.oidc.client.clientId === oidcClients.ID_WEBADMIN || ctx.oidc.client.clientId === oidcClients.ID_DEVELOPMENT) { const grant = new ctx.oidc.provider.Grant({ clientId: ctx.oidc.client.clientId, accountId: ctx.oidc.session.accountId, diff --git a/src/provision.js b/src/provision.js index cdccc7c09..62df2e202 100644 --- a/src/provision.js +++ b/src/provision.js @@ -22,6 +22,7 @@ const appstore = require('./appstore.js'), mail = require('./mail.js'), mailServer = require('./mailserver.js'), network = require('./network.js'), + oidcClients = require('./oidcclients.js'), platform = require('./platform.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), @@ -155,7 +156,7 @@ async function activate(username, password, email, displayName, ip, auditSource) if (error && error.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Already activated'); if (error) throw error; - const token = { clientId: tokens.ID_WEBADMIN, identifier: ownerId, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; + const token = { clientId: oidcClients.ID_WEBADMIN, identifier: ownerId, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; const result = await tokens.add(token); await eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, {}); diff --git a/src/routes/auth.js b/src/routes/auth.js index c4ebf9b97..7b01cfade 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -19,6 +19,7 @@ const assert = require('assert'), eventlog = require('../eventlog.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + oidcClients = require('../oidcclients.js'), safe = require('safetydance'), speakeasy = require('speakeasy'), tokens = require('../tokens.js'), @@ -29,18 +30,18 @@ async function login(req, res, next) { if ('type' in req.body && typeof req.body.type !== 'string') return next(new HttpError(400, 'type must be a string')); - const type = req.body.type || tokens.ID_WEBADMIN; + const type = req.body.type || oidcClients.ID_WEBADMIN; const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null; const userAgent = req.headers['user-agent'] || ''; - const tokenTypeError = tokens.validateTokenType(type); + const tokenTypeError = oidcClients.validateId(type); if (tokenTypeError) return next(new HttpError(400, tokenTypeError.message)); const [error, token] = await safe(tokens.add({ clientId: type, identifier: req.user.id, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); if (error) return next(new HttpError(500, error)); const auditSource = AuditSource.fromRequest(req); - await eventlog.add(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user), type, appId: tokens.ID_CLI }); + await eventlog.add(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user), type, appId: oidcClients.ID_CLI }); await safe(users.notifyLoginLocation(req.user, ip, userAgent, auditSource), { debug }); next(new HttpSuccess(200, token)); @@ -90,7 +91,7 @@ async function passwordReset(req, res, next) { if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message)); if (error) return next(BoxError.toHttpError(error)); - const [addError, result] = await safe(tokens.add({ clientId: tokens.ID_WEBADMIN, identifier: userObject.id, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); + const [addError, result] = await safe(tokens.add({ clientId: oidcClients.ID_WEBADMIN, identifier: userObject.id, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); if (addError) return next(BoxError.toHttpError(addError)); next(new HttpSuccess(202, { accessToken: result.accessToken })); diff --git a/src/routes/test/common.js b/src/routes/test/common.js index 22f82f540..38fd9a9f3 100644 --- a/src/routes/test/common.js +++ b/src/routes/test/common.js @@ -9,6 +9,7 @@ const apps = require('../../apps.js'), fs = require('fs'), mailer = require('../../mailer.js'), nock = require('nock'), + oidcClients = require('../../oidcclients.js'), oidcServer = require('../../oidcserver.js'), safe = require('safetydance'), server = require('../../server.js'), @@ -155,7 +156,7 @@ async function setup() { expect(response.status).to.equal(201); admin.id = response.body.id; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - const token1 = await tokens.add({ identifier: admin.id, clientId: tokens.ID_WEBADMIN, expires: Date.now() + (60 * 60 * 1000), name: 'fromtest', allowedIpRanges: '' }); + const token1 = await tokens.add({ identifier: admin.id, clientId: oidcClients.ID_WEBADMIN, expires: Date.now() + (60 * 60 * 1000), name: 'fromtest', allowedIpRanges: '' }); admin.token = token1.accessToken; // create user @@ -165,7 +166,7 @@ async function setup() { expect(response.status).to.equal(201); user.id = response.body.id; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - const token2 = await tokens.add({ identifier: user.id, clientId: tokens.ID_WEBADMIN, expires: Date.now() + (60 * 60 * 1000), name: 'fromtest', allowedIpRanges: '' }); + const token2 = await tokens.add({ identifier: user.id, clientId: oidcClients.ID_WEBADMIN, expires: Date.now() + (60 * 60 * 1000), name: 'fromtest', allowedIpRanges: '' }); user.token = token2.accessToken; // create app object diff --git a/src/routes/tokens.js b/src/routes/tokens.js index c5b75fd48..e3fd998c8 100644 --- a/src/routes/tokens.js +++ b/src/routes/tokens.js @@ -12,6 +12,7 @@ const assert = require('assert'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + oidcClients = require('../oidcclients.js'), safe = require('safetydance'), tokens = require('../tokens.js'); @@ -59,7 +60,7 @@ async function add(req, res, next) { const scope = req.body.scope || null; const allowedIpRanges = req.body.allowedIpRanges || ''; - const [error, result] = await safe(tokens.add({ clientId: tokens.ID_SDK, identifier: req.user.id, expires: expiresAt, name: req.body.name, scope, allowedIpRanges })); + const [error, result] = await safe(tokens.add({ clientId: oidcClients.ID_SDK, identifier: req.user.id, expires: expiresAt, name: req.body.name, scope, allowedIpRanges })); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(201, result)); diff --git a/src/test/tokens-test.js b/src/test/tokens-test.js index 34cb5c8b4..421e9cd75 100644 --- a/src/test/tokens-test.js +++ b/src/test/tokens-test.js @@ -9,6 +9,7 @@ const BoxError = require('../boxerror.js'), common = require('./common.js'), expect = require('expect.js'), + oidcClients = require('../oidcclients.js'), safe = require('safetydance'), tokens = require('../tokens.js'); @@ -155,7 +156,7 @@ describe('Tokens', function () { const token1 = { name: 'token1', identifier: 'user1', - clientId: tokens.ID_WEBADMIN, + clientId: oidcClients.ID_WEBADMIN, expires: Number.MAX_SAFE_INTEGER, lastUsedTime: null, scope: { '*': 'rw' }, @@ -164,7 +165,7 @@ describe('Tokens', function () { const token2 = { name: 'token2', identifier: 'user1', - clientId: tokens.ID_SDK, + clientId: oidcClients.ID_SDK, expires: Date.now(), lastUsedTime: null, allowedIpRanges: '#this' @@ -173,11 +174,11 @@ describe('Tokens', function () { await tokens.add(token1); await tokens.add(token2); - await tokens.delByUserIdAndType('user2', tokens.ID_WEBADMIN); + await tokens.delByUserIdAndType('user2', oidcClients.ID_WEBADMIN); let result = await tokens.listByUserId('user1'); expect(result.length).to.be(2); // should not have deleted user1 tokens - await tokens.delByUserIdAndType('user1', tokens.ID_WEBADMIN); + await tokens.delByUserIdAndType('user1', oidcClients.ID_WEBADMIN); result = await tokens.listByUserId('user1'); expect(result.length).to.be(1); expect(result[0].name).to.be(token2.name); diff --git a/src/test/user-directory-test.js b/src/test/user-directory-test.js index 679ca9732..4aad4c42a 100644 --- a/src/test/user-directory-test.js +++ b/src/test/user-directory-test.js @@ -7,6 +7,7 @@ const common = require('./common.js'), expect = require('expect.js'), + oidcClients = require('../oidcclients.js'), tokens = require('../tokens.js'), userDirectory = require('../user-directory.js'); @@ -24,7 +25,7 @@ describe('User Directory', function () { }); it('can set default profile config', async function () { - await tokens.add({ name: 'token1', identifier: admin.id, clientId: tokens.ID_WEBADMIN, expires: Number.MAX_SAFE_INTEGER, lastUsedTime: null, allowedIpRanges: '' }); + await tokens.add({ name: 'token1', identifier: admin.id, clientId: oidcClients.ID_WEBADMIN, expires: Number.MAX_SAFE_INTEGER, lastUsedTime: null, allowedIpRanges: '' }); let result = await tokens.listByUserId(admin.id); expect(result.length).to.be(1); // just confirm the token was really added! diff --git a/src/tokens.js b/src/tokens.js index c99d115eb..1b79e0b8c 100644 --- a/src/tokens.js +++ b/src/tokens.js @@ -13,18 +13,10 @@ exports = module.exports = { listByUserId, getByAccessToken, - validateTokenType, - hasScope, isIpAllowedSync, - // token client ids. we categorize them so we can have different restrictions based on the client - ID_WEBADMIN: 'cid-webadmin', // dashboard - ID_DEVELOPMENT: 'cid-development', // dashboard development - ID_CLI: 'cid-cli', // cloudron cli - ID_SDK: 'cid-sdk', // created by user via dashboard - SCOPES: ['*']//, 'apps', 'domains'], }; @@ -57,15 +49,6 @@ function validateTokenName(name) { return null; } -function validateTokenType(type) { - assert.strictEqual(typeof type, 'string'); - - const types = [ exports.ID_WEBADMIN, exports.ID_SDK, exports.ID_DEVELOPMENT, exports.ID_CLI ]; - if (types.indexOf(type) === -1) return new BoxError(BoxError.BAD_FIELD, `type must be one of ${types.join(',')}`); - - return null; -} - function validateScope(scope) { assert.strictEqual(typeof scope, 'object'); diff --git a/src/user-directory.js b/src/user-directory.js index 3cd60109e..d4098a11e 100644 --- a/src/user-directory.js +++ b/src/user-directory.js @@ -10,6 +10,7 @@ const assert = require('assert'), constants = require('./constants.js'), debug = require('debug')('box:user-directory'), eventlog = require('./eventlog.js'), + oidcClients = require('./oidcclients.js'), oidcServer = require('./oidcserver.js'), settings = require('./settings.js'), tokens = require('./tokens.js'), @@ -41,7 +42,7 @@ async function setProfileConfig(profileConfig, options, auditSource) { if (user.twoFactorAuthenticationEnabled) continue; if (options.persistUserIdSessions === user.id) continue; // do not logout the API caller - await tokens.delByUserIdAndType(user.id, tokens.ID_WEBADMIN); + await tokens.delByUserIdAndType(user.id, oidcClients.ID_WEBADMIN); await oidcServer.revokeByUserId(user.id); } } diff --git a/src/users.js b/src/users.js index 23c5d0391..3dc58d91a 100644 --- a/src/users.js +++ b/src/users.js @@ -91,7 +91,7 @@ const appPasswords = require('./apppasswords.js'), mailer = require('./mailer.js'), mysql = require('mysql'), notifications = require('./notifications'), - paths = require('./paths.js'), + oidcClients = require('./oidcclients.js'), qrcode = require('qrcode'), safe = require('safetydance'), settings = require('./settings.js'), @@ -876,7 +876,7 @@ async function setupAccount(user, data, auditSource) { await setPassword(user, data.password, auditSource); - const token = { clientId: tokens.ID_WEBADMIN, identifier: user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS, allowedIpRanges: '' }; + const token = { clientId: oidcClients.ID_WEBADMIN, identifier: user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS, allowedIpRanges: '' }; const result = await tokens.add(token); return result.accessToken; }