Skip to content

Commit

Permalink
remove External API configuration for sending the user info encrypted…
Browse files Browse the repository at this point in the history
… (still sends user info).

add a few new user info headers.

Signed-off-by: Jason Sherman <[email protected]>
  • Loading branch information
usingtechnology committed Jun 20, 2024
1 parent d1c2b02 commit ed67b63
Show file tree
Hide file tree
Showing 10 changed files with 65 additions and 185 deletions.
85 changes: 0 additions & 85 deletions app/frontend/src/components/forms/manage/ExternalAPIs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ export default {
userTokenHeader: null,
userTokenBearer: false,
sendUserInfo: false,
userInfoHeader: null,
userInfoEncrypted: false,
userInfoEncryptionKey: null,
},
show: false,
},
Expand Down Expand Up @@ -78,25 +75,13 @@ export default {
return true;
},
]),
userInfoHeaderRules: ref([
(v) => {
if (
this.editDialog.item.sendUserInfo &&
this.editDialog.item.userInfoEncrypted
) {
return !!v || this.$t('trans.externalAPI.userInfoFieldRequired');
}
return true;
},
]),
};
},
computed: {
...mapState(useFormStore, ['isRTL', 'lang']),
...mapWritableState(useFormStore, ['form']),
},
async mounted() {
await this.getExternalAPIAlgorithmList();
await this.getExternalAPIStatusCodes();
await this.fetchExternalAPIs();
},
Expand Down Expand Up @@ -125,19 +110,6 @@ export default {
this.loading = false;
}
},
async getExternalAPIAlgorithmList() {
try {
const result = await formService.externalAPIAlgorithmList(this.form.id);
this.externalAPIAlgorithmList = result.data;
} catch (e) {
this.addNotification({
text: i18n.t('trans.externalAPI.fetchAlgoListError'),
consoleError: i18n.t('trans.externalAPI.fetchAlgoListError', {
error: e.message,
}),
});
}
},
async getExternalAPIStatusCodes() {
try {
const result = await formService.externalAPIStatusCodes(this.form.id);
Expand Down Expand Up @@ -168,10 +140,6 @@ export default {
userTokenHeader: null,
userTokenBearer: false,
sendUserInfo: false,
userInfoHeader: null,
userInfoEncrypted: false,
userInfoEncryptionKey: null,
userInfoEncryptionAlgo: null,
},
show: false,
};
Expand Down Expand Up @@ -472,59 +440,6 @@ export default {
</template>
</v-checkbox></v-col
>
<v-col cols="4" class="pb-0"
><v-checkbox
v-model="editDialog.item.userInfoEncrypted"
class="my-0 pt-0"
>
<template #label>
<span :class="{ 'mr-2': isRTL }" :lang="lang">
{{ $t('trans.externalAPI.formUserInfoEncrypted') }}
</span>
</template>
</v-checkbox></v-col
>
</v-row>
<v-row class="mt-0">
<v-col cols="4"></v-col>
<v-col cols="8"
><v-text-field
v-model="editDialog.item.userInfoHeader"
density="compact"
solid
variant="outlined"
:label="$t('trans.externalAPI.formUserInfoHeader')"
data-test="text-userInfoHeader"
:lang="lang"
:rules="userInfoHeaderRules"
/></v-col>
</v-row>
<v-row class="mt-0">
<v-col cols="4">
<v-select
v-model="editDialog.item.userInfoEncryptionAlgo"
:items="externalAPIAlgorithmList"
item-title="display"
item-value="code"
:label="$t('trans.externalAPI.formUserInfoEncryptionAlgo')"
density="compact"
solid
variant="outlined"
:rules="userInfoHeaderRules"
:lang="lang"
></v-select
></v-col>
<v-col cols="8">
<v-text-field
v-model="editDialog.item.userInfoEncryptionKey"
density="compact"
solid
variant="outlined"
:label="$t('trans.externalAPI.formUserInfoEncryptionKey')"
data-test="text-userInfoEncryptionKey"
:lang="lang"
:rules="userInfoHeaderRules"
/></v-col>
</v-row>
<!-- User Token -->
<hr v-if="editDialog.item.allowSendUserToken" />
Expand Down
8 changes: 1 addition & 7 deletions app/frontend/src/internationalization/trans/chefs/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@
"editTitle": "Edit External API Configuration",
"fetchError": "An error occurred while fetching the External API.",
"fetchListError": "An error occurred while fetching the External API list.",
"fetchAlgoListError": "An error occurred while fetching the External API Encryption Algorithm list.",
"fetchStatusListError": "An error occurred while fetching the External API Status Code list.",
"save": "Save",
"formName": "Name",
Expand All @@ -110,16 +109,11 @@
"formUserTokenHeader": "User Token Header Name",
"formUserTokenBearer": "User Token as Bearer Token",
"formSendUserInfo": "Send User Information",
"formUserInfoEncrypted": "Encrypt User Information",
"formUserInfoHeader": "Encrypt User Information Header Name",
"formUserInfoEncryptionKey": "User Information Encryption Key",
"formUserInfoEncryptionAlgo": "User Information Encryption Algorithm",
"formNameReq": "Name is required.",
"formNameMaxChars": "Name must be 255 characters or less",
"validEndpointRequired": "Please enter a valid endpoint starting with http:// or https://",
"apiKeyFieldRequired": "Required when 'Send API Key' selected.",
"userTokenFieldRequired": "Required when 'Send User Token' selected.",
"userInfoFieldRequired": "Required when 'Send User Information' & 'Encrypt User Information' selected."
"userTokenFieldRequired": "Required when 'Send User Token' selected."
},
"formSettings": {
"pressToAddMultiEmail": "Press <kbd>enter</kbd> or <kbd>,</kbd> or <kbd>space</kbd> to add multiple email addresses",
Expand Down
10 changes: 10 additions & 0 deletions app/src/components/jwtService.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ class JwtService {
return payload;
}

async getUnverifiedPayload(token) {
// no verification, this is just to read fields
try {
const { payload } = await jose.jwtVerify(token, JWKS, {});
return payload;
} catch (e) {
return null;
}
}

async validateAccessToken(token) {
try {
await this._verify(token);
Expand Down
4 changes: 0 additions & 4 deletions app/src/db/migrations/20240521210143_046_external_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ exports.up = function (knex) {
table.boolean('userTokenBearer').defaultTo(true);

table.boolean('sendUserInfo').defaultTo(false);
table.string('userInfoHeader');
table.boolean('userInfoEncrypted').defaultTo(false);
table.string('userInfoEncryptionKey');
table.string('userInfoEncryptionAlgo');
stamps(knex, table);
})
)
Expand Down
4 changes: 0 additions & 4 deletions app/src/forms/common/models/tables/externalAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ class ExternalAPI extends Timestamps(Model) {
userTokenHeader: { type: ['string', 'null'] },
userTokenBearer: { type: 'boolean', default: true },
sendUserInfo: { type: 'boolean', default: false },
userInfoHeader: { type: ['string', 'null'] },
userInfoEncrypted: { type: 'boolean', default: false },
userInfoEncryptionKey: { type: ['string', 'null'] },
userInfoEncryptionAlgo: { type: ['string', 'null'] },
...stamps,
},
additionalProperties: false,
Expand Down
13 changes: 0 additions & 13 deletions app/src/forms/form/externalApi/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,6 @@ const service = {
throw new Problem(422, `'userTokenHeader' is required when 'sendUserToken' is true.`);
}
}
if (data.sendUserInfo) {
if (data.userInfoEncrypted && !data.userInfoHeader) {
throw new Problem(422, `'userInfoHeader' is required when 'sendUserInfo' and 'userInfoEncrypted' are true.`);
}
if (data.userInfoEncrypted) {
if (!Object.values(ENCRYPTION_ALGORITHMS).includes(data.userInfoEncryptionAlgo)) {
throw new Problem(422, `'${data.userInfoEncryptionAlgo}' is not a valid Encryption Algorithm.`);
}
if (!data.userInfoEncryptionKey) {
throw new Problem(422, `'userInfoEncryptionKey' is required when 'userInfoEncrypted' is true.`);
}
}
}
},

checkAllowSendUserToken: (data, allowSendUserToken) => {
Expand Down
2 changes: 1 addition & 1 deletion app/src/forms/proxy/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ module.exports = {
// add path to endpoint url if included in headers...
const extUrl = service.createExternalAPIUrl(req.headers, extAPI.endpointUrl);
// build list of request headers based on configuration...
const extHeaders = service.createExternalAPIHeaders(extAPI, proxyHeaderInfo);
const extHeaders = await service.createExternalAPIHeaders(extAPI, proxyHeaderInfo);
let axiosInstance = axios.create({
headers: extHeaders,
});
Expand Down
46 changes: 26 additions & 20 deletions app/src/forms/proxy/service.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const { encryptionService } = require('../../components/encryptionService');
const jwtService = require('../../components/jwtService');

const { ExternalAPI } = require('../../forms/common/models');

const headerValue = (headers, key) => {
Expand Down Expand Up @@ -68,7 +70,7 @@ const service = {
}
return endpointUrl;
},
createExternalAPIHeaders: (externalAPI, proxyHeaderInfo) => {
createExternalAPIHeaders: async (externalAPI, proxyHeaderInfo) => {
const result = {};
if (externalAPI.sendApiKey) {
result[externalAPI.apiKeyHeader] = externalAPI.apiKey;
Expand All @@ -87,26 +89,30 @@ const service = {
throw new Error('Cannot create user headers for External API without populated proxy header info object.');
}

if (externalAPI.userInfoEncrypted) {
// do not send the token
delete proxyHeaderInfo['token'];
const encUserInfo = encryptionService.encryptExternal(externalAPI.userInfoEncryptionAlgo, externalAPI.userInfoEncryptionKey, proxyHeaderInfo);
result[externalAPI.userInfoHeader] = encUserInfo;
} else {
// user information (no token)
let prefix = 'X-CHEFS-USER';
let fields = ['userId', 'username', 'firstName', 'lastName', 'fullName', 'email', 'idp'];
fields.forEach((field) => {
if (proxyHeaderInfo[field]) {
result[`${prefix}-${field}`.toUpperCase()] = proxyHeaderInfo[field];
}
});
// form information...
prefix = 'X-CHEFS-FORM';
fields = ['formId', 'versionId', 'submissionId'];
// user information (no token)
let prefix = 'X-CHEFS-USER';
let fields = ['userId', 'username', 'firstName', 'lastName', 'fullName', 'email', 'idp'];
fields.forEach((field) => {
if (proxyHeaderInfo[field]) {
result[`${prefix}-${field}`.toUpperCase()] = proxyHeaderInfo[field];
}
});
// form information...
prefix = 'X-CHEFS-FORM';
fields = ['formId', 'versionId', 'submissionId'];
fields.forEach((field) => {
if (proxyHeaderInfo[field]) {
result[`${prefix}-${field}`.toUpperCase()] = proxyHeaderInfo[field];
}
});
// grab raw token values...
const payload = await jwtService.getUnverifiedPayload(proxyHeaderInfo['token']);
if (payload) {
prefix = 'X-CHEFS-TOKEN';
fields = ['sub', 'iat', 'exp'];
fields.forEach((field) => {
if (proxyHeaderInfo[field]) {
result[`${prefix}-${field}`.toUpperCase()] = proxyHeaderInfo[field];
if (payload[field]) {
result[`${prefix}-${field}`.toUpperCase()] = payload[field];
}
});
}
Expand Down
33 changes: 13 additions & 20 deletions app/tests/unit/forms/proxy/service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,58 +198,54 @@ describe('Proxy Service', () => {
it('should throw error with no headers', async () => {
const externalAPI = undefined;
const proxyHeaderInfo = goodProxyHeaderInfo;
expect(() => service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo)).toThrow();
await expect(service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo)).rejects.toThrow();
});
it('should throw error with no current user and sending user information', async () => {
const externalAPI = goodExternalApi;
const proxyHeaderInfo = undefined;
expect(() => service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo)).toThrow();
await expect(service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo)).rejects.toThrow();
});
it('should throw error with invalid current user and sending user information', async () => {
const externalAPI = goodExternalApi;
const proxyHeaderInfo = {};
expect(() => service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo)).toThrow();
await expect(service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo)).rejects.toThrow();
});
it('should NOT throw error with no current user and not sending user information', async () => {
const externalAPI = Object.assign({}, goodExternalApi);
externalAPI.sendUserToken = false;
externalAPI.sendUserInfo = false;
const proxyHeaderInfo = undefined;
const result = service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
const result = await service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
expect(result).toBeTruthy();
});
it('should NOT throw error with invalid current user and not sending user information', async () => {
const externalAPI = Object.assign({}, goodExternalApi);
externalAPI.sendUserToken = false;
externalAPI.sendUserInfo = false;
const proxyHeaderInfo = {};
const result = service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
const result = await service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
expect(result).toBeTruthy();
});
it('should return populated headers', async () => {
const externalAPI = Object.assign({}, goodExternalApi);
const proxyHeaderInfo = Object.assign({}, goodProxyHeaderInfo);
const result = service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
const result = await service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
expect(result).toBeTruthy();
// with the defaul external API config we should have headers for...
// api key
expect(result[externalAPI.apiKeyHeader]).toBe(externalAPI.apiKey);
// user token (with Bearer)
expect(result[externalAPI.userTokenHeader]).toBe(`Bearer ${token}`);
// user info (encrypted)
expect(result[externalAPI.userInfoHeader]).toBeTruthy();
const decrypted = encryptionService.decryptExternal(externalAPI.userInfoEncryptionAlgo, externalAPI.userInfoEncryptionKey, result[externalAPI.userInfoHeader]);
expect(JSON.parse(decrypted)).toMatchObject(proxyHeaderInfo);
// but no unencrypted user info headers
expect(result['X-CHEFS-USER-EMAIL']).toBeFalsy();
// use r
expect(result['X-CHEFS-USER-EMAIL']).toBeTruthy();
});
it('should return only api key headers', async () => {
const externalAPI = Object.assign({}, goodExternalApi);
externalAPI.sendApiKey = true;
externalAPI.sendUserToken = false;
externalAPI.sendUserInfo = false;
const proxyHeaderInfo = Object.assign({}, goodProxyHeaderInfo);
const result = service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
const result = await service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
expect(result).toBeTruthy();
// api key
expect(result[externalAPI.apiKeyHeader]).toBe(externalAPI.apiKey);
Expand All @@ -267,34 +263,31 @@ describe('Proxy Service', () => {
externalAPI.sendUserInfo = false;
externalAPI.userTokenBearer = false;
const userInfo = Object.assign({}, goodProxyHeaderInfo);
const result = service.createExternalAPIHeaders(externalAPI, userInfo);
const result = await service.createExternalAPIHeaders(externalAPI, userInfo);
expect(result).toBeTruthy();
// no api key
expect(result[externalAPI.apiKeyHeader]).toBeFalsy();
// user token (NO Bearer)
expect(result[externalAPI.userTokenHeader]).toBe(token);
// no user info (encrypted)
expect(result[externalAPI.userInfoHeader]).toBeFalsy();
// no unencrypted user info headers
// no user info headers
expect(result['X-CHEFS-USER-EMAIL']).toBeFalsy();
});
it('should return only unencrypted user info headers', async () => {
const externalAPI = Object.assign({}, goodExternalApi);
externalAPI.sendApiKey = false;
externalAPI.sendUserToken = false;
externalAPI.sendUserInfo = true;
externalAPI.userInfoEncrypted = false;
const userInfo = Object.assign({}, goodProxyHeaderInfo);
const result = service.createExternalAPIHeaders(externalAPI, userInfo);
const result = await service.createExternalAPIHeaders(externalAPI, userInfo);
expect(result).toBeTruthy();
// with the defaul external API config we should have headers for...
// no api key
expect(result[externalAPI.apiKeyHeader]).toBeFalsy();
// no user token (with Bearer)
expect(result[externalAPI.userTokenHeader]).toBeFalsy();
// no user info (encrypted)
expect(result[externalAPI.userInfoHeader]).toBeFalsy();
// unencrypted user info headers
// user info headers
expect(result['X-CHEFS-USER-EMAIL']).toBe(userInfo.email);
expect(result['X-CHEFS-FORM-FORMID']).toBe(userInfo.formId);
});
Expand Down
Loading

0 comments on commit ed67b63

Please sign in to comment.