From 35214a04dc5f19b7edd3979ee5d0e3b1fd0cdc03 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Thu, 27 Oct 2022 07:39:11 -0400 Subject: [PATCH] Feature/jwt auth (#75) * initial changes for jwt auth * return permissions as an array of string * Add JWT expiration environment variable (#74) * allow fromHost in createCall REST API * add JWT_EXPIRES_IN= env variable, 60 mins by default * add jwt expiration in register.js and signin.js * fix tests - add permissions and scope to encoded obj in jwt Co-authored-by: Dave Horton Co-authored-by: eglehelms * return only the jwt-token in the api response Co-authored-by: EgleH Co-authored-by: eglehelms --- db/jambones-sql.sql | 27 +++++ db/jambones.sqs | 116 +++++++++++++++++--- db/reset_admin_password.js | 10 ++ db/seed-integration-test.sql | 7 ++ db/seed-production-database-open-source.sql | 7 ++ db/seed-production-database.sql | 7 ++ lib/auth/index.js | 15 +-- lib/routes/api/login.js | 88 ++++++++------- lib/routes/api/register.js | 2 +- lib/routes/api/signin.js | 2 +- test/call-test.js | 8 +- test/recent-calls.js | 8 +- test/speech-credentials.js | 5 +- 13 files changed, 234 insertions(+), 68 deletions(-) diff --git a/db/jambones-sql.sql b/db/jambones-sql.sql index 3236bdf..a2f2b10 100644 --- a/db/jambones-sql.sql +++ b/db/jambones-sql.sql @@ -22,6 +22,10 @@ DROP TABLE IF EXISTS lcr_routes; DROP TABLE IF EXISTS password_settings; +DROP TABLE IF EXISTS user_permissions; + +DROP TABLE IF EXISTS permissions; + DROP TABLE IF EXISTS predefined_sip_gateways; DROP TABLE IF EXISTS predefined_smpp_gateways; @@ -145,6 +149,14 @@ require_digit BOOLEAN NOT NULL DEFAULT false, require_special_character BOOLEAN NOT NULL DEFAULT false ); +CREATE TABLE permissions +( +permission_sid CHAR(36) NOT NULL UNIQUE , +name VARCHAR(32) NOT NULL UNIQUE , +description VARCHAR(255), +PRIMARY KEY (permission_sid) +); + CREATE TABLE predefined_carriers ( predefined_carrier_sid CHAR(36) NOT NULL UNIQUE , @@ -318,6 +330,14 @@ is_active BOOLEAN NOT NULL DEFAULT true, PRIMARY KEY (user_sid) ); +CREATE TABLE user_permissions +( +user_permissions_sid CHAR(36) NOT NULL UNIQUE , +user_sid CHAR(36) NOT NULL, +permission_sid CHAR(36) NOT NULL, +PRIMARY KEY (user_permissions_sid) +); + CREATE TABLE voip_carriers ( voip_carrier_sid CHAR(36) NOT NULL UNIQUE , @@ -481,6 +501,7 @@ ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid); ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid); +CREATE INDEX permission_sid_idx ON permissions (permission_sid); CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid); CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid); CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid); @@ -552,6 +573,12 @@ CREATE INDEX service_provider_sid_idx ON users (service_provider_sid); ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid); CREATE INDEX email_activation_code_idx ON users (email_activation_code); +CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid); +CREATE INDEX user_sid_idx ON user_permissions (user_sid); +ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE; + +ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid); + CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid); CREATE INDEX account_sid_idx ON voip_carriers (account_sid); ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid); diff --git a/db/jambones.sqs b/db/jambones.sqs index 5a501e1..0cefdfc 100644 --- a/db/jambones.sqs +++ b/db/jambones.sqs @@ -287,8 +287,8 @@ - 2170.00 - 216.00 + 2690.00 + 260.00 276.00 @@ -328,7 +328,7 @@ 16.00 - 316.00 + 259.00 380.00 11 @@ -336,6 +336,7 @@ 1 + @@ -1001,8 +1002,8 @@ - 2131.00 - 136.00 + 2769.00 + 28.00 159.00 @@ -1355,8 +1356,8 @@ - 2123.00 - 24.00 + 2752.00 + 129.00 215.00 @@ -1387,6 +1388,42 @@ + + + + + 2144.00 + 170.00 + + + 261.00 + 80.00 + + 32 + + + + 1 + + + + + + + + + + + + + + + + + + + + @@ -2597,6 +2634,59 @@ + + + + + 2150.00 + 47.00 + + + 300.00 + 80.00 + + 33 + + + + 1 + + + + + + + + + user_sid + users + 1 + + + 4 + 1 + + + + + + + + + + permission_sid + permissions + + + 4 + 1 + + + + + + + @@ -2611,17 +2701,17 @@ - + - - - - - + + + + + diff --git a/db/reset_admin_password.js b/db/reset_admin_password.js index 53f15b0..e55634d 100755 --- a/db/reset_admin_password.js +++ b/db/reset_admin_password.js @@ -12,6 +12,9 @@ values (?, ?)`; const sqlQueryAccount = 'SELECT * from accounts LEFT JOIN api_keys ON api_keys.account_sid = accounts.account_sid'; const sqlAddAccountToken = `INSERT into api_keys (api_key_sid, token, account_sid) VALUES (?, ?, ?)`; +const sqlInsertPermissions = ` +INSERT into user_permissions (user_permissions_sid, user_sid, permission_sid) +VALUES (?,?,?)`; const password = process.env.JAMBONES_ADMIN_INITIAL_PASSWORD || 'admin'; console.log(`reset_admin_password, initial admin password is ${password}`); @@ -21,6 +24,7 @@ const doIt = async() => { const sid = uuidv4(); await promisePool.execute('DELETE from users where name = "admin"'); await promisePool.execute('DELETE from api_keys where account_sid is null and service_provider_sid is null'); + await promisePool.execute(sqlInsert, [ sid, @@ -34,6 +38,12 @@ const doIt = async() => { ); await promisePool.execute(sqlInsertAdminToken, [uuidv4(), uuidv4()]); + /* assign all permissions to the admin user */ + const [p] = await promisePool.query('SELECT * from permissions'); + for (const perm of p) { + await promisePool.execute(sqlInsertPermissions, [uuidv4(), sid, perm.permission_sid]); + } + /* create admin token for single account */ const [r] = await promisePool.query({sql: sqlQueryAccount, nestTables: true}); if (1 === r.length && r[0].api_keys.api_key_sid === null) { diff --git a/db/seed-integration-test.sql b/db/seed-integration-test.sql index 56b923f..a473987 100644 --- a/db/seed-integration-test.sql +++ b/db/seed-integration-test.sql @@ -1,5 +1,12 @@ SET FOREIGN_KEY_CHECKS=0; +-- create standard permissions +insert into permissions (permission_sid, name, description) +values +('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'), +('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'), +('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users'); + insert into sbc_addresses (sbc_address_sid, ipv4, port) values('f6567ae1-bf97-49af-8931-ca014b689995', '52.55.111.178', 5060); insert into sbc_addresses (sbc_address_sid, ipv4, port) diff --git a/db/seed-production-database-open-source.sql b/db/seed-production-database-open-source.sql index a5302cb..81e2d61 100644 --- a/db/seed-production-database-open-source.sql +++ b/db/seed-production-database-open-source.sql @@ -1,5 +1,12 @@ SET FOREIGN_KEY_CHECKS=0; +-- create standard permissions +insert into permissions (permission_sid, name, description) +values +('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'), +('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'), +('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users'); + -- create one service provider and account insert into api_keys (api_key_sid, token) values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de'); diff --git a/db/seed-production-database.sql b/db/seed-production-database.sql index 6e7a864..b464cf0 100644 --- a/db/seed-production-database.sql +++ b/db/seed-production-database.sql @@ -1,5 +1,12 @@ SET FOREIGN_KEY_CHECKS=0; +-- create standard permissions +insert into permissions (permission_sid, name, description) +values +('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'), +('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'), +('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users'); + -- create one service provider insert into service_providers (service_provider_sid, name, description, root_domain) values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'sip.jambonz.xyz', 'jambonz.xyz service provider', 'sip.jambonz.xyz'); diff --git a/lib/auth/index.js b/lib/auth/index.js index 0ea0a43..d1ba4f5 100644 --- a/lib/auth/index.js +++ b/lib/auth/index.js @@ -35,20 +35,21 @@ function makeStrategy(logger, retrieveKey) { debug(err); logger.info({err}, 'Error checking blacklist for jwt'); } - const {user_sid, account_sid, email, name} = decoded; - //logger.debug({user_sid, account_sid}, 'successfully validated jwt'); - const scope = ['account']; + const {user_sid, service_provider_sid, account_sid, email, name, scope, permissions} = decoded; const user = { + service_provider_sid, account_sid, user_sid, jwt: token, email, name, - hasScope: (s) => s === 'account', - hasAdminAuth: false, - hasServiceProviderAuth: false, - hasAccountAuth: true + permissions, + hasScope: (s) => s === scope, + hasAdminAuth: scope === 'admin', + hasServiceProviderAuth: scope === 'service_provider', + hasAccountAuth: scope === 'account' }; + logger.debug({user}, 'successfully validated jwt'); return done(null, user, {scope}); } }); diff --git a/lib/routes/api/login.js b/lib/routes/api/login.js index 4d61432..35c373f 100644 --- a/lib/routes/api/login.js +++ b/lib/routes/api/login.js @@ -1,12 +1,19 @@ const router = require('express').Router(); -const {getMysqlConnection} = require('../../db'); +const jwt = require('jsonwebtoken'); const {verifyPassword} = require('../../utils/password-utils'); - +const {promisePool} = require('../../db'); +const sysError = require('../error'); +const retrievePemissionsSql = ` +SELECT p.name +FROM permissions p, user_permissions up +WHERE up.permission_sid = p.permission_sid +AND up.user_sid = ? +`; const retrieveSql = 'SELECT * from users where name = ?'; const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL'; -router.post('/', (req, res) => { +router.post('/', async(req, res) => { const logger = req.app.locals.logger; const {username, password} = req.body; if (!username || !password) { @@ -14,48 +21,47 @@ router.post('/', (req, res) => { return res.sendStatus(400); } - getMysqlConnection((err, conn) => { - if (err) { - logger.error({err}, 'Error getting db connection'); + try { + const [r] = await promisePool.query(retrieveSql, username); + if (r.length === 0) { + logger.info(`Failed login attempt for user ${username}`); + return res.sendStatus(403); + } + logger.info({r}, 'successfully retrieved user account'); + const isCorrect = await verifyPassword(r[0].hashed_password, password); + if (!isCorrect) return res.sendStatus(403); + const force_change = !!r[0].force_change; + const [t] = await promisePool.query(tokenSql); + if (t.length === 0) { + logger.error('Database has no admin token provisioned...run reset_admin_password'); return res.sendStatus(500); } - conn.query(retrieveSql, [username], async(err, results) => { - conn.release(); - if (err) { - logger.error({err}, 'Error getting db connection'); - return res.sendStatus(500); + + if (process.env.JAMBONES_AUTH_USE_JWT) { + const [p] = await promisePool.query(retrievePemissionsSql, r[0].user_sid); + const permissions = p.map((x) => x.name); + const obj = {user_sid: r[0].user_sid, scope: 'admin', force_change, permissions}; + if (r[0].service_provider_sid) { + obj.scope = 'service-provider'; + obj.service_provider_sid = r[0].service_provider_sid; } - if (0 === results.length) { - logger.info(`Failed login attempt for user ${username}`); - return res.sendStatus(403); + else if (r[0].account_sid) { + obj.scope = 'account'; + obj.account_sid = r[0].account_sid; } - - logger.info({results}, 'successfully retrieved account'); - const isCorrect = await verifyPassword(results[0].hashed_password, password); - if (!isCorrect) return res.sendStatus(403); - - const force_change = !!results[0].force_change; - - getMysqlConnection((err, conn) => { - if (err) { - logger.error({err}, 'Error getting db connection'); - return res.sendStatus(500); - } - conn.query(tokenSql, (err, tokenResults) => { - conn.release(); - if (err) { - logger.error({err}, 'Error getting db connection'); - return res.sendStatus(500); - } - if (0 === tokenResults.length) { - logger.error('Database has no admin token provisioned...run reset_admin_password'); - return res.sendStatus(500); - } - res.json({user_sid: results[0].user_sid, force_change, token: tokenResults[0].token}); - }); - }); - }); - }); + const token = jwt.sign( + obj, + process.env.JWT_SECRET, + { expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 } + ); + res.json({token}); + } + else { + res.json({user_sid: r[0].user_sid, force_change, token: t[0].token}); + } + } catch (err) { + sysError(logger, res, err); + } }); diff --git a/lib/routes/api/register.js b/lib/routes/api/register.js index 4b98826..58d34c4 100644 --- a/lib/routes/api/register.js +++ b/lib/routes/api/register.js @@ -347,7 +347,7 @@ router.post('/', async(req, res) => { account_sid: userProfile.account_sid, email: userProfile.email, name: userProfile.name - }, process.env.JWT_SECRET, { expiresIn: '1h' }); + }, process.env.JWT_SECRET, { expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 }); logger.debug({ user_sid: userProfile.user_sid, diff --git a/lib/routes/api/signin.js b/lib/routes/api/signin.js index 52b1b4b..578778c 100644 --- a/lib/routes/api/signin.js +++ b/lib/routes/api/signin.js @@ -68,7 +68,7 @@ router.post('/', async(req, res) => { const token = jwt.sign({ user_sid: userProfile.user_sid, account_sid: userProfile.account_sid - }, process.env.JWT_SECRET, { expiresIn: '1h' }); + }, process.env.JWT_SECRET, { expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 }); logger.debug({ user_sid: userProfile.user_sid, diff --git a/test/call-test.js b/test/call-test.js index 8e14d7f..4ba27ea 100644 --- a/test/call-test.js +++ b/test/call-test.js @@ -18,7 +18,9 @@ test('Create Call Success With Synthesizer in Payload', async (t) => { const service_provider_sid = await createServiceProvider(request, 'account_has_synthesizer'); const account_sid = await createAccount(request, service_provider_sid, 'account_has_synthesizer'); const token = jwt.sign({ - account_sid + account_sid, + scope: "account", + permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"] }, process.env.JWT_SECRET, { expiresIn: '1h' }); const authUser = { bearer: token }; const speech_sid = await createGoogleSpeechCredentials(request, account_sid, null, authUser, true, true) @@ -58,7 +60,9 @@ test('Create Call Success Without Synthesizer in Payload', async (t) => { const service_provider_sid = await createServiceProvider(request, 'account2_has_synthesizer'); const account_sid = await createAccount(request, service_provider_sid, 'account2_has_synthesizer'); const token = jwt.sign({ - account_sid + account_sid, + scope: "account", + permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"] }, process.env.JWT_SECRET, { expiresIn: '1h' }); const authUser = { bearer: token }; const speech_sid = await createGoogleSpeechCredentials(request, account_sid, null, authUser, true, true) diff --git a/test/recent-calls.js b/test/recent-calls.js index 0f5076a..a207617 100644 --- a/test/recent-calls.js +++ b/test/recent-calls.js @@ -24,12 +24,16 @@ test('recent calls tests', async(t) => { const account_sid = await createAccount(request, service_provider_sid); const token = jwt.sign({ - account_sid + account_sid, + scope: "account", + permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"] }, process.env.JWT_SECRET, { expiresIn: '1h' }); const authUser = {bearer: token}; const tokenSP = jwt.sign({ - service_provider_sid + service_provider_sid, + scope: "account", + permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"] }, process.env.JWT_SECRET, { expiresIn: '1h' }); const authUserSP = {bearer: token}; diff --git a/test/speech-credentials.js b/test/speech-credentials.js index 0f00f62..7da82fb 100644 --- a/test/speech-credentials.js +++ b/test/speech-credentials.js @@ -50,7 +50,10 @@ test('speech credentials tests', async(t) => { await deleteObjectBySid(request, `/ServiceProviders/${service_provider_sid}/SpeechCredentials`, speech_credential_sid); const token = jwt.sign({ - account_sid + account_sid, + account_sid, + scope: "account", + permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"] }, process.env.JWT_SECRET, { expiresIn: '1h' }); const authUser = {bearer: token};