@nice-digital/global-nav 7.1.1187 → 7.1.1189-GN-264-load-aws-chal
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/lib/Footer/Footer.js +7 -2
- package/lib/Header/Account/nice-accounts.js +9 -1
- package/lib/Header/Header.js +7 -2
- package/lib/Main/Main.js +7 -2
- package/lib/aws-waf/index.js +117 -0
- package/lib/aws-waf/integrations.js +128 -0
- package/lib/aws-waf/propTypes.js +20 -0
- package/lib/hooks/useAwsWafChallenge.js +19 -0
- package/lib/renderer.js +9 -1
- package/package.json +1 -1
- package/types.d.ts +10 -0
package/README.md
CHANGED
|
@@ -313,6 +313,13 @@ For a full list of all the available props, see the [props section](#props) belo
|
|
|
313
313
|
|
|
314
314
|
Optional spacing between page content and footer back-to-top link.
|
|
315
315
|
|
|
316
|
+
###### Main.awsWafScriptURL
|
|
317
|
+
|
|
318
|
+
- Type: `String | false`
|
|
319
|
+
- Default: `undefined`
|
|
320
|
+
|
|
321
|
+
See [Header.awsWafScriptURL](#headerawswafscripturl).
|
|
322
|
+
|
|
316
323
|
##### Header props
|
|
317
324
|
|
|
318
325
|
###### Header.service
|
|
@@ -663,6 +670,27 @@ If the authentication provider has been set to "idam", then an array of links mu
|
|
|
663
670
|
|
|
664
671
|
The displayName is the user's name and must be provided if the "idam" provider is used and the user is logged in.
|
|
665
672
|
|
|
673
|
+
###### Header.awsWafScriptURL
|
|
674
|
+
|
|
675
|
+
- Type: `String | false`
|
|
676
|
+
- Default: `undefined`
|
|
677
|
+
|
|
678
|
+
Overrides how the [AWS WAF challenge](#aws-waf-challenge) script is loaded. Leave it out and Global Nav loads the script for the page's hostname, if it knows of one.
|
|
679
|
+
|
|
680
|
+
Pass a URL to load that script instead, for example to test against a stand-in locally:
|
|
681
|
+
|
|
682
|
+
```js
|
|
683
|
+
<Header awsWafScriptURL="https://localhost:4000/mock-aws-waf-jsapi.js" />
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Pass `false` to stop Global Nav loading the script at all, for example because your app includes it itself:
|
|
687
|
+
|
|
688
|
+
```js
|
|
689
|
+
<Header awsWafScriptURL={false} />
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
The `Header`, `Footer` and `Main` components all take this prop and all behave the same way, because no one of them is on every page of every service. Whichever mounts first does the work. `false` from any of them switches the challenge off for the rest of the page's life, so there's no need to pass it to all three as long as the one you pass it to mounts first - the header, usually.
|
|
693
|
+
|
|
666
694
|
##### Footer props
|
|
667
695
|
|
|
668
696
|
###### Footer.service
|
|
@@ -674,6 +702,13 @@ The displayName is the user's name and must be provided if the "idam" provider i
|
|
|
674
702
|
The identifier of the currently active service.
|
|
675
703
|
See [services.json](src/services.json) for a list of the available service identifiers.
|
|
676
704
|
|
|
705
|
+
###### Footer.awsWafScriptURL
|
|
706
|
+
|
|
707
|
+
- Type: `String | false`
|
|
708
|
+
- Default: `undefined`
|
|
709
|
+
|
|
710
|
+
See [Header.awsWafScriptURL](#headerawswafscripturl).
|
|
711
|
+
|
|
677
712
|
### CDN
|
|
678
713
|
|
|
679
714
|
Reference the Global Nav bundle directly from the NICE CDN to render the Global Nav. We recommend including this before the closing `</body>` tag but before your application's scripts:
|
|
@@ -816,6 +851,69 @@ The footer renders by default, set `footer` to `false` to stop it from rendering
|
|
|
816
851
|
Or, pass an object of key/value pairs of settings specific to the footer.
|
|
817
852
|
See the [footer props](#footer-props) section for available options.
|
|
818
853
|
|
|
854
|
+
##### awsWafScriptURL
|
|
855
|
+
|
|
856
|
+
- Type: `String | Boolean`
|
|
857
|
+
- Default: `undefined`
|
|
858
|
+
|
|
859
|
+
Overrides how the [AWS WAF challenge](#aws-waf-challenge) script is loaded: a URL to load that script instead of the one for the page's hostname, or `false` to not load it at all e.g. `global_nav_config = { awsWafScriptURL: false }`.
|
|
860
|
+
|
|
861
|
+
This sits at the top level of the config, next to `service`, rather than under `header` or `footer`. That's deliberate: it still applies when either of those is set to `false`.
|
|
862
|
+
|
|
863
|
+
### AWS WAF challenge
|
|
864
|
+
|
|
865
|
+
Global Nav loads the [AWS WAF JavaScript SDK](https://docs.aws.amazon.com/waf/latest/developerguide/waf-js-challenge-api.html) on services that sit behind an AWS WAF web ACL. The SDK silently acquires an `aws-waf-token` cookie in the background. The web ACL checks requests for that token and challenges the ones without it, which is how we tell real browsers from bots.
|
|
866
|
+
|
|
867
|
+
It's the web ACL that keeps the bots out, not this script. The script is there so that real users already hold a token by the time they need one, and never see the challenge interstitial.
|
|
868
|
+
|
|
869
|
+
Global Nav carries this because it's the one piece of code already on every NICE service. The alternative is each service adding the script for itself.
|
|
870
|
+
|
|
871
|
+
#### How it picks the script
|
|
872
|
+
|
|
873
|
+
Each web ACL issues its own script URL, and the ids in that URL are generated by AWS, so it can't be worked out from the hostname. Instead there's a lookup table in [_src/aws-waf/integrations.js_](src/aws-waf/integrations.js), with one entry per web ACL listing the hostnames it protects.
|
|
874
|
+
|
|
875
|
+
When Global Nav loads, it looks up `window.location.hostname` in that table. If there's an entry, it adds the script to the `<head>`. If there isn't, it does nothing, which is what we want for localhost and for any environment with no web ACL in front of it.
|
|
876
|
+
|
|
877
|
+
> The table lists ops' sandbox and our live, alpha, beta, test and dev hostnames already, but every `scriptURL` is empty until ops create the web ACLs: there are no URLs to put in before then. An entry with an empty URL does nothing, so until the URLs are filled in the script only loads when `awsWafScriptURL` is passed.
|
|
878
|
+
|
|
879
|
+
The script is only ever added once per page. Global Nav also does nothing if the SDK is already there, i.e. `window.AwsWafIntegration` exists or the page has its own script tag pointing at `awswaf.com`.
|
|
880
|
+
|
|
881
|
+
Use [`awsWafScriptURL`](#headerawswafscripturl) to override the lookup, or to switch the whole thing off.
|
|
882
|
+
|
|
883
|
+
#### Adding or filling in a web ACL
|
|
884
|
+
|
|
885
|
+
Each entry in [_src/aws-waf/integrations.js_](src/aws-waf/integrations.js) looks like this:
|
|
886
|
+
|
|
887
|
+
```js
|
|
888
|
+
export const integrations = [
|
|
889
|
+
{
|
|
890
|
+
hosts: ['www.nice.org.uk'],
|
|
891
|
+
scriptURL: 'https://<id>.eu-west-1.sdk.awswaf.com/<id>/<id>/challenge.js',
|
|
892
|
+
cookieDomains: ['.nice.org.uk'],
|
|
893
|
+
},
|
|
894
|
+
];
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
- `hosts` are exact hostnames, no wildcards. Hostnames that share a load balancer (or CloudFront distribution) share a web ACL, so list them all against the one entry. The entries we ship were grouped by which hostnames resolve to the same address, which is a best guess at that: confirm it against the web ACLs ops actually create, and merge or split entries to match.
|
|
898
|
+
- `scriptURL` comes from _Application integration_ in the AWS WAF console. Ops are issuing the _Intelligent threat integration_ URL, which ends in `challenge.js`; the _CAPTCHA integration_ one ends in `jsapi.js` and works the same way. It isn't a secret: it ends up in the page source. Leave it empty for a web ACL that doesn't exist yet and the entry does nothing.
|
|
899
|
+
- `cookieDomains` is optional, e.g. `['.nice.org.uk']`. It widens the token cookie from the exact hostname so that one token works across subdomains. It's only valid once the same domain is on the web ACL's token domain list, otherwise AWS rejects the tokens, so ask ops for that at the same time as the URL. The shipped entries all set it.
|
|
900
|
+
|
|
901
|
+
A new or changed entry needs a Global Nav release. Services loading the unversioned CDN bundle pick that up straight away; services using the npm package pick it up when they next upgrade and deploy.
|
|
902
|
+
|
|
903
|
+
#### NICE Accounts
|
|
904
|
+
|
|
905
|
+
NICE Accounts sits behind a web ACL too, and the header calls it to find out whether you're signed in. That call is a script tag, which can't send the token as a header, so it relies on the `aws-waf-token` cookie instead. Global Nav waits for the SDK to have a token before it makes the call, so the cookie is there to send. With no SDK on the page it doesn't wait at all, and if the SDK hasn't got a token after 5 seconds it makes the call anyway.
|
|
906
|
+
|
|
907
|
+
This only works when the cookie reaches `accounts.nice.org.uk`:
|
|
908
|
+
|
|
909
|
+
- The page has to be on `nice.org.uk`. A browser won't send a cookie from one domain to another, so services on other domains, and sandbox on `sandbox1-ds.net`, call NICE Accounts without a token.
|
|
910
|
+
- The cookie has to be set on `.nice.org.uk` and not on the exact hostname. The shipped entries do that with `cookieDomains`. An [`awsWafScriptURL`](#headerawswafscripturl) override doesn't, so set `window.awsWafCookieDomainList = ['.nice.org.uk']` on the page before Global Nav loads.
|
|
911
|
+
- `.nice.org.uk` has to be on the token domain list of both web ACLs: the one in front of the page and the one in front of NICE Accounts.
|
|
912
|
+
|
|
913
|
+
#### Content Security Policy
|
|
914
|
+
|
|
915
|
+
If your service sets a Content Security Policy then `script-src` and `connect-src` need to allow `https://*.awswaf.com`.
|
|
916
|
+
|
|
819
917
|
## Deployments
|
|
820
918
|
|
|
821
919
|
We create 2 deployment artifacts: one for deploying to the CDN and one test site for previewing the header and footer.
|
package/lib/Footer/Footer.js
CHANGED
|
@@ -14,10 +14,14 @@ var _Pages = _interopRequireDefault(require("./Pages"));
|
|
|
14
14
|
var _FooterModule = _interopRequireDefault(require("./Footer.module.scss"));
|
|
15
15
|
var _TrackedLink = _interopRequireDefault(require("../TrackedLink"));
|
|
16
16
|
var _tracker = require("../tracker");
|
|
17
|
+
var _propTypes2 = require("../aws-waf/propTypes");
|
|
18
|
+
var _useAwsWafChallenge = require("../hooks/useAwsWafChallenge");
|
|
17
19
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
18
20
|
//TODO add string type for service prop
|
|
19
21
|
const Footer = _ref => {
|
|
20
|
-
let service = _ref.service
|
|
22
|
+
let service = _ref.service,
|
|
23
|
+
awsWafScriptURL = _ref.awsWafScriptURL;
|
|
24
|
+
(0, _useAwsWafChallenge.useAwsWafChallenge)(awsWafScriptURL);
|
|
21
25
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)("footer", {
|
|
22
26
|
className: _FooterModule.default.footer,
|
|
23
27
|
"data-tracking": "Global footer",
|
|
@@ -39,6 +43,7 @@ const Footer = _ref => {
|
|
|
39
43
|
});
|
|
40
44
|
};
|
|
41
45
|
Footer.propTypes = {
|
|
42
|
-
service: _propTypes.default.string
|
|
46
|
+
service: _propTypes.default.string,
|
|
47
|
+
awsWafScriptURL: _propTypes2.awsWafScriptURLPropType
|
|
43
48
|
};
|
|
44
49
|
var _default = exports.default = Footer;
|
|
@@ -4,11 +4,20 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.getDomainBaseUrl = exports.checkIsLoggedIn = void 0;
|
|
7
|
+
var _awsWaf = require("../../aws-waf");
|
|
7
8
|
// Checks if you are logged in via NICE Accounts
|
|
8
9
|
// Returns a promise that resolves with the data from NICE Accounts.
|
|
9
10
|
// Returns a promise that rejects if the data could not be loaded.
|
|
10
11
|
|
|
11
12
|
const checkIsLoggedIn = function (environment) {
|
|
13
|
+
// NICE Accounts sits behind an AWS WAF web ACL. A script tag can't send the token as a header,
|
|
14
|
+
// so wait for the SDK to set the token cookie, which the browser then sends along by itself
|
|
15
|
+
return (0, _awsWaf.whenAwsWafTokenReady)().then(function () {
|
|
16
|
+
return loadAccountsData(environment);
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
exports.checkIsLoggedIn = checkIsLoggedIn;
|
|
20
|
+
const loadAccountsData = function (environment) {
|
|
12
21
|
return new Promise(function (resolve, reject) {
|
|
13
22
|
const url = getDomainBaseUrl(environment) + "tophat";
|
|
14
23
|
var body = document.body;
|
|
@@ -30,7 +39,6 @@ const checkIsLoggedIn = function (environment) {
|
|
|
30
39
|
body.insertBefore(script, body.firstChild);
|
|
31
40
|
});
|
|
32
41
|
};
|
|
33
|
-
exports.checkIsLoggedIn = checkIsLoggedIn;
|
|
34
42
|
const getDomainBaseUrl = function (environment) {
|
|
35
43
|
return "https://".concat(environment !== "live" ? environment + "-" : "", "accounts.nice.org.uk/");
|
|
36
44
|
};
|
package/lib/Header/Header.js
CHANGED
|
@@ -17,6 +17,8 @@ var _Search = _interopRequireDefault(require("./Search"));
|
|
|
17
17
|
var _Account = _interopRequireDefault(require("./Account"));
|
|
18
18
|
var _SkipLink = _interopRequireDefault(require("./SkipLink"));
|
|
19
19
|
var _utils = require("../utils");
|
|
20
|
+
var _propTypes2 = require("../aws-waf/propTypes");
|
|
21
|
+
var _useAwsWafChallenge = require("../hooks/useAwsWafChallenge");
|
|
20
22
|
var _HeaderModule = _interopRequireDefault(require("./Header.module.scss"));
|
|
21
23
|
var _HeaderContext = require("./context/HeaderContext");
|
|
22
24
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
@@ -38,7 +40,8 @@ const Header = _ref => {
|
|
|
38
40
|
_ref$additionalSubMen = _ref.additionalSubMenuItems,
|
|
39
41
|
additionalSubMenuItems = _ref$additionalSubMen === void 0 ? [] : _ref$additionalSubMen,
|
|
40
42
|
onDropdownOpen = _ref.onDropdownOpen,
|
|
41
|
-
onDropdownClose = _ref.onDropdownClose
|
|
43
|
+
onDropdownClose = _ref.onDropdownClose,
|
|
44
|
+
awsWafScriptURL = _ref.awsWafScriptURL;
|
|
42
45
|
const _useState = (0, _react.useState)(false),
|
|
43
46
|
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
|
|
44
47
|
isExpanded = _useState2[0],
|
|
@@ -51,6 +54,7 @@ const Header = _ref => {
|
|
|
51
54
|
_useState6 = (0, _slicedToArray2.default)(_useState5, 2),
|
|
52
55
|
accountsData = _useState6[0],
|
|
53
56
|
setAccountsData = _useState6[1];
|
|
57
|
+
(0, _useAwsWafChallenge.useAwsWafChallenge)(awsWafScriptURL);
|
|
54
58
|
(0, _react.useEffect)(() => {
|
|
55
59
|
if (!document.getElementById(skipLinkId)) {
|
|
56
60
|
const firstH1OnPage = document.getElementsByTagName("h1")[0];
|
|
@@ -356,7 +360,8 @@ Header.propTypes = {
|
|
|
356
360
|
onDropdownClose: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.string]),
|
|
357
361
|
onRendered: _propTypes.default.func,
|
|
358
362
|
additionalSubMenuItems: _propTypes.default.arrayOf(_propTypes.default.object),
|
|
359
|
-
renderSearchOnly: _propTypes.default.bool
|
|
363
|
+
renderSearchOnly: _propTypes.default.bool,
|
|
364
|
+
awsWafScriptURL: _propTypes2.awsWafScriptURLPropType
|
|
360
365
|
};
|
|
361
366
|
OldHeader.propTypes = {
|
|
362
367
|
service: _propTypes.default.string,
|
package/lib/Main/Main.js
CHANGED
|
@@ -12,8 +12,10 @@ var _BackToTop = require("./BackToTop/BackToTop");
|
|
|
12
12
|
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
13
13
|
var _classnames = _interopRequireDefault(require("classnames"));
|
|
14
14
|
var _MainModule = _interopRequireDefault(require("./Main.module.scss"));
|
|
15
|
+
var _propTypes2 = require("../aws-waf/propTypes");
|
|
16
|
+
var _useAwsWafChallenge = require("../hooks/useAwsWafChallenge");
|
|
15
17
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
16
|
-
const _excluded = ["children", "className", "withPadding"];
|
|
18
|
+
const _excluded = ["children", "className", "withPadding", "awsWafScriptURL"];
|
|
17
19
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
18
20
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
19
21
|
function Main(props) {
|
|
@@ -21,7 +23,9 @@ function Main(props) {
|
|
|
21
23
|
className = props.className,
|
|
22
24
|
_props$withPadding = props.withPadding,
|
|
23
25
|
withPadding = _props$withPadding === void 0 ? true : _props$withPadding,
|
|
26
|
+
awsWafScriptURL = props.awsWafScriptURL,
|
|
24
27
|
rest = (0, _objectWithoutProperties2.default)(props, _excluded);
|
|
28
|
+
(0, _useAwsWafChallenge.useAwsWafChallenge)(awsWafScriptURL);
|
|
25
29
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)("main", _objectSpread(_objectSpread({
|
|
26
30
|
className: (0, _classnames.default)([_MainModule.default.main, className], withPadding && _MainModule.default.withPadding)
|
|
27
31
|
}, rest), {}, {
|
|
@@ -31,6 +35,7 @@ function Main(props) {
|
|
|
31
35
|
Main.propTypes = {
|
|
32
36
|
children: _propTypes.default.oneOfType([_propTypes.default.arrayOf(_propTypes.default.node), _propTypes.default.node]).isRequired,
|
|
33
37
|
className: _propTypes.default.string,
|
|
34
|
-
withPadding: _propTypes.default.bool
|
|
38
|
+
withPadding: _propTypes.default.bool,
|
|
39
|
+
awsWafScriptURL: _propTypes2.awsWafScriptURLPropType
|
|
35
40
|
};
|
|
36
41
|
var _default = exports.default = Main;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.whenAwsWafTokenReady = exports.scriptId = exports.loadAwsWafChallenge = exports.getIntegration = void 0;
|
|
7
|
+
var _integrations = require("./integrations");
|
|
8
|
+
/** The id of the script tag we add, so we can tell whether we've already added it */
|
|
9
|
+
const scriptId = exports.scriptId = "global-nav-aws-waf";
|
|
10
|
+
|
|
11
|
+
// Passing `false` switches the integration off for the rest of the page's life, whichever of the
|
|
12
|
+
// header, footer or main asks next
|
|
13
|
+
let isDisabled = false;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Finds the web ACL integration for the given hostname, if we know of one
|
|
17
|
+
*
|
|
18
|
+
* @param {string} hostname
|
|
19
|
+
* @returns {object | undefined}
|
|
20
|
+
*/
|
|
21
|
+
const getIntegration = function (hostname) {
|
|
22
|
+
return _integrations.integrations.find(_ref => {
|
|
23
|
+
let hosts = _ref.hosts;
|
|
24
|
+
return hosts.indexOf(hostname) > -1;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// How long a caller of `whenAwsWafTokenReady` is held up before it goes ahead without a token
|
|
29
|
+
exports.getIntegration = getIntegration;
|
|
30
|
+
const tokenTimeout = 5000;
|
|
31
|
+
|
|
32
|
+
// The SDK's script tag, whether we added it or the host page loads it for itself
|
|
33
|
+
const findSdkScript = function () {
|
|
34
|
+
return document.getElementById(scriptId) || document.querySelector("script[src*='.awswaf.com/']");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Either the SDK has already run, we've already added it, or the host page loads it for itself
|
|
38
|
+
const isAlreadyOnPage = function () {
|
|
39
|
+
return !!(window.AwsWafIntegration || findSdkScript());
|
|
40
|
+
};
|
|
41
|
+
const getToken = function () {
|
|
42
|
+
const sdk = window.AwsWafIntegration;
|
|
43
|
+
return sdk && typeof sdk.getToken === "function" ? sdk.getToken() : undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Loads the AWS WAF JavaScript SDK, which silently acquires an `aws-waf-token` cookie in the
|
|
48
|
+
* background. The web ACL in front of a service checks for that token, so real users with a token
|
|
49
|
+
* never meet a challenge interstitial. It's the web ACL that keeps bots out, not this script.
|
|
50
|
+
*
|
|
51
|
+
* Safe to call as many times as you like: the script is only ever added once. It's called from the
|
|
52
|
+
* CDN entry point and from the header, footer and main, because no single one of those is on every
|
|
53
|
+
* page of every service.
|
|
54
|
+
*
|
|
55
|
+
* @param {string | false} [scriptURL] An integration URL to use instead of looking one up from the
|
|
56
|
+
* hostname, or `false` to not load the SDK at all
|
|
57
|
+
* @returns {boolean} Whether the script was added by this call
|
|
58
|
+
*/
|
|
59
|
+
const loadAwsWafChallenge = function (scriptURL) {
|
|
60
|
+
if (scriptURL === false) isDisabled = true;
|
|
61
|
+
if (isDisabled || isAlreadyOnPage()) return false;
|
|
62
|
+
const integration = scriptURL ? {
|
|
63
|
+
scriptURL
|
|
64
|
+
} : getIntegration(window.location.hostname);
|
|
65
|
+
|
|
66
|
+
// A listed hostname whose web ACL doesn't exist yet has an empty URL: nothing to load
|
|
67
|
+
if (!integration || !integration.scriptURL) return false;
|
|
68
|
+
|
|
69
|
+
// The SDK reads this as it loads, so it has to be in place first. Leave it be if the host
|
|
70
|
+
// page has already set its own
|
|
71
|
+
if (integration.cookieDomains && !window.awsWafCookieDomainList) {
|
|
72
|
+
window.awsWafCookieDomainList = integration.cookieDomains;
|
|
73
|
+
}
|
|
74
|
+
const script = document.createElement("script");
|
|
75
|
+
script.id = scriptId;
|
|
76
|
+
script.src = integration.scriptURL;
|
|
77
|
+
script.async = true;
|
|
78
|
+
document.head.appendChild(script);
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Waits for the SDK to hold a token, so that a request made straight afterwards carries the
|
|
84
|
+
* `aws-waf-token` cookie. For requests to another service behind a web ACL that can't go through
|
|
85
|
+
* the SDK's own `fetch`, e.g. the NICE Accounts script tag.
|
|
86
|
+
*
|
|
87
|
+
* The promise always resolves and never rejects: straight away when there's no SDK on the page, and
|
|
88
|
+
* after the timeout if the SDK can't get a token. The caller's request goes ahead either way, and
|
|
89
|
+
* it's for the web ACL to decide what to do with one that has no token.
|
|
90
|
+
*
|
|
91
|
+
* @param {number} [timeout] The longest to wait, in milliseconds
|
|
92
|
+
* @returns {Promise<void>}
|
|
93
|
+
*/
|
|
94
|
+
exports.loadAwsWafChallenge = loadAwsWafChallenge;
|
|
95
|
+
const whenAwsWafTokenReady = function () {
|
|
96
|
+
let timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : tokenTimeout;
|
|
97
|
+
return new Promise(function (resolve) {
|
|
98
|
+
const timer = setTimeout(resolve, timeout),
|
|
99
|
+
done = function () {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Look for the SDK on the next tick, not now. A child's effects run before its parent's, so
|
|
105
|
+
// when the account menu asks, the header hasn't added the script yet
|
|
106
|
+
Promise.resolve().then(function () {
|
|
107
|
+
if (window.AwsWafIntegration) return getToken();
|
|
108
|
+
const script = findSdkScript();
|
|
109
|
+
if (!script) return;
|
|
110
|
+
return new Promise(function (loaded) {
|
|
111
|
+
script.addEventListener("load", loaded);
|
|
112
|
+
script.addEventListener("error", loaded);
|
|
113
|
+
}).then(getToken);
|
|
114
|
+
}).then(done, done);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
exports.whenAwsWafTokenReady = whenAwsWafTokenReady;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.integrations = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* The AWS WAF web ACLs that protect NICE services, and the hostnames each one sits in front of.
|
|
9
|
+
*
|
|
10
|
+
* A web ACL issues its own integration URL for the AWS WAF JavaScript SDK, and that URL can't be
|
|
11
|
+
* worked out from the hostname: the ids in it are generated by AWS. So this is a lookup table, with
|
|
12
|
+
* one entry per web ACL rather than one per hostname. Hostnames that share a load balancer (or
|
|
13
|
+
* CloudFront distribution) share a web ACL, and so share an entry.
|
|
14
|
+
*
|
|
15
|
+
* Each entry is:
|
|
16
|
+
* - `hosts` - the exact hostnames served from behind the web ACL, e.g. `["www.nice.org.uk"]`. No
|
|
17
|
+
* wildcards, so list every hostname.
|
|
18
|
+
* - `scriptURL` - the integration URL from 'Application integration' in the AWS WAF console. Ops are
|
|
19
|
+
* issuing the 'Intelligent threat integration' one, which ends in `challenge.js`, e.g.
|
|
20
|
+
* `https://<id>.eu-west-1.sdk.awswaf.com/<id>/<id>/challenge.js`. The 'CAPTCHA integration' one
|
|
21
|
+
* ends in `jsapi.js` and would work just the same; both put `AwsWafIntegration` on `window`.
|
|
22
|
+
* Leave it empty and the entry does nothing, so hostnames can be listed before their web ACL exists.
|
|
23
|
+
* - `cookieDomains` - optional, e.g. `[".nice.org.uk"]`. Widens the `aws-waf-token` cookie from the
|
|
24
|
+
* exact host so one token works across subdomains. Only set this once the same domain is on the
|
|
25
|
+
* web ACL's token domain list, otherwise AWS rejects every token we ask it for.
|
|
26
|
+
*
|
|
27
|
+
* A hostname that isn't listed gets no script, which is what we want for localhost and for any
|
|
28
|
+
* environment with no web ACL in front of it.
|
|
29
|
+
*
|
|
30
|
+
* ! The hostnames below were grouped by which ones resolved to the same address in DNS on
|
|
31
|
+
* ! 2026-10-01, on the basis that a shared address means a shared load balancer. That's a starting
|
|
32
|
+
* ! point, not the truth: confirm the grouping with ops when they hand over each script URL, and
|
|
33
|
+
* ! merge or split entries to match the web ACLs they actually create. Every URL is empty until then.
|
|
34
|
+
*
|
|
35
|
+
* @see https://docs.aws.amazon.com/waf/latest/developerguide/waf-js-challenge-api.html
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const cookieDomains = [".nice.org.uk"];
|
|
39
|
+
const integrations = exports.integrations = [
|
|
40
|
+
// web ACL sandbox (nextweb-sandbox1).
|
|
41
|
+
{
|
|
42
|
+
hosts: ["nextweb.sandbox1-ds.net"],
|
|
43
|
+
scriptURL: "https://e92f72f6da93.eu-west-1.sdk.awswaf.com/e92f72f6da93/6169085d0d0b/challenge.js",
|
|
44
|
+
cookieDomains: [".sandbox1-ds.net"]
|
|
45
|
+
},
|
|
46
|
+
// Live. Main site's load balancer fronts next-web, guidance-web, consultations and the
|
|
47
|
+
// niceorg backend, so they're all covered by this one entry. The bare nice.org.uk redirects to
|
|
48
|
+
// www and is served from a different address, so it's left out: no page ever renders there.
|
|
49
|
+
{
|
|
50
|
+
hosts: ["www.nice.org.uk"],
|
|
51
|
+
scriptURL: "",
|
|
52
|
+
cookieDomains
|
|
53
|
+
}, {
|
|
54
|
+
hosts: ["bnf.nice.org.uk"],
|
|
55
|
+
scriptURL: "",
|
|
56
|
+
cookieDomains
|
|
57
|
+
}, {
|
|
58
|
+
hosts: ["bnfc.nice.org.uk"],
|
|
59
|
+
scriptURL: "",
|
|
60
|
+
cookieDomains
|
|
61
|
+
}, {
|
|
62
|
+
hosts: ["cks.nice.org.uk"],
|
|
63
|
+
scriptURL: "",
|
|
64
|
+
cookieDomains
|
|
65
|
+
}, {
|
|
66
|
+
hosts: ["indev.nice.org.uk"],
|
|
67
|
+
scriptURL: "",
|
|
68
|
+
cookieDomains
|
|
69
|
+
},
|
|
70
|
+
// Alpha
|
|
71
|
+
{
|
|
72
|
+
hosts: ["alpha.nice.org.uk", "alpha-indev.nice.org.uk", "alpha-monitoring.nice.org.uk", "alpha-publications.nice.org.uk"],
|
|
73
|
+
scriptURL: "",
|
|
74
|
+
cookieDomains
|
|
75
|
+
},
|
|
76
|
+
// Beta
|
|
77
|
+
{
|
|
78
|
+
hosts: ["beta.nice.org.uk", "beta-monitoring.nice.org.uk"],
|
|
79
|
+
scriptURL: "",
|
|
80
|
+
cookieDomains
|
|
81
|
+
}, {
|
|
82
|
+
hosts: ["beta-indev.nice.org.uk"],
|
|
83
|
+
scriptURL: "",
|
|
84
|
+
cookieDomains
|
|
85
|
+
}, {
|
|
86
|
+
hosts: ["beta-publications.nice.org.uk"],
|
|
87
|
+
scriptURL: "",
|
|
88
|
+
cookieDomains
|
|
89
|
+
}, {
|
|
90
|
+
hosts: ["beta-bnf.nice.org.uk"],
|
|
91
|
+
scriptURL: "",
|
|
92
|
+
cookieDomains
|
|
93
|
+
}, {
|
|
94
|
+
hosts: ["beta-bnfc.nice.org.uk"],
|
|
95
|
+
scriptURL: "",
|
|
96
|
+
cookieDomains
|
|
97
|
+
}, {
|
|
98
|
+
hosts: ["beta-cks.nice.org.uk"],
|
|
99
|
+
scriptURL: "",
|
|
100
|
+
cookieDomains
|
|
101
|
+
},
|
|
102
|
+
// Test and dev. The main site, indev and publications on test resolve to the same address as
|
|
103
|
+
// the main site and indev on dev, so they look to share one load balancer
|
|
104
|
+
{
|
|
105
|
+
hosts: ["test.nice.org.uk", "test-indev.nice.org.uk", "test-publications.nice.org.uk", "dev.nice.org.uk", "dev-indev.nice.org.uk"],
|
|
106
|
+
scriptURL: "",
|
|
107
|
+
cookieDomains
|
|
108
|
+
}, {
|
|
109
|
+
hosts: ["test-monitoring.nice.org.uk"],
|
|
110
|
+
scriptURL: "",
|
|
111
|
+
cookieDomains
|
|
112
|
+
}, {
|
|
113
|
+
hosts: ["dev-monitoring.nice.org.uk"],
|
|
114
|
+
scriptURL: "",
|
|
115
|
+
cookieDomains
|
|
116
|
+
}, {
|
|
117
|
+
hosts: ["test-bnf.nice.org.uk"],
|
|
118
|
+
scriptURL: "",
|
|
119
|
+
cookieDomains
|
|
120
|
+
}, {
|
|
121
|
+
hosts: ["test-bnfc.nice.org.uk"],
|
|
122
|
+
scriptURL: "",
|
|
123
|
+
cookieDomains
|
|
124
|
+
}, {
|
|
125
|
+
hosts: ["test-cks.nice.org.uk"],
|
|
126
|
+
scriptURL: "",
|
|
127
|
+
cookieDomains
|
|
128
|
+
}];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.awsWafScriptURLPropType = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* PropTypes validator for `awsWafScriptURL`: a script URL, `false` to switch the challenge off, or
|
|
9
|
+
* left out. Not `PropTypes.bool`, because `true` has no meaning and would be loaded as the URL `/true`
|
|
10
|
+
*
|
|
11
|
+
* @see loadAwsWafChallenge
|
|
12
|
+
*/
|
|
13
|
+
const awsWafScriptURLPropType = function (props, propName, componentName) {
|
|
14
|
+
const value = props[propName];
|
|
15
|
+
if (value === undefined || value === false || typeof value === "string") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return new Error("Invalid prop `".concat(propName, "` of value `").concat(value, "` supplied to `").concat(componentName, "`, expected a string, `false` or `undefined`."));
|
|
19
|
+
};
|
|
20
|
+
exports.awsWafScriptURLPropType = awsWafScriptURLPropType;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useAwsWafChallenge = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _awsWaf = require("../aws-waf");
|
|
9
|
+
/**
|
|
10
|
+
* Loads the AWS WAF JavaScript SDK once the component has mounted, so never on the server.
|
|
11
|
+
*
|
|
12
|
+
* @param {string | false} [scriptURL] See `loadAwsWafChallenge`
|
|
13
|
+
*/
|
|
14
|
+
const useAwsWafChallenge = scriptURL => {
|
|
15
|
+
(0, _react.useEffect)(() => {
|
|
16
|
+
(0, _awsWaf.loadAwsWafChallenge)(scriptURL);
|
|
17
|
+
}, [scriptURL]);
|
|
18
|
+
};
|
|
19
|
+
exports.useAwsWafChallenge = useAwsWafChallenge;
|
package/lib/renderer.js
CHANGED
|
@@ -4,11 +4,12 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
-
exports.renderHeader = exports.renderFooter = exports.headerId = exports.footerId = void 0;
|
|
7
|
+
exports.renderHeader = exports.renderFooter = exports.loadAwsWaf = exports.headerId = exports.footerId = void 0;
|
|
8
8
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
9
|
var _client = require("react-dom/client");
|
|
10
10
|
var _Header = _interopRequireDefault(require("./Header"));
|
|
11
11
|
var _Footer = _interopRequireDefault(require("./Footer"));
|
|
12
|
+
var _awsWaf = require("./aws-waf");
|
|
12
13
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
14
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
14
15
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
@@ -31,6 +32,13 @@ const ensureCallback = function (callback) {
|
|
|
31
32
|
}
|
|
32
33
|
return null;
|
|
33
34
|
};
|
|
35
|
+
|
|
36
|
+
// Load here, because header and footer can be switched off in config and the challenge would not then happen
|
|
37
|
+
const loadAwsWaf = function () {
|
|
38
|
+
const config = window.global_nav_config || {};
|
|
39
|
+
return (0, _awsWaf.loadAwsWafChallenge)(config.awsWafScriptURL);
|
|
40
|
+
};
|
|
41
|
+
exports.loadAwsWaf = loadAwsWaf;
|
|
34
42
|
const renderHeader = function () {
|
|
35
43
|
const config = window.global_nav_config || {};
|
|
36
44
|
if (config.header !== false) {
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -66,8 +66,16 @@ declare module "@nice-digital/global-nav" {
|
|
|
66
66
|
links: Array<Link>;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The header, footer and main each load the AWS WAF JavaScript SDK if the page's hostname is
|
|
71
|
+
* behind a web ACL that Global Nav knows about. Pass an integration URL to use that instead,
|
|
72
|
+
* or `false` to not load the SDK at all e.g. because your app loads it for itself.
|
|
73
|
+
*/
|
|
74
|
+
export type AwsWafScriptURL = string | false;
|
|
75
|
+
|
|
69
76
|
export type HeaderProps = {
|
|
70
77
|
service?: Service;
|
|
78
|
+
awsWafScriptURL?: AwsWafScriptURL;
|
|
71
79
|
skipLinkId?: string;
|
|
72
80
|
search?: false | SearchProps;
|
|
73
81
|
auth?: NiceAccountsProviderProps | IdamProviderProps | false;
|
|
@@ -80,6 +88,7 @@ declare module "@nice-digital/global-nav" {
|
|
|
80
88
|
|
|
81
89
|
export type FooterProps = {
|
|
82
90
|
service?: Service;
|
|
91
|
+
awsWafScriptURL?: AwsWafScriptURL;
|
|
83
92
|
};
|
|
84
93
|
|
|
85
94
|
export interface MainProps {
|
|
@@ -90,6 +99,7 @@ declare module "@nice-digital/global-nav" {
|
|
|
90
99
|
/** Additional classnames for the main element. */
|
|
91
100
|
className?: string;
|
|
92
101
|
withPadding?: boolean;
|
|
102
|
+
awsWafScriptURL?: AwsWafScriptURL;
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
const Header: React.FC<HeaderProps>;
|