Compare commits

...

3 Commits

Author SHA1 Message Date
Markus Frindt
97b17d9e1d Improved invalidation of JWT in redis (#139)
* Improved invalidation of JWT in redis

* use jwt as default value in generateRedisKey

* import logger in app.js

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-03-31 16:19:34 -04:00
Dave Horton
57110ede76 fix: Dockerfile to reduce vulnerabilities (#137)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3368756
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3368756
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-5291792
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-5291792

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-03-31 07:57:08 -04:00
Guilherme Rauen
d656857509 extend sid validation to all routes (#138)
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2023-03-31 07:46:33 -04:00
29 changed files with 419 additions and 143 deletions

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3

14
app.js
View File

@@ -1,12 +1,5 @@
const assert = require('assert');
const opts = Object.assign({
timestamp: () => {
return `, "time": "${new Date().toISOString()}"`;
}
}, {
level: process.env.JAMBONES_LOGLEVEL || 'info'
});
const logger = require('pino')(opts);
const logger = require('./lib/logger');
const express = require('express');
const app = express();
const helmet = require('helmet');
@@ -44,10 +37,7 @@ const {
addKey,
retrieveKey,
deleteKey,
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
} = require('./lib/helpers/realtimedb-helpers');
const {
getTtsVoices
} = require('@jambonz/speech-utils')({

View File

@@ -1,41 +1,42 @@
const Strategy = require('passport-http-bearer').Strategy;
const {getMysqlConnection} = require('../db');
const {hashString} = require('../utils/password-utils');
const debug = require('debug')('jambonz:api-server');
const {cacheClient} = require('../helpers');
const jwt = require('jsonwebtoken');
const sql = `
SELECT *
FROM api_keys
WHERE api_keys.token = ?`;
function makeStrategy(logger, retrieveKey) {
function makeStrategy(logger) {
return new Strategy(
async function(token, done) {
//logger.debug(`validating with token ${token}`);
jwt.verify(token, process.env.JWT_SECRET, async(err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
logger.debug('jwt expired');
return done(null, false);
}
/* its not a jwt obtained through login, check api leys */
/* its not a jwt obtained through login, check api keys */
checkApiTokens(logger, token, done);
}
else {
/* validated -- make sure it is not on blacklist */
try {
const s = `jwt:${hashString(token)}`;
const result = await retrieveKey(s);
if (result) {
debug(`result from searching for ${s}: ${result}`);
const {user_sid} = decoded;
/* Valid jwt tokens are stored in redis by hashed user_id */
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
const result = await cacheClient.get(redisKey);
if (result === null) {
debug(`result from searching for ${redisKey}: ${result}`);
logger.info('jwt invalidated after logout');
return done(null, false);
}
} catch (err) {
} catch (error) {
debug(err);
logger.info({err}, 'Error checking blacklist for jwt');
logger.info({err}, 'Error checking redis for jwt');
}
const {user_sid, service_provider_sid, account_sid, email, name, scope, permissions} = decoded;
const { user_sid, service_provider_sid, account_sid, email, name, scope, permissions } = decoded;
const user = {
service_provider_sid,
account_sid,

View File

@@ -0,0 +1,82 @@
const {
addKey: addKeyRedis,
deleteKey: deleteKeyRedis,
retrieveKey: retrieveKeyRedis,
} = require('./realtimedb-helpers');
const { hashString } = require('../utils/password-utils');
const logger = require('../logger');
class CacheClient {
constructor() { }
async set(params) {
const {
redisKey,
value = '1',
time = 3600,
} = params || {};
try {
await addKeyRedis(redisKey, value, time);
} catch (err) {
logger.error('CacheClient.get set', {
error: {
message: err.message,
name: err.name
},
...params
});
}
}
async get(redisKey) {
try {
const result = await retrieveKeyRedis(redisKey);
return result;
} catch (err) {
logger.error('CacheClient.get error', {
error: {
message: err.message,
name: err.name
},
redisKey
});
}
}
async delete(key) {
try {
await deleteKeyRedis(key);
logger.debug('CacheClient.delete key from redis', { key });
} catch (err) {
logger.error('CacheClient.delete error', {
error: {
message: err.message,
name: err.name
},
key
});
}
}
generateRedisKey(type, key, version) {
let suffix = '';
if (version) {
suffix = `:version:${version}`;
}
switch (type) {
case 'reset-link':
return `reset-link:${key}`;
case 'jwt':
default:
return `jwt:${hashString(key)}${suffix}`;
}
}
}
const cacheClient = new CacheClient();
module.exports = { cacheClient };

4
lib/helpers/index.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
...require('./cache-client'),
...require('./realtimedb-helpers'),
};

View File

@@ -0,0 +1,28 @@
const logger = require('../logger');
const {
retrieveCall,
deleteCall,
listCalls,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
client: redisClient,
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
module.exports = {
retrieveCall,
deleteCall,
listCalls,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
redisClient,
};

11
lib/logger.js Normal file
View File

@@ -0,0 +1,11 @@
const opts = Object.assign({
timestamp: () => {
return `, "time": "${new Date().toISOString()}"`;
}
}, {
level: process.env.JAMBONES_LOGLEVEL || 'info'
});
const logger = require('pino')(opts);
module.exports = logger;

View File

@@ -12,7 +12,14 @@ const { v4: uuidv4 } = require('uuid');
const snakeCase = require('../../utils/snake-case');
const sysError = require('../error');
const {promisePool} = require('../../db');
const {hasAccountPermissions, parseAccountSid, enableSubspace, disableSubspace} = require('./utils');
const {
hasAccountPermissions,
parseAccountSid,
parseCallSid,
enableSubspace,
disableSubspace,
parseVoipCarrierSid
} = require('./utils');
const short = require('short-uuid');
const VoipCarrier = require('../../models/voip-carrier');
const translator = short();
@@ -45,10 +52,8 @@ const stripPort = (hostport) => {
return hostport;
};
const validateUpdateForCarrier = async(req) => {
const validateUpdateForCarrier = async(req, account_sid) => {
try {
const account_sid = parseAccountSid(req);
if (req.user.hasScope('admin')) {
return;
}
@@ -106,13 +111,18 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
});
router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateUpdateForCarrier(req);
const rowsAffected = await VoipCarrier.update(req.params.voip_carrier_sid, req.body);
const sid = parseVoipCarrierSid(req);
const account_sid = parseAccountSid(req);
await validateUpdateForCarrier(req, account_sid);
const rowsAffected = await VoipCarrier.update(sid, req.body);
if (rowsAffected === 0) {
return res.sendStatus(404);
}
res.status(204).end();
return res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
@@ -232,6 +242,7 @@ function validateTo(to) {
}
throw new DbErrorBadRequest(`missing or invalid to property: ${JSON.stringify(to)}`);
}
async function validateCreateCall(logger, sid, req) {
const {lookupAppBySid} = req.app.locals;
const obj = req.body;
@@ -351,6 +362,7 @@ async function validateAdd(req) {
throw new DbErrorBadRequest('\'queue_event_hook\' must be an object when adding an account');
}
}
async function validateUpdate(req, sid) {
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
@@ -377,6 +389,7 @@ async function validateUpdate(req, sid) {
}
if (req.body.service_provider_sid) throw new DbErrorBadRequest('service_provider_sid may not be modified');
}
async function validateDelete(req, sid) {
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
@@ -389,7 +402,6 @@ async function validateDelete(req, sid) {
}
}
/* add */
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
@@ -431,8 +443,9 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(req.params.sid, service_provider_sid);
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}
@@ -444,13 +457,14 @@ router.get('/:sid', async(req, res) => {
router.get('/:sid/WebhookSecret', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(req.params.sid, service_provider_sid);
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
let {webhook_secret} = results[0];
if (req.query.regenerate) {
const secret = `wh_secret_${translator.generate()}`;
await Account.update(req.params.sid, {webhook_secret: secret});
await Account.update(account_sid, {webhook_secret: secret});
webhook_secret = secret;
}
return res.status(200).json({webhook_secret});
@@ -463,8 +477,9 @@ router.get('/:sid/WebhookSecret', async(req, res) => {
router.post('/:sid/SubspaceTeleport', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(req.params.sid, service_provider_sid);
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
const {subspace_client_id, subspace_client_secret} = results[0];
const {destination} = req.body;
@@ -477,7 +492,7 @@ router.post('/:sid/SubspaceTeleport', async(req, res) => {
destination: dest
});
logger.info({destination, teleport}, 'SubspaceTeleport - create teleport');
await Account.update(req.params.sid, {
await Account.update(account_sid, {
subspace_sip_teleport_id: teleport.id,
subspace_sip_teleport_destinations: JSON.stringify(teleport.teleport_entry_points)//hacky
});
@@ -495,13 +510,14 @@ router.post('/:sid/SubspaceTeleport', async(req, res) => {
router.delete('/:sid/SubspaceTeleport', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(req.params.sid, service_provider_sid);
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
const {subspace_client_id, subspace_client_secret, subspace_sip_teleport_id} = results[0];
await disableSubspace({subspace_client_id, subspace_client_secret, subspace_sip_teleport_id});
await Account.update(req.params.sid, {
await Account.update(account_sid, {
subspace_sip_teleport_id: null,
subspace_sip_teleport_destinations: null
});
@@ -512,11 +528,13 @@ router.delete('/:sid/SubspaceTeleport', async(req, res) => {
}
});
/* update */
/**
* update
*/
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseAccountSid(req);
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -576,12 +594,13 @@ router.put('/:sid', async(req, res) => {
/* delete */
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
const sqlDeleteGateways = `DELETE from sip_gateways
WHERE voip_carrier_sid IN
(SELECT voip_carrier_sid from voip_carriers where account_sid = ?)`;
try {
const sid = parseAccountSid(req);
await validateDelete(req, sid);
const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid);
@@ -645,13 +664,17 @@ account_subscriptions WHERE account_sid = ?)
}
});
/* retrieve account level api keys */
/**
* retrieve account level api keys
*/
router.get('/:sid/ApiKeys', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ApiKey.retrieveAll(req.params.sid);
const sid = parseAccountSid(req);
const results = await ApiKey.retrieveAll(sid);
res.status(200).json(results);
updateLastUsed(logger, req.params.sid, req).catch((err) => {});
updateLastUsed(logger, sid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -661,36 +684,38 @@ router.get('/:sid/ApiKeys', async(req, res) => {
* create a new Call
*/
router.post('/:sid/Calls', async(req, res) => {
const sid = req.params.sid;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
res.status(480).json({msg: 'no available feature servers at this time'});
} else {
try {
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
}
return res.status(480).json({msg: 'no available feature servers at this time'});
}
try {
const sid = parseAccountSid(req);
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
return res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
}
});
@@ -698,10 +723,10 @@ router.post('/:sid/Calls', async(req, res) => {
* retrieve info for a group of calls under an account
*/
router.get('/:sid/Calls', async(req, res) => {
const accountSid = req.params.sid;
const {logger, listCalls} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
const calls = await listCalls(accountSid);
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
res.status(200).json(coerceNumbers(snakeCase(calls)));
@@ -715,11 +740,11 @@ router.get('/:sid/Calls', async(req, res) => {
* retrieve single call
*/
router.get('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, retrieveCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
const callSid = parseCallSid(req);
const callInfo = await retrieveCall(accountSid, callSid);
if (callInfo) {
logger.debug(callInfo, `retrieved call info for call sid ${callSid}`);
@@ -739,11 +764,11 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
* delete call
*/
router.delete('/:sid/Calls/:callSid', async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, deleteCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
const callSid = parseCallSid(req);
const result = await deleteCall(accountSid, callSid);
if (result) {
logger.debug(`successfully deleted call ${callSid}`);
@@ -763,11 +788,11 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
* update a call
*/
const updateCall = async(req, res) => {
const accountSid = req.params.sid;
const callSid = req.params.callSid;
const {logger, retrieveCall} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
const callSid = parseCallSid(req);
validateUpdateCall(req.body);
const call = await retrieveCall(accountSid, callSid);
if (call) {
@@ -794,6 +819,7 @@ const updateCall = async(req, res) => {
router.post('/:sid/Calls/:callSid', async(req, res) => {
await updateCall(req, res);
});
router.put('/:sid/Calls/:callSid', async(req, res) => {
await updateCall(req, res);
});

View File

@@ -7,6 +7,7 @@ const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const { validate } = require('@jambonz/verb-specifications');
const { parseApplicationSid } = require('./utils');
const preconditions = {
'add': validateAdd,
'update': validateUpdate
@@ -111,9 +112,10 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const application_sid = parseApplicationSid(req);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await Application.retrieve(req.params.sid, service_provider_sid, account_sid);
const results = await Application.retrieve(application_sid, service_provider_sid, account_sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}
@@ -124,9 +126,9 @@ router.get('/:sid', async(req, res) => {
/* delete */
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseApplicationSid(req);
await validateDelete(req, sid);
const [application] = await promisePool.query('SELECT * FROM applications WHERE application_sid = ?', sid);
@@ -165,9 +167,9 @@ router.delete('/:sid', async(req, res) => {
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseApplicationSid(req);
await validateUpdate(req, sid);
// create webhooks if provided

View File

@@ -1,15 +1,15 @@
const router = require('express').Router();
//const debug = require('debug')('jambonz:api-server');
const {DbErrorBadRequest} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const sqlUpdatePassword = `UPDATE users
SET hashed_password= ?
WHERE user_sid = ?`;
router.post('/', async(req, res) => {
const {logger, retrieveKey, deleteKey} = req.app.locals;
const {logger} = req.app.locals;
const {user_sid} = req.user;
const {old_password, new_password} = req.body;
try {
@@ -26,10 +26,10 @@ router.post('/', async(req, res) => {
const isCorrect = await verifyPassword(r[0].hashed_password, old_password);
if (!isCorrect) {
const key = `reset-link:${old_password}`;
const user_sid = await retrieveKey(key);
const key = cacheClient.generateRedisKey('reset-link', old_password);
const user_sid = await cacheClient.get(key);
if (!user_sid) throw new DbErrorBadRequest('old_password is incorrect');
await deleteKey(key);
await cacheClient.delete(key);
}
}
@@ -42,6 +42,9 @@ router.post('/', async(req, res) => {
sysError(logger, res, err);
return;
}
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
});
module.exports = router;

View File

@@ -4,6 +4,7 @@ const short = require('short-uuid');
const translator = short();
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const sql = `SELECT * from users user
LEFT JOIN accounts AS acc
@@ -45,6 +46,7 @@ function createResetEmailText(link) {
router.post('/', async(req, res) => {
const {logger, addKey} = req.app.locals;
const {email} = req.body;
const {user_sid} = req.user;
let obj;
try {
if (!email || !validateEmail(email)) {
@@ -73,10 +75,14 @@ router.post('/', async(req, res) => {
else {
/* generate a link for this user to reset, send email */
const link = translator.generate();
addKey(`reset-link:${link}`, obj.user.user_sid, 3600)
const redisKey = cacheClient.generateRedisKey('reset-link', link);
addKey(redisKey, obj.user.user_sid, 3600)
.catch((err) => logger.error({err}, 'Error adding reset link to redis'));
emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link));
}
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
});
module.exports = router;

View File

@@ -22,6 +22,7 @@ DELETE FROM account_limits
WHERE account_sid = ?
AND category = ?
`;
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const {

View File

@@ -2,6 +2,7 @@ const router = require('express').Router();
const jwt = require('jsonwebtoken');
const {verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const Account = require('../../models/account');
const ServiceProvider = require('../../models/service-provider');
const sysError = require('../error');
@@ -71,12 +72,22 @@ router.post('/', async(req, res) => {
}),
user_sid: obj.user_sid
};
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
const token = jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 }
{ expiresIn }
);
res.json({token, ...obj});
/* Store jwt based on user_id after successful login */
await cacheClient.set({
redisKey: cacheClient.generateRedisKey('jwt', obj.user_sid, 'v2'),
value: token,
time: expiresIn,
});
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -1,19 +1,18 @@
const router = require('express').Router();
const debug = require('debug')('jambonz:api-server');
const {hashString} = require('../../utils/password-utils');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
router.post('/', async(req, res) => {
const {logger, addKey} = req.app.locals;
const {jwt} = req.user;
const {logger} = req.app.locals;
const {user_sid} = req.user;
debug(`adding jwt to blacklist: ${jwt}`);
debug(`logout user and invalidate jwt token for user: ${user_sid}`);
try {
/* add key to blacklist */
const s = `jwt:${hashString(jwt)}`;
const result = await addKey(s, '1', 3600);
debug(`result from adding ${s}: ${result}`);
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);

View File

@@ -10,6 +10,7 @@ const preconditions = {
'update': validateUpdate
};
const sysError = require('../error');
const { parsePhoneNumberSid } = require('./utils');
/* check for required fields when adding */
@@ -85,8 +86,9 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parsePhoneNumberSid(req);
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await PhoneNumber.retrieve(req.params.sid, account_sid);
const results = await PhoneNumber.retrieve(sid, account_sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}

View File

@@ -4,6 +4,7 @@ const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/er
const {promisePool} = require('../../db');
const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils');
const {validateEmail} = require('../../utils/email-utils');
const {cacheClient} = require('../../helpers');
const { v4: uuid } = require('uuid');
const short = require('short-uuid');
const translator = short();
@@ -338,16 +339,19 @@ router.post('/', async(req, res) => {
/* deactivate the old/replaced user */
const [r] = await promisePool.execute('DELETE FROM users WHERE user_sid = ?', [user_sid]);
logger.debug({r}, 'register - removed old user');
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
}
}
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 ;
// generate a json web token for this user
const token = jwt.sign({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid,
email: userProfile.email,
name: userProfile.name
}, process.env.JWT_SECRET, { expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 });
}, process.env.JWT_SECRET, { expiresIn });
logger.debug({
user_sid: userProfile.user_sid,
@@ -356,6 +360,13 @@ router.post('/', async(req, res) => {
res.json({jwt: token, ...userProfile});
/* Store jwt based on user_id after successful login */
await cacheClient.set({
redisKey: cacheClient.generateRedisKey('jwt', userProfile.user_sid, 'v2'),
value: token,
time: expiresIn,
});
} catch (err) {
debug(err, 'Error');
sysError(logger, res, err);

View File

@@ -8,7 +8,7 @@ const VoipCarrier = require('../../models/voip-carrier');
const Application = require('../../models/application');
const PhoneNumber = require('../../models/phone-number');
const ApiKey = require('../../models/api-key');
const {hasServiceProviderPermissions, parseServiceProviderSid} = require('./utils');
const {hasServiceProviderPermissions, parseServiceProviderSid, parseVoipCarrierSid} = require('./utils');
const sysError = require('../error');
const decorate = require('./decorate');
const preconditions = {
@@ -73,12 +73,11 @@ function validateUpdate(req) {
throw new DbErrorForbidden('insufficient permissions to update service provider');
} catch (error) {
console.log('Passing forward the error received');
throw error;
}
}
/* can not delete a service provider if it has any active accounts */
/* can not delete a service provider if it has any active accounts or users*/
async function noActiveAccountsOrUsers(req, sid) {
if (!req.user.hasAdminAuth) {
throw new DbErrorForbidden('only admin users can delete a service provider');
@@ -99,6 +98,7 @@ async function noActiveAccountsOrUsers(req, sid) {
await promisePool.query(sqlDeleteSipGateways, [sid]);
await promisePool.query(sqlDeleteSmppGateways, [sid]);
await promisePool.query('DELETE from voip_carriers WHERE service_provider_sid = ?', [sid]);
await promisePool.query('DELETE from api_keys WHERE service_provider_sid = ?', [sid]);
}
decorate(router, ServiceProvider, ['delete'], preconditions);
@@ -181,7 +181,8 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
validateUpdate(req);
const rowsAffected = await VoipCarrier.update(req.params.voip_carrier_sid, req.body);
const sid = parseVoipCarrierSid(req);
const rowsAffected = await VoipCarrier.update(sid, req.body);
if (rowsAffected === 0) {
return res.sendStatus(404);
}
@@ -249,7 +250,8 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await ServiceProvider.retrieve(req.params.sid);
const sid = parseServiceProviderSid(req);
const results = await ServiceProvider.retrieve(sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}
@@ -263,7 +265,7 @@ router.put('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
validateUpdate(req);
const sid = req.params.sid;
const sid = parseServiceProviderSid(req);
// create webhooks if provided
const obj = Object.assign({}, req.body);

View File

@@ -3,6 +3,7 @@ const router = require('express').Router();
const {DbErrorBadRequest} = require('../../utils/errors');
const {promisePool} = require('../../db');
const {verifyPassword} = require('../../utils/password-utils');
const {cacheClient} = require('../../helpers');
const jwt = require('jsonwebtoken');
const sysError = require('../error');
@@ -64,11 +65,12 @@ router.post('/', async(req, res) => {
pristine: false
});
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
// generate a json web token for this session
const token = jwt.sign({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid
}, process.env.JWT_SECRET, { expiresIn: parseInt(process.env.JWT_EXPIRES_IN || 60) * 60 });
}, process.env.JWT_SECRET, { expiresIn });
logger.debug({
user_sid: userProfile.user_sid,
@@ -76,6 +78,14 @@ router.post('/', async(req, res) => {
}, 'generated jwt');
res.json({jwt: token, ...userProfile});
/* Store jwt based on user_id after successful login */
await cacheClient.set({
redisKey: cacheClient.generateRedisKey('jwt', userProfile.user_sid, 'v2'),
value: token,
time: expiresIn,
});
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -4,7 +4,7 @@ const Account = require('../../models/account');
const SpeechCredential = require('../../models/speech-credential');
const sysError = require('../error');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
const {parseAccountSid, parseServiceProviderSid} = require('./utils');
const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = require('./utils');
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
const {
testGoogleTts,
@@ -261,9 +261,9 @@ router.get('/', async(req, res) => {
* retrieve a specific speech credential
*/
router.get('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const cred = await SpeechCredential.retrieve(sid);
if (0 === cred.length) return res.sendStatus(404);
const {credential, ...obj} = cred[0];
@@ -337,9 +337,9 @@ router.get('/:sid', async(req, res) => {
* delete a speech credential
*/
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const count = await SpeechCredential.remove(sid);
if (0 === count) return res.sendStatus(404);
res.sendStatus(204);
@@ -353,9 +353,9 @@ router.delete('/:sid', async(req, res) => {
* update a speech credential -- we only allow use_for_tts and use_for_stt to be updated
*/
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const {use_for_tts, use_for_stt, region, aws_region, stt_region, tts_region,
riva_server_uri, nuance_tts_uri, nuance_stt_uri} = req.body;
if (typeof use_for_tts === 'undefined' && typeof use_for_stt === 'undefined') {
@@ -424,9 +424,9 @@ router.put('/:sid', async(req, res) => {
* Test a credential
*/
router.get('/:sid/test', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const creds = await SpeechCredential.retrieve(sid);
if (!creds || 0 === creds.length) return res.sendStatus(404);

View File

@@ -1,11 +1,11 @@
const router = require('express').Router();
const User = require('../../models/user');
const request = require('request');
const {DbErrorBadRequest} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const {validatePasswordSettings} = require('./utils');
const {decrypt} = require('../../utils/encrypt-decrypt');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const retrieveMyDetails = `SELECT *
FROM users user
@@ -358,6 +358,8 @@ router.put('/:user_sid', async(req, res) => {
'UPDATE users SET account_sid = ? WHERE user_sid = ?',
[account_sid, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
}
if (service_provider_sid || service_provider_sid === null) {
@@ -365,6 +367,8 @@ router.put('/:user_sid', async(req, res) => {
'UPDATE users SET service_provider_sid = ? WHERE user_sid = ?',
[service_provider_sid, user_sid]);
if (0 === r.changedRows) throw new Error('database update failed');
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
}
if (email) {
@@ -459,19 +463,10 @@ router.delete('/:user_sid', async(req, res) => {
(req.user.hasAccountAuth && req.user.account_sid === user[0].account_sid) ||
(req.user.hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid)) {
await User.remove(user_sid);
//logout user after self-delete
if (req.user.user_sid === user_sid) {
request({
url:'http://localhost:3000/v1/logout',
method: 'POST',
}, (err) => {
if (err) {
logger.error(err, 'could not log out user');
return res.sendStatus(500);
}
logger.debug({user}, 'user deleted and logged out');
});
}
/* invalidate the jwt of the deleted user */
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
return res.sendStatus(204);
} else {
throw new DbErrorBadRequest('invalid request');

View File

@@ -138,29 +138,83 @@ const createTestAlerts = async(writeAlerts, AlertType, account_sid) => {
};
const parseServiceProviderSid = (req) => {
const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl);
const validateSid = (model, req) => {
const arr = new RegExp(`${model}\/([^\/]*)`).exec(req.originalUrl);
if (arr) {
const sid = arr[1];
const sid_validation = validate(sid);
if (!sid_validation) {
throw new BadRequestError('invalid service_provider_sid format');
throw new BadRequestError(`invalid ${model}Sid format`);
}
return arr[1];
}
return;
};
const parseServiceProviderSid = (req) => {
try {
return validateSid('ServiceProviders', req);
} catch (error) {
throw error;
}
};
const parseAccountSid = (req) => {
const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl);
if (arr) {
const sid = arr[1];
const sid_validation = validate(sid);
if (!sid_validation) {
throw new BadRequestError('invalid account_sid format');
}
try {
return validateSid('Accounts', req);
} catch (error) {
throw error;
}
};
return arr[1];
const parseApplicationSid = (req) => {
try {
return validateSid('Applications', req);
} catch (error) {
throw error;
}
};
const parseCallSid = (req) => {
try {
return validateSid('Calls', req);
} catch (error) {
throw error;
}
};
const parsePhoneNumberSid = (req) => {
try {
return validateSid('PhoneNumbers', req);
} catch (error) {
throw error;
}
};
const parseSpeechCredentialSid = (req) => {
try {
return validateSid('SpeechCredentials', req);
} catch (error) {
throw error;
}
};
const parseVoipCarrierSid = (req) => {
try {
return validateSid('VoipCarriers', req);
} catch (error) {
throw error;
}
};
const parseWebhookSid = (req) => {
try {
return validateSid('Webhooks', req);
} catch (error) {
throw error;
}
};
@@ -344,7 +398,13 @@ module.exports = {
createTestCdrs,
createTestAlerts,
parseAccountSid,
parseApplicationSid,
parseCallSid,
parsePhoneNumberSid,
parseServiceProviderSid,
parseSpeechCredentialSid,
parseVoipCarrierSid,
parseWebhookSid,
hasAccountPermissions,
hasServiceProviderPermissions,
checkLimits,

View File

@@ -4,6 +4,7 @@ const VoipCarrier = require('../../models/voip-carrier');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const { parseVoipCarrierSid } = require('./utils');
const validate = async(req) => {
const {lookupAppBySid, lookupAccountBySid} = req.app.locals;
@@ -84,8 +85,9 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseVoipCarrierSid(req);
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await VoipCarrier.retrieve(req.params.sid, account_sid);
const results = await VoipCarrier.retrieve(sid, account_sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}

View File

@@ -2,6 +2,7 @@ const router = require('express').Router();
const Webhook = require('../../models/webhook');
const decorate = require('./decorate');
const sysError = require('../error');
const { parseWebhookSid } = require('./utils');
decorate(router, Webhook, ['add']);
@@ -9,7 +10,8 @@ decorate(router, Webhook, ['add']);
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await Webhook.retrieve(req.params.sid);
const sid = parseWebhookSid(req);
const results = await Webhook.retrieve(sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
}

View File

@@ -10,6 +10,7 @@
"upgrade-db": "node ./db/upgrade-jambonz-db.js",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib",
"jslint:fix": "eslint app.js lib --fix",
"prepare": "husky install"
},
"author": "Dave Horton",

View File

@@ -231,7 +231,7 @@ test('account tests', async(t) => {
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if sid param is not a valid uuid');
t.ok(err.statusCode === 400, 'returns 400 bad request if account sid param is not a valid uuid');
}
/* query all limits for an account */

View File

@@ -61,6 +61,16 @@ test('phone number tests', async(t) => {
});
t.ok(result.number === '16173333456' , 'successfully retrieved phone number by sid');
/* fail to query one phone number with invalid uuid */
try {
result = await request.get(`/PhoneNumbers/foobar`, {
auth: authAdmin,
json: true,
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if phone number sid param is not a valid uuid');
}
/* delete phone number */
result = await request.delete(`/PhoneNumbers/${sid}`, {
auth: authAdmin,

View File

@@ -118,7 +118,7 @@ test('service provider tests', async(t) => {
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if sid param is not a valid uuid');
t.ok(err.statusCode === 400, 'returns 400 bad request if service provider sid param is not a valid uuid');
}
/* add an api key for a service provider */

View File

@@ -37,7 +37,7 @@ test('speech credentials tests', async(t) => {
}
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if sid param is not a valid uuid');
t.ok(err.statusCode === 400, 'returns 400 bad request if service provider sid param is not a valid uuid');
}
/* add a speech credential to a service provider */
@@ -119,12 +119,20 @@ test('speech credentials tests', async(t) => {
t.ok(result[0].vendor === 'google' && result.length === 1, 'successfully retrieved all speech credentials');
/* return 404 when deleting unknown credentials */
/* return 400 when deleting credentials with invalid uuid */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/foobarbaz`, {
auth: authUser,
resolveWithFullResponse: true,
simple: false
});
t.ok(result.statusCode === 400, 'return 400 when attempting to delete credential with invalid uuid');
/* return 404 when deleting unknown credentials - randomSid: bed7ae17-f8b4-4b74-9e5b-4f6318aae9c9 */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/`, {
auth: authUser,
resolveWithFullResponse: true,
simple: false
});
t.ok(result.statusCode === 404, 'return 404 when attempting to delete unknown credential');
/* delete the credential */

View File

@@ -43,6 +43,15 @@ test('voip carrier tests', async(t) => {
});
t.ok(result.name === 'daveh' , 'successfully retrieved voip carrier by sid');
/* fail to query one voip carriers with invalid uuid */
try {
result = await request.get(`/VoipCarriers/123`, {
auth: authAdmin,
json: true,
});
} catch (err) {
t.ok(err.statusCode === 400, 'returns 400 bad request if voip carrier sid param is not a valid uuid');
}
/* update voip carriers */
result = await request.put(`/VoipCarriers/${sid}`, {