Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean the output when useJWT=true #756

Merged
merged 24 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions global_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// "apiKey": "<REPLACE ME>", // The answers api key found on the experiences page. This will be provided automatically by the Yext CI system
// "experienceVersion": "<REPLACE ME>", // the Answers Experience version to use for API requests. This will be provided automatically by the Yext CI system
// "businessId": "<REPLACE ME>", // The business ID of the account. This will be provided automatically by the Yext CI system
// "useJWT": true, // Whether or not to enable JWT. If true, the apiKey will be ignored and initAnswersJWT(token) or initAnswersFrameJWT(token) must be called.
"logo": "", // The link to the logo for open graph meta tag - og:image.
"favicon": "",
"googleTagManagerName": "dataLayer", // The name of your Google Tag Manager data layer
Expand Down
26 changes: 26 additions & 0 deletions hooks/templatedataformatter.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const getCleanedJamboInjectedData = require('../static/webpack/getCleanedJamboInjectedData');

/**
* Formats the data sent to the handlebars templates during Jambo builds.
*
Expand Down Expand Up @@ -26,6 +28,9 @@ module.exports = function (pageMetadata, siteLevelAttributes, pageNameToConfig)
JAMBO_INJECTED_DATA: env.JAMBO_INJECTED_DATA
}
};
if (globalConfig.useJWT) {
return getCleanedTemplateData(templateData);
}
return templateData;
}

Expand All @@ -50,4 +55,25 @@ function getLocalizedGlobalConfig(globalConfig, currentLocaleConfig, locale) {
localizedGlobalConfig.locale = locale;
}
return localizedGlobalConfig;
}

/**
* Returns the provided template data without the API Key
*
* @param {Object} templateData
* @returns {Object}
*/
function getCleanedTemplateData(templateData) {
const jamboInjectedData = templateData.env.JAMBO_INJECTED_DATA;
const globalConfig = templateData.global_config;
return {
...templateData,
global_config: {
...globalConfig,
apiKey: undefined
},
env: {
JAMBO_INJECTED_DATA: getCleanedJamboInjectedData(jamboInjectedData)
}
}
}
11 changes: 10 additions & 1 deletion layouts/html.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@
window.iframeLoaded = new Promise(resolve => {
iframeLoadedResolve = resolve;
});
window.answersJWT = new Promise(resolve => {
window.initAnswersJWT = resolve;
});
window.iFrameResizer = {
onReady: function() {
window.parentIFrame.sendMessage(JSON.stringify({
Expand All @@ -132,7 +135,13 @@
}));
iframeLoadedResolve();
},
onMessage: window.isOverlay ? window.Overlay.onMessage : function() {},
onMessage: (message) => {
if (message.token) {
initAnswersJWT(message.token);
return;
}
window.isOverlay && window.Overlay.onMessage(message);
}
};
{{/babel}}
</script>
Expand Down
30 changes: 27 additions & 3 deletions script/core.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
const IS_STAGING = HitchhikerJS.isStaging(JAMBO_INJECTED_DATA?.pages?.stagingDomains || []);
const injectedConfig = {
experienceVersion: IS_STAGING ? 'STAGING' : 'PRODUCTION',
apiKey: HitchhikerJS.getInjectedProp('{{{global_config.experienceKey}}}', ['apiKey']),
{{#unless global_config.useJWT}}
apiKey: HitchhikerJS.getInjectedProp('{{{global_config.experienceKey}}}', ['apiKey']),
{{/unless}}
{{#with env.JAMBO_INJECTED_DATA}}
{{#if businessId}}businessId: "{{businessId}}",{{/if}}
{{/with}}
Expand All @@ -28,10 +30,32 @@
{{/if}}
{{/with}}
};
{{#if global_config.useJWT}}
const jwtNotProvidedTimeout = setTimeout(() => {
console.warn(
'A JWT has not been received within 5 seconds of page load, and "useJWT" is set to true.\n' +
'Load the experience by calling initAnswersJWT(token).'
);
}, 5000);
window.answersJWT.then(token => {
clearTimeout(jwtNotProvidedTimeout);
initAnswersInstance({
...injectedConfig,
...userConfig,
apiKey: token
});
});
{{else}}
initAnswersInstance({
...injectedConfig,
...userConfig
});
{{/if}}
}
function initAnswersInstance (config) {
ANSWERS.init({
templateBundle: TemplateBundle.default,
...injectedConfig,
...userConfig,
...config,
querySource: window.isOverlay ? 'OVERLAY' : 'STANDARD',
onStateChange: (objParams, stringParams, replaceHistory) => {
if ('parentIFrame' in window) {
Expand Down
7 changes: 6 additions & 1 deletion static/js/iframe-common.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require('iframe-resizer');

export function generateIFrame(domain, queryParam, urlParam) {
export function generateIFrame(domain, queryParam, urlParam, token) {
var isLocalHost = window.location.host.split(':')[0] === 'localhost';
var containerEl = document.querySelector('#answers-container');
var iframe = document.createElement('iframe');
Expand Down Expand Up @@ -82,6 +82,11 @@ export function generateIFrame(domain, queryParam, urlParam) {
// For dynamic iFrame resizing
iFrameResize({
checkOrigin: false,
onInit: function(iframe) {
token && iframe.iFrameResizer.sendMessage({
token: token
});
},
onMessage: function(messageData) {
const message = JSON.parse(messageData.message);
if (message.action === "paginate") {
Expand Down
11 changes: 11 additions & 0 deletions static/js/iframe-jwt-prod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateIFrame } from './iframe-common';
import InjectedData from './models/InjectedData';
import getJwtNotProvidedTimeout from './utils/getJwtNotProvidedTimeout';

const prodDomain = new InjectedData().getProdDomain();
const jwtNotProvidedTimeout = getJwtNotProvidedTimeout();

window.initAnswersFrameJWT = function (token) {
clearTimeout(jwtNotProvidedTimeout);
generateIFrame(prodDomain, null, null, token);
}
11 changes: 11 additions & 0 deletions static/js/iframe-jwt-staging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateIFrame } from './iframe-common';
import InjectedData from './models/InjectedData';
import getJwtNotProvidedTimeout from './utils/getJwtNotProvidedTimeout';

const stagingDomain = new InjectedData().getStagingDomain();
const jwtNotProvidedTimeout = getJwtNotProvidedTimeout();

window.initAnswersFrameJWT = function (token) {
clearTimeout(jwtNotProvidedTimeout);
generateIFrame(stagingDomain, null, null, token);
}
11 changes: 11 additions & 0 deletions static/js/iframe-jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateIFrame } from './iframe-common';
import InjectedData from './models/InjectedData';
import getJwtNotProvidedTimeout from './utils/getJwtNotProvidedTimeout';

const domain = new InjectedData().getDomain();
const jwtNotProvidedTimeout = getJwtNotProvidedTimeout();

window.initAnswersFrameJWT = function (token) {
clearTimeout(jwtNotProvidedTimeout);
generateIFrame(domain, null, null, token);
}
8 changes: 8 additions & 0 deletions static/js/utils/getJwtNotProvidedTimeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function getJwtNotProvidedTimeout () {
return setTimeout(() => {
console.warn(
'A JWT has not been received within 5 seconds of page load, and "useJWT" is set to true.\n' +
'Load the experience by calling initAnswersFrameJWT(token).'
);
}, 5000);
}
28 changes: 26 additions & 2 deletions static/webpack-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlPlugin = require('html-webpack-plugin');
const RemovePlugin = require('remove-files-webpack-plugin');
const { merge } = require('webpack-merge');
const { parse } = require('comment-json');

module.exports = function () {
const jamboConfig = require('./jambo.json');
Expand All @@ -25,11 +26,31 @@ module.exports = function () {
});
}

const globalConfigPath = `./${jamboConfig.dirs.config}/global_config.json`;
let globalConfig = null;
cea2aj marked this conversation as resolved.
Show resolved Hide resolved
if (fs.existsSync(globalConfigPath)) {
globalConfigRaw = fs.readFileSync(globalConfigPath, 'utf-8');
globalConfig = parse(globalConfigRaw);
}

const useJWT = globalConfig && globalConfig.useJWT
let jamboInjectedData = process.env.JAMBO_INJECTED_DATA || null;
jamboInjectedData = (jamboInjectedData && JSON.parse(jamboInjectedData));

const getCleanedJamboInjectedData =
require(`./${jamboConfig.dirs.output}/static/webpack/getCleanedJamboInjectedData.js`);

cea2aj marked this conversation as resolved.
Show resolved Hide resolved
let updatedJamboInjectedData = useJWT
? getCleanedJamboInjectedData(jamboInjectedData)
: jamboInjectedData

const plugins = [
new MiniCssExtractPlugin({ filename: '[name].css' }),
...htmlPlugins,
new webpack.EnvironmentPlugin({
JAMBO_INJECTED_DATA: null
new webpack.DefinePlugin({
'process.env.JAMBO_INJECTED_DATA': updatedJamboInjectedData
? JSON.stringify(JSON.stringify(updatedJamboInjectedData))
: null
}),
new RemovePlugin({
after: {
Expand All @@ -52,6 +73,9 @@ module.exports = function () {
'HitchhikerJS': `./${jamboConfig.dirs.output}/static/entry.js`,
'HitchhikerCSS': `./${jamboConfig.dirs.output}/static/css-entry.js`,
'iframe': `./${jamboConfig.dirs.output}/static/js/iframe.js`,
'iframe-jwt': `./${jamboConfig.dirs.output}/static/js/iframe-jwt.js`,
'iframe-jwt-prod': `./${jamboConfig.dirs.output}/static/js/iframe-jwt-prod.js`,
'iframe-jwt-staging': `./${jamboConfig.dirs.output}/static/js/iframe-jwt-staging.js`,
'answers': `./${jamboConfig.dirs.output}/static/js/iframe.js`,
'overlay-button': `./${jamboConfig.dirs.output}/static/js/overlay/button-frame/entry.js`,
'overlay': `./${jamboConfig.dirs.output}/static/js/overlay/parent-frame/yxtanswersoverlay.js`,
Expand Down
31 changes: 31 additions & 0 deletions static/webpack/getCleanedJamboInjectedData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const _ = require('lodash');

/**
* Returns JAMBO_INJECTED_DATA with instances of the global config's apiKey removed
*
* @param {Object} data JAMBO_INJECTED_DATA
* @returns {Object}
*/
function getCleanedJamboInjectedData (data) {
if (!data || !data.answers || !data.answers.experiences) {
return;
}
const updatedData = _.cloneDeep(data);
const experiences = updatedData.answers.experiences;

const removeApiKeyFromConfig = config => {
if ('apiKey' in config) {
delete config['apiKey'];
}
}

Object.values(experiences).forEach(config => {
removeApiKeyFromConfig(config);
if ('configByLabel' in config) {
Object.values(config.configByLabel).forEach(removeApiKeyFromConfig);
}
});
return updatedData;
}

module.exports = getCleanedJamboInjectedData;
1 change: 1 addition & 0 deletions test-site/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
!config/index.json
!pages/index.html.hbs
!public/iframe_test.html
!public/iframe_jwt_test.html
!public/overlay.html
public/
config/
Expand Down
5 changes: 3 additions & 2 deletions test-site/jambo.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"directanswercards"
],
"preservedFiles": [
"public/overlay.html",
"public/iframe_test.html"
"public/iframe_test.html",
"public/iframe_jwt_test.html",
"public/overlay.html"
]
},
"defaultTheme": "answers-hitchhiker-theme"
Expand Down
1 change: 1 addition & 0 deletions test-site/public/iframe_jwt_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><head><meta name="viewport" content="initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,width=device-width"></head><body><div id="answers-container"></div><script src="iframe-jwt.js"></script></body></html>
34 changes: 34 additions & 0 deletions tests/static/webpack/getCleanedJamboInjectedData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import getCleanedJamboInjectedData from '../../../static/webpack/getCleanedJamboInjectedData';

describe('secures the injected data', () => {
const sampleConfig = {
apiKey: 999,
verticals: {
KM: {
displayName: 'Locations',
source: 'KNOWLEDGE_MANAGER'
}
}
};

const mockInjectedData = {
businessId: 999,
answers: {
experiences: {
test_experience: {
...sampleConfig,
configByLabel: {
PRODUCTION: sampleConfig,
STAGING: sampleConfig
}
}
}
}
};

it('removes instances of the apiKey', () => {
const securedInjectedData = getCleanedJamboInjectedData(mockInjectedData);
expect(securedInjectedData).toEqual(expect.not.objectContaining({apiKey: 999}));
});
});