eliminate parsing of jwt to support either jwt or api key (#124)

* eliminate parsing of jwt to support either jwt or api key

* fixes for preventing non-authorized changes to users

* update to AWS v3 api
This commit is contained in:
Dave Horton
2023-03-14 18:54:56 -04:00
committed by GitHub
parent aea7388ba0
commit c4cc6c51ee
6 changed files with 203 additions and 75 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ const checkApiTokens = (logger, token, done) => {
hasServiceProviderAuth: scope === 'service_provider',
hasAccountAuth: scope === 'account'
};
logger.info(user, `successfully validated with scope ${scope}`);
logger.debug({user}, `successfully validated with scope ${scope}`);
return done(null, user, {scope});
});
});
+59 -52
View File
@@ -1,6 +1,5 @@
const router = require('express').Router();
const User = require('../../models/user');
const jwt = require('jsonwebtoken');
const request = require('request');
const {DbErrorBadRequest} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
@@ -28,7 +27,8 @@ AND account_subscriptions.pending=0`;
const updateSql = 'UPDATE users set hashed_password = ?, force_change = false WHERE user_sid = ?';
const retrieveStaticIps = 'SELECT * FROM account_static_ips WHERE account_sid = ?';
const validateRequest = async(user_sid, payload) => {
const validateRequest = async(user_sid, req) => {
const payload = req.body;
const {
old_password,
new_password,
@@ -37,19 +37,43 @@ const validateRequest = async(user_sid, payload) => {
email,
email_activation_code,
force_change,
is_active} = payload;
if ('account_sid' in payload) {
throw new DbErrorBadRequest('user may not be moved to a different account');
}
if ('service_provider_sid' in payload) {
throw new DbErrorBadRequest('user may not be moved to a different service provider');
}
is_active
} = payload;
const [r] = await promisePool.query(retrieveSql, user_sid);
if (r.length === 0) return null;
if (r.length === 0) {
throw new DbErrorBadRequest('Invalid request: user_sid does not exist');
}
const user = r[0];
/* it is not allowed for anyone to promote a user to a higher level of authority */
if (null === payload.account_sid || null === payload.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted');
}
if (req.user.hasAccountAuth) {
/* account user may not change modify account_sid or service_provider_sid */
if ('account_sid' in payload && payload.account_sid !== user.account_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another account');
}
if ('service_provider_sid' in payload && payload.service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another service provider');
}
}
if (req.user.hasServiceProviderAuth) {
if ('service_provider_sid' in payload && payload.service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be promoted or moved to another service provider');
}
}
if ('account_sid' in payload) {
const [r] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', payload.account_sid);
if (r.length === 0) throw new DbErrorBadRequest('Invalid request: account_sid does not exist');
const {service_provider_sid} = r[0];
if (service_provider_sid !== user.service_provider_sid) {
throw new DbErrorBadRequest('Invalid request: user may not be moved to another service provider');
}
}
if ((old_password && !new_password) || (new_password && !old_password)) {
throw new DbErrorBadRequest('new_password and old_password both required');
}
@@ -69,23 +93,18 @@ const validateRequest = async(user_sid, payload) => {
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
let usersList;
try {
let results;
if (decodedJwt.scope === 'admin') {
if (req.user.hasAdminAuth) {
results = await User.retrieveAll();
}
else if (decodedJwt.scope === 'account') {
results = await User.retrieveAllForAccount(decodedJwt.account_sid, true);
else if (req.user.hasAccountAuth) {
results = await User.retrieveAllForAccount(req.user.account_sid, true);
}
else if (decodedJwt.scope === 'service_provider') {
results = await User.retrieveAllForServiceProvider(decodedJwt.service_provider_sid, true);
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
else if (req.user.hasServiceProviderAuth) {
results = await User.retrieveAllForServiceProvider(req.user.service_provider_sid, true);
}
if (results.length === 0) throw new Error('failure retrieving users list');
@@ -229,8 +248,6 @@ router.get('/me', async(req, res) => {
router.get('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {user_sid} = req.params;
try {
@@ -239,9 +256,9 @@ router.get('/:user_sid', async(req, res) => {
const {hashed_password, ...rest} = user;
if (!user) throw new Error('failure retrieving user');
if (decodedJwt.scope === 'admin' ||
decodedJwt.scope === 'account' && decodedJwt.account_sid === user.account_sid ||
decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user.service_provider_sid) {
if (req.user.hasAdminAuth ||
req.user.hasAccountAuth && req.user.account_sid === user.account_sid ||
req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
res.status(200).json(rest);
} else {
res.sendStatus(403);
@@ -256,8 +273,7 @@ router.put('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const user = await User.retrieve(user_sid);
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const {hasAccountAuth, hasServiceProviderAuth, hasAdminAuth} = req.user;
const {
old_password,
new_password,
@@ -273,15 +289,15 @@ router.put('/:user_sid', async(req, res) => {
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
if (decodedJwt.scope !== 'admin' &&
!(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) &&
!(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid) &&
if (!hasAdminAuth &&
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}
try {
const user = await validateRequest(user_sid, req.body);
const user = await validateRequest(user_sid, req);
if (!user) return res.sendStatus(404);
if (new_password) {
@@ -374,8 +390,6 @@ router.post('/', async(req, res) => {
hashed_password: passwordHash,
};
const allUsers = await User.retrieveAll();
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
delete payload.initial_password;
try {
@@ -392,30 +406,27 @@ router.post('/', async(req, res) => {
return res.status(422).json({msg: 'user with this email already exists'});
}
if (decodedJwt.scope === 'admin') {
if (req.user.hasAdminAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make(payload);
res.status(201).json({user_sid: uuid});
}
else if (decodedJwt.scope === 'account') {
else if (req.user.hasAccountAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
account_sid: decodedJwt.account_sid,
account_sid: req.user.account_sid,
});
res.status(201).json({user_sid: uuid});
}
else if (decodedJwt.scope === 'service_provider') {
else if (req.user.hasServiceProviderAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make({
...payload,
service_provider_sid: decodedJwt.service_provider_sid,
service_provider_sid: req.user.service_provider_sid,
});
res.status(201).json({user_sid: uuid});
}
else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
}
} catch (err) {
sysError(logger, res, err);
}
@@ -424,24 +435,21 @@ router.post('/', async(req, res) => {
router.delete('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const token = req.user.jwt;
const decodedJwt = jwt.verify(token, process.env.JWT_SECRET);
const allUsers = await User.retrieveAll();
const activeAdminUsers = allUsers.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
const user = await User.retrieve(user_sid);
try {
if (decodedJwt.scope === 'admin' && !user.account_sid && !user.service_provider_sid &&
activeAdminUsers.length === 1) {
if (req.user.hasAdminAuth && activeAdminUsers.length === 1) {
throw new Error('cannot delete this admin user - there are no other active admin users');
}
if (decodedJwt.scope === 'admin' ||
(decodedJwt.scope === 'account' && decodedJwt.account_sid === user[0].account_sid) ||
(decodedJwt.scope === 'service_provider' && decodedJwt.service_provider_sid === user[0].service_provider_sid)) {
if (req.user.hasAdminAuth ||
(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 (decodedJwt.user_sid === user_sid) {
if (req.user.user_sid === user_sid) {
request({
url:'http://localhost:3000/v1/logout',
method: 'POST',
@@ -455,12 +463,11 @@ router.delete('/:user_sid', async(req, res) => {
}
return res.sendStatus(204);
} else {
throw new DbErrorBadRequest(`invalid scope: ${decodedJwt.scope}`);
throw new DbErrorBadRequest('invalid request');
}
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;
+22 -19
View File
@@ -1,7 +1,7 @@
const ttsGoogle = require('@google-cloud/text-to-speech');
const sttGoogle = require('@google-cloud/speech').v1p1beta1;
const Polly = require('aws-sdk/clients/polly');
const AWS = require('aws-sdk');
const { PollyClient, DescribeVoicesCommand } = require('@aws-sdk/client-polly');
const { TranscribeClient, ListVocabulariesCommand } = require('@aws-sdk/client-transcribe');
const { Deepgram } = require('@deepgram/sdk');
const sdk = require('microsoft-cognitiveservices-speech-sdk');
const { SpeechClient } = require('@soniox/soniox-node');
@@ -120,25 +120,28 @@ const testMicrosoftStt = async(logger, credentials) => {
});
};
const testAwsTts = (logger, credentials) => {
const polly = new Polly(credentials);
return new Promise((resolve, reject) => {
polly.describeVoices({LanguageCode: 'en-US'}, (err, data) => {
if (err) return reject(err);
resolve();
});
});
const testAwsTts = async(logger, credentials) => {
try {
const client = new PollyClient(credentials);
const command = new DescribeVoicesCommand({LanguageCode: 'en-US'});
const response = await client.send(command);
return response;
} catch (err) {
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
throw err;
}
};
const testAwsStt = (logger, credentials) => {
const transcribeservice = new AWS.TranscribeService(credentials);
return new Promise((resolve, reject) => {
transcribeservice.listVocabularies((err, data) => {
if (err) return reject(err);
logger.info({data}, 'retrieved language models');
resolve();
});
});
const testAwsStt = async(logger, credentials) => {
try {
const client = new TranscribeClient(credentials);
const command = new ListVocabulariesCommand({});
const response = await client.send(command);
return response;
} catch (err) {
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
throw err;
}
};
const testMicrosoftTts = async(logger, credentials) => {
+89 -1
View File
@@ -9,6 +9,8 @@
"version": "v0.8.1",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-polly": "^3.290.0",
"@aws-sdk/client-transcribe": "^3.290.0",
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@google-cloud/text-to-speech": "^4.0.3",
@@ -19,7 +21,6 @@
"@jambonz/verb-specifications": "^0.0.3",
"@soniox/soniox-node": "^1.1.0",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1302.0",
"bent": "^7.3.12",
"cors": "^2.8.5",
"debug": "^4.3.4",
@@ -316,6 +317,51 @@
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/client-transcribe": {
"version": "3.290.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-transcribe/-/client-transcribe-3.290.0.tgz",
"integrity": "sha512-L7Nahq8zLqAZ+aYV7QRx6/LRX+Py2/cWvIauT9MeWyjEvy17rSUqth7VQfpvFefDQIcRssVXqCN33z+SIKWksQ==",
"dependencies": {
"@aws-crypto/sha256-browser": "3.0.0",
"@aws-crypto/sha256-js": "3.0.0",
"@aws-sdk/client-sts": "3.290.0",
"@aws-sdk/config-resolver": "3.290.0",
"@aws-sdk/credential-provider-node": "3.290.0",
"@aws-sdk/fetch-http-handler": "3.290.0",
"@aws-sdk/hash-node": "3.290.0",
"@aws-sdk/invalid-dependency": "3.290.0",
"@aws-sdk/middleware-content-length": "3.290.0",
"@aws-sdk/middleware-endpoint": "3.290.0",
"@aws-sdk/middleware-host-header": "3.290.0",
"@aws-sdk/middleware-logger": "3.290.0",
"@aws-sdk/middleware-recursion-detection": "3.290.0",
"@aws-sdk/middleware-retry": "3.290.0",
"@aws-sdk/middleware-serde": "3.290.0",
"@aws-sdk/middleware-signing": "3.290.0",
"@aws-sdk/middleware-stack": "3.290.0",
"@aws-sdk/middleware-user-agent": "3.290.0",
"@aws-sdk/node-config-provider": "3.290.0",
"@aws-sdk/node-http-handler": "3.290.0",
"@aws-sdk/protocol-http": "3.290.0",
"@aws-sdk/smithy-client": "3.290.0",
"@aws-sdk/types": "3.290.0",
"@aws-sdk/url-parser": "3.290.0",
"@aws-sdk/util-base64": "3.208.0",
"@aws-sdk/util-body-length-browser": "3.188.0",
"@aws-sdk/util-body-length-node": "3.208.0",
"@aws-sdk/util-defaults-mode-browser": "3.290.0",
"@aws-sdk/util-defaults-mode-node": "3.290.0",
"@aws-sdk/util-endpoints": "3.290.0",
"@aws-sdk/util-retry": "3.290.0",
"@aws-sdk/util-user-agent-browser": "3.290.0",
"@aws-sdk/util-user-agent-node": "3.290.0",
"@aws-sdk/util-utf8": "3.254.0",
"tslib": "^2.3.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/config-resolver": {
"version": "3.290.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.290.0.tgz",
@@ -8948,6 +8994,48 @@
"tslib": "^2.3.1"
}
},
"@aws-sdk/client-transcribe": {
"version": "3.290.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-transcribe/-/client-transcribe-3.290.0.tgz",
"integrity": "sha512-L7Nahq8zLqAZ+aYV7QRx6/LRX+Py2/cWvIauT9MeWyjEvy17rSUqth7VQfpvFefDQIcRssVXqCN33z+SIKWksQ==",
"requires": {
"@aws-crypto/sha256-browser": "3.0.0",
"@aws-crypto/sha256-js": "3.0.0",
"@aws-sdk/client-sts": "3.290.0",
"@aws-sdk/config-resolver": "3.290.0",
"@aws-sdk/credential-provider-node": "3.290.0",
"@aws-sdk/fetch-http-handler": "3.290.0",
"@aws-sdk/hash-node": "3.290.0",
"@aws-sdk/invalid-dependency": "3.290.0",
"@aws-sdk/middleware-content-length": "3.290.0",
"@aws-sdk/middleware-endpoint": "3.290.0",
"@aws-sdk/middleware-host-header": "3.290.0",
"@aws-sdk/middleware-logger": "3.290.0",
"@aws-sdk/middleware-recursion-detection": "3.290.0",
"@aws-sdk/middleware-retry": "3.290.0",
"@aws-sdk/middleware-serde": "3.290.0",
"@aws-sdk/middleware-signing": "3.290.0",
"@aws-sdk/middleware-stack": "3.290.0",
"@aws-sdk/middleware-user-agent": "3.290.0",
"@aws-sdk/node-config-provider": "3.290.0",
"@aws-sdk/node-http-handler": "3.290.0",
"@aws-sdk/protocol-http": "3.290.0",
"@aws-sdk/smithy-client": "3.290.0",
"@aws-sdk/types": "3.290.0",
"@aws-sdk/url-parser": "3.290.0",
"@aws-sdk/util-base64": "3.208.0",
"@aws-sdk/util-body-length-browser": "3.188.0",
"@aws-sdk/util-body-length-node": "3.208.0",
"@aws-sdk/util-defaults-mode-browser": "3.290.0",
"@aws-sdk/util-defaults-mode-node": "3.290.0",
"@aws-sdk/util-endpoints": "3.290.0",
"@aws-sdk/util-retry": "3.290.0",
"@aws-sdk/util-user-agent-browser": "3.290.0",
"@aws-sdk/util-user-agent-node": "3.290.0",
"@aws-sdk/util-utf8": "3.254.0",
"tslib": "^2.3.1"
}
},
"@aws-sdk/config-resolver": {
"version": "3.290.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.290.0.tgz",
+2 -1
View File
@@ -18,6 +18,8 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@aws-sdk/client-polly": "^3.290.0",
"@aws-sdk/client-transcribe": "^3.290.0",
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@google-cloud/text-to-speech": "^4.0.3",
@@ -28,7 +30,6 @@
"@jambonz/verb-specifications": "^0.0.3",
"@soniox/soniox-node": "^1.1.0",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1302.0",
"bent": "^7.3.12",
"cors": "^2.8.5",
"debug": "^4.3.4",
+30 -1
View File
@@ -171,6 +171,35 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for microsoft stt');
}
/* add / test a credential for AWS */
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY && process.env.AWS_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'aws',
use_for_tts: true,
use_for_stt: true,
access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
aws_region: process.env.AWS_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for AWS');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for AWS tts');
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for AWS stt');
}
/* add a credential for wellsaid */
if (process.env.WELLSAID_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
@@ -224,7 +253,7 @@ test('speech credentials tests', async(t) => {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
//console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for deepgram');
/* delete the credential */