From 15b2767df938a7e13af96252f451928ef562288a Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:25:50 +0100 Subject: [PATCH 01/28] Authentication plugin https://eaflood.atlassian.net/browse/WATER-4085 We implement a plugin to authenticate users. This is purely intended for use by frontend routes, ie. those where a request is passed from the UI to the system. From 2347f01a0be6b09aff25a94a1794f56d10d39cac Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:29:17 +0100 Subject: [PATCH 02/28] Install `@hapi/cookie` package --- package-lock.json | 325 +++++++++------------------------------------- package.json | 1 + 2 files changed, 60 insertions(+), 266 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87adb859a0..beea453c56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@airbrake/node": "^2.1.8", "@aws-sdk/client-s3": "^3.379.1", "@hapi/boom": "^10.0.1", + "@hapi/cookie": "^12.0.1", "@hapi/hapi": "^21.3.2", "@hapi/inert": "^7.1.0", "@hapi/vision": "^7.0.2", @@ -1265,25 +1266,6 @@ "integrity": "sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==", "dev": true }, - "node_modules/@hapi/bossy/node_modules/@hapi/topo": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.0.tgz", - "integrity": "sha512-aorJvN1Q1n5xrZuA50Z4X6adI6VAM2NalIVm46ALL9LUvdoqhof3JPY69jdJH8asM3PsWr2SUVYzp57EqUP41A==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^10.0.0" - } - }, - "node_modules/@hapi/bossy/node_modules/@hapi/validate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.0.tgz", - "integrity": "sha512-w5m8MvBgqGndbMIB+AWmXTb8CLtF1DlIxbnbAHNAo7aFuNQuI1Ywc2e0zDLK5fbFXDoqRzNrHnC7JjNJ+hDigw==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^10.0.0", - "@hapi/topo": "^6.0.0" - } - }, "node_modules/@hapi/bounce": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.1.tgz", @@ -1345,23 +1327,6 @@ "node": ">=14.0.0" } }, - "node_modules/@hapi/catbox/node_modules/@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, - "node_modules/@hapi/catbox/node_modules/@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "dependencies": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - }, "node_modules/@hapi/code": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/@hapi/code/-/code-9.0.3.tgz", @@ -1379,6 +1344,17 @@ "@hapi/boom": "^10.0.0" } }, + "node_modules/@hapi/cookie": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@hapi/cookie/-/cookie-12.0.1.tgz", + "integrity": "sha512-TpykARUIgTBvgbsgtYe5CrM/7XBGms2/22JD3N6dzct6bG1vcOC6pH1JWIos1O5zvQnyDSJ2pnaFk+DToHkAng==", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, "node_modules/@hapi/eslint-plugin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@hapi/eslint-plugin/-/eslint-plugin-6.0.0.tgz", @@ -1456,15 +1432,6 @@ "@hapi/hoek": "^11.0.2" } }, - "node_modules/@hapi/hapi/node_modules/@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "dependencies": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - }, "node_modules/@hapi/heavy": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz", @@ -1475,23 +1442,6 @@ "@hapi/validate": "^2.0.1" } }, - "node_modules/@hapi/heavy/node_modules/@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, - "node_modules/@hapi/heavy/node_modules/@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "dependencies": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - }, "node_modules/@hapi/hoek": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.2.tgz", @@ -1510,23 +1460,6 @@ "lru-cache": "^7.14.1" } }, - "node_modules/@hapi/inert/node_modules/@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, - "node_modules/@hapi/inert/node_modules/@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "dependencies": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - }, "node_modules/@hapi/inert/node_modules/lru-cache": { "version": "7.14.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", @@ -1621,23 +1554,6 @@ "@hapi/validate": "^2.0.1" } }, - "node_modules/@hapi/shot/node_modules/@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, - "node_modules/@hapi/shot/node_modules/@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "dependencies": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - }, "node_modules/@hapi/somever": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz", @@ -1692,23 +1608,6 @@ "@hapi/hoek": "^11.0.2" } }, - "node_modules/@hapi/statehood/node_modules/@hapi/topo": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", - "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, - "node_modules/@hapi/statehood/node_modules/@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "dependencies": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - }, "node_modules/@hapi/subtext": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.0.tgz", @@ -1736,6 +1635,23 @@ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" }, + "node_modules/@hapi/validate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", + "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/topo": "^6.0.1" + } + }, + "node_modules/@hapi/validate/node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@hapi/vise": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz", @@ -1755,23 +1671,6 @@ "@hapi/validate": "^2.0.1" } }, - "node_modules/@hapi/vision/node_modules/@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, - "node_modules/@hapi/vision/node_modules/@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "dependencies": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - }, "node_modules/@hapi/wreck": { "version": "18.0.1", "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.0.1.tgz", @@ -8681,25 +8580,6 @@ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-10.0.1.tgz", "integrity": "sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==", "dev": true - }, - "@hapi/topo": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.0.tgz", - "integrity": "sha512-aorJvN1Q1n5xrZuA50Z4X6adI6VAM2NalIVm46ALL9LUvdoqhof3JPY69jdJH8asM3PsWr2SUVYzp57EqUP41A==", - "dev": true, - "requires": { - "@hapi/hoek": "^10.0.0" - } - }, - "@hapi/validate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.0.tgz", - "integrity": "sha512-w5m8MvBgqGndbMIB+AWmXTb8CLtF1DlIxbnbAHNAo7aFuNQuI1Ywc2e0zDLK5fbFXDoqRzNrHnC7JjNJ+hDigw==", - "dev": true, - "requires": { - "@hapi/hoek": "^10.0.0", - "@hapi/topo": "^6.0.0" - } } } }, @@ -8751,23 +8631,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.0.tgz", "integrity": "sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A==" - }, - "@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "requires": { - "@hapi/hoek": "^11.0.2" - } - }, - "@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "requires": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } } } }, @@ -8797,6 +8660,17 @@ "@hapi/boom": "^10.0.0" } }, + "@hapi/cookie": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@hapi/cookie/-/cookie-12.0.1.tgz", + "integrity": "sha512-TpykARUIgTBvgbsgtYe5CrM/7XBGms2/22JD3N6dzct6bG1vcOC6pH1JWIos1O5zvQnyDSJ2pnaFk+DToHkAng==", + "requires": { + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, "@hapi/eslint-plugin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@hapi/eslint-plugin/-/eslint-plugin-6.0.0.tgz", @@ -8856,15 +8730,6 @@ "requires": { "@hapi/hoek": "^11.0.2" } - }, - "@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "requires": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } } } }, @@ -8876,25 +8741,6 @@ "@hapi/boom": "^10.0.1", "@hapi/hoek": "^11.0.2", "@hapi/validate": "^2.0.1" - }, - "dependencies": { - "@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "requires": { - "@hapi/hoek": "^11.0.2" - } - }, - "@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "requires": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - } } }, "@hapi/hoek": { @@ -8915,23 +8761,6 @@ "lru-cache": "^7.14.1" }, "dependencies": { - "@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "requires": { - "@hapi/hoek": "^11.0.2" - } - }, - "@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "requires": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - }, "lru-cache": { "version": "7.14.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", @@ -9010,25 +8839,6 @@ "requires": { "@hapi/hoek": "^11.0.2", "@hapi/validate": "^2.0.1" - }, - "dependencies": { - "@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "requires": { - "@hapi/hoek": "^11.0.2" - } - }, - "@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "requires": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - } } }, "@hapi/somever": { @@ -9081,23 +8891,6 @@ "@hapi/cryptiles": "^6.0.1", "@hapi/hoek": "^11.0.2" } - }, - "@hapi/topo": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", - "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", - "requires": { - "@hapi/hoek": "^11.0.2" - } - }, - "@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "requires": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } } } }, @@ -9130,6 +8923,25 @@ } } }, + "@hapi/validate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", + "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", + "requires": { + "@hapi/hoek": "^11.0.2", + "@hapi/topo": "^6.0.1" + }, + "dependencies": { + "@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "requires": { + "@hapi/hoek": "^11.0.2" + } + } + } + }, "@hapi/vise": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz", @@ -9147,25 +8959,6 @@ "@hapi/bounce": "^3.0.1", "@hapi/hoek": "^11.0.2", "@hapi/validate": "^2.0.1" - }, - "dependencies": { - "@hapi/topo": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.1.tgz", - "integrity": "sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==", - "requires": { - "@hapi/hoek": "^11.0.2" - } - }, - "@hapi/validate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", - "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", - "requires": { - "@hapi/hoek": "^11.0.2", - "@hapi/topo": "^6.0.1" - } - } } }, "@hapi/wreck": { diff --git a/package.json b/package.json index 1c706f775d..e27f355c6e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@airbrake/node": "^2.1.8", "@aws-sdk/client-s3": "^3.379.1", "@hapi/boom": "^10.0.1", + "@hapi/cookie": "^12.0.1", "@hapi/hapi": "^21.3.2", "@hapi/inert": "^7.1.0", "@hapi/vision": "^7.0.2", From 535fa43218cf902d19b9eafeac9e302966362381 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:52:39 +0100 Subject: [PATCH 03/28] First pass at plugin We create a plugin that sets up the authentication strategy and creates a route to test it. We do not yet set routes to be authenticated by default; we can initially check it by uncommenting `server.auth.default('session')` in the plugin, which will enable authentication for all routes (which will break everything else as practically every request to the system is unauthenticated) --- app/plugins/authentication.plugin.js | 62 ++++++++++++++++++++++++++++ app/server.js | 2 + 2 files changed, 64 insertions(+) create mode 100644 app/plugins/authentication.plugin.js diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js new file mode 100644 index 0000000000..95ae9e736f --- /dev/null +++ b/app/plugins/authentication.plugin.js @@ -0,0 +1,62 @@ +'use strict' + +/** + * Plugin to authenticate users + * @module AuthenticationPlugin + */ + +// TODO: use a config file instead +require('dotenv').config() + +const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 + +/** + * Some of our routes serve up pages, and are intended to be called from the UI rather than being hit directly. We do + * not rely on the UI authenticating requests first as there are some pages which we do not wish to have authentication + * on (eg. service status pages). We therefore authenticate pages on the System side, relying on an authenticated cookie + * being passed on by the UI. + */ + +const AuthenticationPlugin = { + name: 'authentication', + register: async (server, _options) => { + // TODO: Set @hapi/cookie as a dependency instead + await server.register(require('@hapi/cookie')) + + server.auth.strategy('session', 'cookie', { + cookie: { + name: 'sid', + password: process.env.COOKIE_SECRET, + isSecure: false, + isSameSite: 'Lax', + ttl: TWO_HOURS_IN_MS, + isHttpOnly: true + }, + redirectTo: '/signIn', + validate: async (_request, session) => { + const { userId } = session + // TODO: Look up userId in the IDM to ensure user exists. Also get user role and add to `credentials` + return { isValid: !!userId, credentials: { userId } } + } + }) + + server.route({ + method: 'GET', + path: '/auth-test', + handler: (request, _h) => { + return { auth: request.auth } + }, + options: { + description: 'Test that authentication is working', + app: { excludeFromProd: true } + } + }) + + // TODO: confirm if we want to use this to enable user authentication on ALL routes by default, so we would need to + // add `auth: false` to the config of each route that doesn't need authentication + // + // server.auth.default('session') + } +} + +module.exports = AuthenticationPlugin diff --git a/app/server.js b/app/server.js index bc1795464c..5020abc9e5 100644 --- a/app/server.js +++ b/app/server.js @@ -3,6 +3,7 @@ const Hapi = require('@hapi/hapi') const AirbrakePlugin = require('./plugins/airbrake.plugin.js') +const AuthenticationPlugin = require('./plugins/authentication.plugin.js') const BlippPlugin = require('./plugins/blipp.plugin.js') const ChargingModuleTokenCachePlugin = require('./plugins/charging-module-token-cache.plugin.js') const ErrorPagesPlugin = require('./plugins/error-pages.plugin.js') @@ -20,6 +21,7 @@ const registerPlugins = async (server) => { // Register the remaining plugins await server.register(StopPlugin) await server.register(require('@hapi/inert')) + await server.register(AuthenticationPlugin) await server.register(RouterPlugin) await server.register(HapiPinoPlugin()) await server.register(AirbrakePlugin) From b6cf4a1b5bb35d33cfd0aff5e2bb8d01480a9331 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:25:04 +0100 Subject: [PATCH 04/28] Add TODO --- app/plugins/authentication.plugin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 95ae9e736f..d2bed47d56 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -42,6 +42,7 @@ const AuthenticationPlugin = { server.route({ method: 'GET', + // TODO: pick a better path path: '/auth-test', handler: (request, _h) => { return { auth: request.auth } From 041c3be449d070fd359f90fa985ab6c7f88102f3 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:44:14 +0100 Subject: [PATCH 05/28] Use `server.dependency()` in plugin Calling `server.dependency()` in a plugin allows to specify which we will wait to be registeted, and some code to be executed once it's registered. The rest of the plugin code is executed while waiting. In this case, we use `server.dependency()` to wait until `@hapi/cookie` is registered before we set up the authentication strategy, but immediately create the test route as this isn't dependent on anything else. All of this ensures that the order of the plugins in our `registerPlugins()` function isn't important, ie. we have no issues if we register them in the "wrong" order. To demonstrate this, we deliberately register `@hapi/cookie` _after_ `AuthenticationPlugin` --- app/plugins/authentication.plugin.js | 46 ++++++++++++++-------------- app/server.js | 1 + 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index d2bed47d56..864af0f5dd 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -20,24 +20,29 @@ const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 const AuthenticationPlugin = { name: 'authentication', register: async (server, _options) => { - // TODO: Set @hapi/cookie as a dependency instead - await server.register(require('@hapi/cookie')) - - server.auth.strategy('session', 'cookie', { - cookie: { - name: 'sid', - password: process.env.COOKIE_SECRET, - isSecure: false, - isSameSite: 'Lax', - ttl: TWO_HOURS_IN_MS, - isHttpOnly: true - }, - redirectTo: '/signIn', - validate: async (_request, session) => { - const { userId } = session - // TODO: Look up userId in the IDM to ensure user exists. Also get user role and add to `credentials` - return { isValid: !!userId, credentials: { userId } } - } + // We wait for @hapi/cookie to be registered before setting up the authentication strategy + server.dependency('@hapi/cookie', async (server) => { + server.auth.strategy('session', 'cookie', { + cookie: { + name: 'sid', + password: process.env.COOKIE_SECRET, + isSecure: false, + isSameSite: 'Lax', + ttl: TWO_HOURS_IN_MS, + isHttpOnly: true + }, + redirectTo: '/signIn', + validate: async (_request, session) => { + const { userId } = session + // TODO: Look up userId in the IDM to ensure user exists. Also get user role and add to `credentials` + return { isValid: !!userId, credentials: { userId } } + } + }) + + // TODO: confirm if we want to use this to enable user authentication on ALL routes by default, so we would need + // to add `auth: false` to the config of each route that doesn't need authentication + // + // server.auth.default('session') }) server.route({ @@ -52,11 +57,6 @@ const AuthenticationPlugin = { app: { excludeFromProd: true } } }) - - // TODO: confirm if we want to use this to enable user authentication on ALL routes by default, so we would need to - // add `auth: false` to the config of each route that doesn't need authentication - // - // server.auth.default('session') } } diff --git a/app/server.js b/app/server.js index 5020abc9e5..ac0dc4eedf 100644 --- a/app/server.js +++ b/app/server.js @@ -22,6 +22,7 @@ const registerPlugins = async (server) => { await server.register(StopPlugin) await server.register(require('@hapi/inert')) await server.register(AuthenticationPlugin) + await server.register(require('@hapi/cookie')) await server.register(RouterPlugin) await server.register(HapiPinoPlugin()) await server.register(AirbrakePlugin) From c5c5d55b14d740a408531ec6fc8a07bc01ebbc3b Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:55:43 +0100 Subject: [PATCH 06/28] Directly add authentication to route To make it easier to see what's going on with authentication, we directly add our authentication to our test route --- app/plugins/authentication.plugin.js | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 864af0f5dd..243ec14344 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -39,24 +39,25 @@ const AuthenticationPlugin = { } }) - // TODO: confirm if we want to use this to enable user authentication on ALL routes by default, so we would need - // to add `auth: false` to the config of each route that doesn't need authentication + // We set up our route in the dependency callback as we can't set authentcation before the strategy is registered + server.route({ + method: 'GET', + // TODO: pick a better path + path: '/auth-test', + handler: (request, _h) => { + return { auth: request.auth } + }, + options: { + description: 'Test that authentication is working', + app: { excludeFromProd: true }, + auth: 'session' + } + }) + + // NOTE: If we wa // // server.auth.default('session') }) - - server.route({ - method: 'GET', - // TODO: pick a better path - path: '/auth-test', - handler: (request, _h) => { - return { auth: request.auth } - }, - options: { - description: 'Test that authentication is working', - app: { excludeFromProd: true } - } - }) } } From 8220b4489e8f1e04f83622d84928424ffc03f561 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:00:52 +0100 Subject: [PATCH 07/28] Remove comment --- app/plugins/authentication.plugin.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 243ec14344..5d6b4d3a54 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -53,10 +53,6 @@ const AuthenticationPlugin = { auth: 'session' } }) - - // NOTE: If we wa - // - // server.auth.default('session') }) } } From 6e6fe17d674d6714532a72c05b93a7f5de7677cf Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 22 Aug 2023 15:27:51 +0100 Subject: [PATCH 08/28] Add `COOKIE_SECRET` to CI workflow Our unit tests are failing because we don't yet have a `COOKIE_SECRET` environment variable. We therefore add it to our `ci.yml` workflow --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf3d0e965f..6fb481a84e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: POSTGRES_DB: wabs_system_test POSTGRES_DB_TEST: wabs_system_test ENVIRONMENT: dev + COOKIE_SECRET: cookiesecret # Service containers to run with `runner-job` From be3915d04c331c630da48f559aafc5457485a9f2 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:27:04 +0100 Subject: [PATCH 09/28] Create `AuthenticationConfig` file We originally read the `COOKIE_SECRET` environment variable directly so that we could quickly get a working plugin up and running. Now that we have it working, we create `authentication.config.js` in line with our usual pattern for accessing env vars. Note that we only set the password in the env vars, not the cookie name, ttl etc. We do this because only the password is configurable in our other repos, so there's little point making other things configurable in this repo. --- app/plugins/authentication.plugin.js | 5 ++--- config/authentication.config.js | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 config/authentication.config.js diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 5d6b4d3a54..d06556d71f 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -5,8 +5,7 @@ * @module AuthenticationPlugin */ -// TODO: use a config file instead -require('dotenv').config() +const AuthenticationConfigConfig = require('../../config/authentication.config.js') const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 @@ -25,7 +24,7 @@ const AuthenticationPlugin = { server.auth.strategy('session', 'cookie', { cookie: { name: 'sid', - password: process.env.COOKIE_SECRET, + password: AuthenticationConfigConfig.password, isSecure: false, isSameSite: 'Lax', ttl: TWO_HOURS_IN_MS, diff --git a/config/authentication.config.js b/config/authentication.config.js new file mode 100644 index 0000000000..621ba792e0 --- /dev/null +++ b/config/authentication.config.js @@ -0,0 +1,16 @@ +'use strict' + +/** + * Config values used for cookie authentication + * @module AuthenticationConfig + */ + +// We require dotenv directly in each config file to support unit tests that depend on this this subset of config. +// Requiring dotenv in multiple places has no effect on the app when running for real. +require('dotenv').config() + +const config = { + password: process.env.COOKIE_SECRET +} + +module.exports = config From 891e8c22ca65b57986d6017391913df1a38405d8 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Thu, 24 Aug 2023 15:24:05 +0100 Subject: [PATCH 10/28] Create db migrations --- .../20230823150507_create-idm-schema.js | 13 +++++++ .../20230823150600_create-idm-users.js | 39 +++++++++++++++++++ .../20230824092452_create-idm-group-roles.js | 28 +++++++++++++ .../20230824092657_create-idm-groups.js | 29 ++++++++++++++ .../20230824092939_create-idm-roles.js | 29 ++++++++++++++ .../20230824093122_create-idm-user-groups.js | 28 +++++++++++++ .../20230824142221_create-idm-user-roles.js | 28 +++++++++++++ 7 files changed, 194 insertions(+) create mode 100644 db/migrations/20230823150507_create-idm-schema.js create mode 100644 db/migrations/20230823150600_create-idm-users.js create mode 100644 db/migrations/20230824092452_create-idm-group-roles.js create mode 100644 db/migrations/20230824092657_create-idm-groups.js create mode 100644 db/migrations/20230824092939_create-idm-roles.js create mode 100644 db/migrations/20230824093122_create-idm-user-groups.js create mode 100644 db/migrations/20230824142221_create-idm-user-roles.js diff --git a/db/migrations/20230823150507_create-idm-schema.js b/db/migrations/20230823150507_create-idm-schema.js new file mode 100644 index 0000000000..2d4dd578f7 --- /dev/null +++ b/db/migrations/20230823150507_create-idm-schema.js @@ -0,0 +1,13 @@ +'use strict' + +exports.up = function (knex) { + return knex.raw(` + CREATE SCHEMA IF NOT EXISTS "idm"; + `) +} + +exports.down = function (knex) { + return knex.raw(` + DROP SCHEMA IF EXISTS "idm"; + `) +} diff --git a/db/migrations/20230823150600_create-idm-users.js b/db/migrations/20230823150600_create-idm-users.js new file mode 100644 index 0000000000..e762ba52fa --- /dev/null +++ b/db/migrations/20230823150600_create-idm-users.js @@ -0,0 +1,39 @@ +'use strict' + +const tableName = 'users' + +exports.up = function (knex) { + return knex + .schema + .withSchema('idm') + .createTable(tableName, (table) => { + // Primary Key + table.integer('user_id').primary().notNullable() + + // Data + table.string('user_name').notNullable() + table.string('password').notNullable() + table.jsonb('user_data') + table.string('reset_guid') + table.bigint('reset_required') + + table.timestamp('last_login') + table.bigint('bad_logins') + table.string('application') // TODO: confirm what application_name datatype is + table.jsonb('role') + table.string('external_id') + table.timestamp('reset_guid_date_created') + table.boolean('enabled').notNullable().defaultTo(true) + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + table.timestamp('date_updated', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('idm') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/20230824092452_create-idm-group-roles.js b/db/migrations/20230824092452_create-idm-group-roles.js new file mode 100644 index 0000000000..ba9fd4423d --- /dev/null +++ b/db/migrations/20230824092452_create-idm-group-roles.js @@ -0,0 +1,28 @@ +'use strict' + +const tableName = 'group_roles' + +exports.up = function (knex) { + return knex + .schema + .withSchema('idm') + .createTable(tableName, (table) => { + // Primary Key + table.string('user_id').primary().notNullable() + + // Data + table.string('group_id').notNullable() + table.string('role_id').notNullable() + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + table.timestamp('date_updated', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('idm') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/20230824092657_create-idm-groups.js b/db/migrations/20230824092657_create-idm-groups.js new file mode 100644 index 0000000000..bca137bb42 --- /dev/null +++ b/db/migrations/20230824092657_create-idm-groups.js @@ -0,0 +1,29 @@ +'use strict' + +const tableName = 'groups' + +exports.up = function (knex) { + return knex + .schema + .withSchema('idm') + .createTable(tableName, (table) => { + // Primary Key + table.string('group_id').primary().notNullable() + + // Data + table.string('application') // TODO: confirm what application_name datatype is + table.string('group').notNullable() + table.string('description').notNullable() + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + table.timestamp('date_updated', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('idm') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/20230824092939_create-idm-roles.js b/db/migrations/20230824092939_create-idm-roles.js new file mode 100644 index 0000000000..7e00706dd7 --- /dev/null +++ b/db/migrations/20230824092939_create-idm-roles.js @@ -0,0 +1,29 @@ +'use strict' + +const tableName = 'roles' + +exports.up = function (knex) { + return knex + .schema + .withSchema('idm') + .createTable(tableName, (table) => { + // Primary Key + table.integer('role_id').primary() + + // Data + table.string('application') // TODO: confirm what application_name datatype is + table.string('role').notNullable() + table.string('description').notNullable() + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + table.timestamp('date_updated', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('idm') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/20230824093122_create-idm-user-groups.js b/db/migrations/20230824093122_create-idm-user-groups.js new file mode 100644 index 0000000000..9a358b9973 --- /dev/null +++ b/db/migrations/20230824093122_create-idm-user-groups.js @@ -0,0 +1,28 @@ +'use strict' + +const tableName = 'user_groups' + +exports.up = function (knex) { + return knex + .schema + .withSchema('idm') + .createTable(tableName, (table) => { + // Primary Key + table.string('user_group_id').primary().notNullable() + + // Data + table.integer('user_id').notNullable() + table.string('group_id').notNullable() + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + table.timestamp('date_updated', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('idm') + .dropTableIfExists(tableName) +} diff --git a/db/migrations/20230824142221_create-idm-user-roles.js b/db/migrations/20230824142221_create-idm-user-roles.js new file mode 100644 index 0000000000..ca366f9b4c --- /dev/null +++ b/db/migrations/20230824142221_create-idm-user-roles.js @@ -0,0 +1,28 @@ +'use strict' + +const tableName = 'user_roles' + +exports.up = function (knex) { + return knex + .schema + .withSchema('idm') + .createTable(tableName, (table) => { + // Primary Key + table.string('user_role_id').primary().notNullable() + + // Data + table.integer('user_id').notNullable() + table.string('role_id').notNullable() + + // Legacy timestamps + table.timestamp('date_created', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + table.timestamp('date_updated', { useTz: false }).notNullable().defaultTo(knex.fn.now()) + }) +} + +exports.down = function (knex) { + return knex + .schema + .withSchema('idm') + .dropTableIfExists(tableName) +} From 70758e13dfba933c9d5daa6ad4b91db87585df73 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Fri, 25 Aug 2023 12:07:41 +0100 Subject: [PATCH 11/28] Create models --- app/models/idm/group-role.model.js | 27 +++++++++++++++++++++++ app/models/idm/group.model.js | 27 +++++++++++++++++++++++ app/models/idm/idm-base.model.js | 16 ++++++++++++++ app/models/idm/role.model.js | 27 +++++++++++++++++++++++ app/models/idm/user-role.model.js | 27 +++++++++++++++++++++++ app/models/idm/user.model.js | 35 ++++++++++++++++++++++++++++++ 6 files changed, 159 insertions(+) create mode 100644 app/models/idm/group-role.model.js create mode 100644 app/models/idm/group.model.js create mode 100644 app/models/idm/idm-base.model.js create mode 100644 app/models/idm/role.model.js create mode 100644 app/models/idm/user-role.model.js create mode 100644 app/models/idm/user.model.js diff --git a/app/models/idm/group-role.model.js b/app/models/idm/group-role.model.js new file mode 100644 index 0000000000..e46dd2cdbe --- /dev/null +++ b/app/models/idm/group-role.model.js @@ -0,0 +1,27 @@ +'use strict' + +/** + * Model for group role + * @module GroupRoleModel + */ + +const IDMBaseModel = require('./idm-base.model.js') + +class GroupRoleModel extends IDMBaseModel { + static get tableName () { + return 'groupRoles' + } + + static get idColumn () { + return 'groupRoleId' + } + + static get translations () { + return [ + { database: 'dateCreated', model: 'createdAt' }, + { database: 'dateUpdated', model: 'updatedAt' } + ] + } +} + +module.exports = GroupRoleModel diff --git a/app/models/idm/group.model.js b/app/models/idm/group.model.js new file mode 100644 index 0000000000..f0d6e08166 --- /dev/null +++ b/app/models/idm/group.model.js @@ -0,0 +1,27 @@ +'use strict' + +/** + * Model for group + * @module GroupModel + */ + +const IDMBaseModel = require('./idm-base.model.js') + +class GroupModel extends IDMBaseModel { + static get tableName () { + return 'groups' + } + + static get idColumn () { + return 'groupId' + } + + static get translations () { + return [ + { database: 'dateCreated', model: 'createdAt' }, + { database: 'dateUpdated', model: 'updatedAt' } + ] + } +} + +module.exports = GroupModel diff --git a/app/models/idm/idm-base.model.js b/app/models/idm/idm-base.model.js new file mode 100644 index 0000000000..27cd93aef5 --- /dev/null +++ b/app/models/idm/idm-base.model.js @@ -0,0 +1,16 @@ +'use strict' + +/** + * Base class for all models based on the legacy 'idm' schema + * @module IDMBaseModel + */ + +const LegacyBaseModel = require('../legacy-base.model.js') + +class IDMBaseModel extends LegacyBaseModel { + static get schema () { + return 'idm' + } +} + +module.exports = IDMBaseModel diff --git a/app/models/idm/role.model.js b/app/models/idm/role.model.js new file mode 100644 index 0000000000..2c45b2da90 --- /dev/null +++ b/app/models/idm/role.model.js @@ -0,0 +1,27 @@ +'use strict' + +/** + * Model for role + * @module RoleModel + */ + +const IDMBaseModel = require('./idm-base.model.js') + +class RoleModel extends IDMBaseModel { + static get tableName () { + return 'roles' + } + + static get idColumn () { + return 'roleId' + } + + static get translations () { + return [ + { database: 'dateCreated', model: 'createdAt' }, + { database: 'dateUpdated', model: 'updatedAt' } + ] + } +} + +module.exports = RoleModel diff --git a/app/models/idm/user-role.model.js b/app/models/idm/user-role.model.js new file mode 100644 index 0000000000..990b78d404 --- /dev/null +++ b/app/models/idm/user-role.model.js @@ -0,0 +1,27 @@ +'use strict' + +/** + * Model for user role + * @module UserRoleModel + */ + +const IDMBaseModel = require('./idm-base.model.js') + +class UserRoleModel extends IDMBaseModel { + static get tableName () { + return 'userRoles' + } + + static get idColumn () { + return 'userRoleId' + } + + static get translations () { + return [ + { database: 'dateCreated', model: 'createdAt' }, + { database: 'dateUpdated', model: 'updatedAt' } + ] + } +} + +module.exports = UserRoleModel diff --git a/app/models/idm/user.model.js b/app/models/idm/user.model.js new file mode 100644 index 0000000000..6ecd244f08 --- /dev/null +++ b/app/models/idm/user.model.js @@ -0,0 +1,35 @@ +'use strict' + +/** + * Model for user + * @module UserModel + */ + +const IDMBaseModel = require('./idm-base.model.js') + +class UserModel extends IDMBaseModel { + static get tableName () { + return 'users' + } + + static get idColumn () { + return 'userId' + } + + static get translations () { + return [ + { database: 'dateCreated', model: 'createdAt' }, + { database: 'dateUpdated', model: 'updatedAt' } + ] + } + + // Defining which fields contain json allows us to insert an object without needing to stringify it first + static get jsonAttributes () { + return [ + 'userData', + 'role' + ] + } +} + +module.exports = UserModel From 7091643466cafdf6824a573616e15fdf1dd52d8e Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:24:55 +0100 Subject: [PATCH 12/28] Comment typo --- app/plugins/authentication.plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index d06556d71f..d484b235c6 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -38,7 +38,7 @@ const AuthenticationPlugin = { } }) - // We set up our route in the dependency callback as we can't set authentcation before the strategy is registered + // We set up our route in the dependency callback as we can't set authentication before the strategy is registered server.route({ method: 'GET', // TODO: pick a better path From 025005bdf0194487a988f085b3c05ac88d4adedc Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:30:43 +0100 Subject: [PATCH 13/28] Rename required config --- app/plugins/authentication.plugin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index d484b235c6..ad47c0be3e 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -5,7 +5,7 @@ * @module AuthenticationPlugin */ -const AuthenticationConfigConfig = require('../../config/authentication.config.js') +const AuthenticationConfig = require('../../config/authentication.config.js') const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 @@ -24,7 +24,7 @@ const AuthenticationPlugin = { server.auth.strategy('session', 'cookie', { cookie: { name: 'sid', - password: AuthenticationConfigConfig.password, + password: AuthenticationConfig.password, isSecure: false, isSameSite: 'Lax', ttl: TWO_HOURS_IN_MS, From 1ac0878e751ae0d2388d87821ed859f15b7730c3 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:59:35 +0100 Subject: [PATCH 14/28] Add `userFound` flag to `FetchUserRolesAndGroupsService` We realise that our plugin needs to validate whether or not the user exists, and rather than add additional logic to the plugin to check if the user was found (for example, by checking if the response from `FetchUserRolesAndGroupsService` contains any roles in the roles array) we instead add an explicit `userFound` flag to the service. --- .../idm/fetch-user-roles-and-groups.service.js | 5 ++++- .../fetch-user-roles-and-groups.service.test.js | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/services/idm/fetch-user-roles-and-groups.service.js b/app/services/idm/fetch-user-roles-and-groups.service.js index 29c3a802f3..3d3b26568a 100644 --- a/app/services/idm/fetch-user-roles-and-groups.service.js +++ b/app/services/idm/fetch-user-roles-and-groups.service.js @@ -16,11 +16,12 @@ const UserModel = require('../../models/idm/user.model.js') * This service looks up the user in the `idm` (identity management) schema and returns a combined array of all roles * (deduped in case the user is given the same role multiple times; for example, by being assigned a role directly, then * later added to a group which also includes that role). It also returns an array of groups that the user is a member - * of. + * of, along with `userFound` to explicitly indicate whether or not the user id exists. * * @param {Number} userId The user id to get roles and groups for * * @returns {Object} result The resulting roles and groups + * @returns {Boolean} result.userFound Returns `true` if the user could be found in the `users` table * @returns {RoleModel[]} result.roles An array of RoleModel objects representing the roles the user has * @returns {GroupModel[]} result.groups An array of GroupModel objects representing the groups the user is a member of */ @@ -31,6 +32,7 @@ async function go (userId) { if (!user) { return { + userFound: false, roles: [], groups: [] } @@ -41,6 +43,7 @@ async function go (userId) { const combinedAndDedupedRoles = _combineAndDedupeRoles([...roles, ...rolesFromGroups]) return { + userFound: true, roles: combinedAndDedupedRoles, groups } diff --git a/test/services/idm/fetch-user-roles-and-groups.service.test.js b/test/services/idm/fetch-user-roles-and-groups.service.test.js index a56ceefa39..cde03b906d 100644 --- a/test/services/idm/fetch-user-roles-and-groups.service.test.js +++ b/test/services/idm/fetch-user-roles-and-groups.service.test.js @@ -19,7 +19,7 @@ const UserRoleHelper = require('../../support/helpers/idm/user-role.helper.js') // Thing under test const FetchUserRolesAndGroupsService = require('../../../app/services/idm/fetch-user-roles-and-groups.service.js') -describe('Fetch User Roles And Groups service', () => { +describe.only('Fetch User Roles And Groups service', () => { let testRoleForUser let testRoleForGroup let testUser @@ -47,6 +47,12 @@ describe('Fetch User Roles And Groups service', () => { }) describe('when the user exists', () => { + it('returns `true` for `userFound`', async () => { + const result = await FetchUserRolesAndGroupsService.go(testUser.userId) + + expect(result.userFound).to.be.true() + }) + it("returns the user's roles", async () => { const result = await FetchUserRolesAndGroupsService.go(testUser.userId) @@ -82,6 +88,12 @@ describe('Fetch User Roles And Groups service', () => { }) describe('when the user does not exist', () => { + it('returns `false` for `userFound`', async () => { + const result = await FetchUserRolesAndGroupsService.go(0) + + expect(result.userFound).to.be.false() + }) + it('returns an empty roles array', async () => { const result = await FetchUserRolesAndGroupsService.go(0) From 2b65b63f24546e0900d9c4a3a8a68960ad766ce8 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Mon, 4 Sep 2023 17:16:35 +0100 Subject: [PATCH 15/28] Remove `.only` --- test/services/idm/fetch-user-roles-and-groups.service.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/services/idm/fetch-user-roles-and-groups.service.test.js b/test/services/idm/fetch-user-roles-and-groups.service.test.js index cde03b906d..1a8b61578d 100644 --- a/test/services/idm/fetch-user-roles-and-groups.service.test.js +++ b/test/services/idm/fetch-user-roles-and-groups.service.test.js @@ -19,7 +19,7 @@ const UserRoleHelper = require('../../support/helpers/idm/user-role.helper.js') // Thing under test const FetchUserRolesAndGroupsService = require('../../../app/services/idm/fetch-user-roles-and-groups.service.js') -describe.only('Fetch User Roles And Groups service', () => { +describe('Fetch User Roles And Groups service', () => { let testRoleForUser let testRoleForGroup let testUser From 70310fdd498546c492f7bdbf2afbb5e4c87322ec Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 10:31:40 +0100 Subject: [PATCH 16/28] Return `user` from `FetchUserRoles...` service We decided that instead of simplying returning a boolean to say if the user was found, we instead return the actual user object. This allows us to check that the user was found (as it will be `null` if not), and gives us the option of using user data later on (eg. email address) --- .../fetch-user-roles-and-groups.service.js | 21 +++++++++++++++---- ...etch-user-roles-and-groups.service.test.js | 8 +++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/services/idm/fetch-user-roles-and-groups.service.js b/app/services/idm/fetch-user-roles-and-groups.service.js index 3d3b26568a..0e7e5eb3e4 100644 --- a/app/services/idm/fetch-user-roles-and-groups.service.js +++ b/app/services/idm/fetch-user-roles-and-groups.service.js @@ -21,7 +21,7 @@ const UserModel = require('../../models/idm/user.model.js') * @param {Number} userId The user id to get roles and groups for * * @returns {Object} result The resulting roles and groups - * @returns {Boolean} result.userFound Returns `true` if the user could be found in the `users` table + * @returns {UseModel[]} result.user Returns the UserModel representing the user, or `null` if the user is not found * @returns {RoleModel[]} result.roles An array of RoleModel objects representing the roles the user has * @returns {GroupModel[]} result.groups An array of GroupModel objects representing the groups the user is a member of */ @@ -32,23 +32,36 @@ async function go (userId) { if (!user) { return { - userFound: false, + user: null, roles: [], groups: [] } } - const { groups, roles } = user + const { roles, groups } = _extractRolesAndGroupsFromUser(user) const rolesFromGroups = _extractRolesFromGroups(groups) const combinedAndDedupedRoles = _combineAndDedupeRoles([...roles, ...rolesFromGroups]) return { - userFound: true, + user, roles: combinedAndDedupedRoles, groups } } +/** + * The user object we get back from the query has the roles and groups attached to it. The service returns the user, + * roles and groups separately so we remove the roles and groups from the user object so we aren't returning the same + * data twice. + */ +function _extractRolesAndGroupsFromUser (user) { + const { roles, groups } = user + delete user.roles + delete user.groups + + return { roles, groups } +} + /** * The roles that the user is assigned to via groups are returned from our main query within the user's Group objects. * We want to extract the roles and remove them from the groups in order to keep the Group objects clean and avoid diff --git a/test/services/idm/fetch-user-roles-and-groups.service.test.js b/test/services/idm/fetch-user-roles-and-groups.service.test.js index 1a8b61578d..8a6067000e 100644 --- a/test/services/idm/fetch-user-roles-and-groups.service.test.js +++ b/test/services/idm/fetch-user-roles-and-groups.service.test.js @@ -47,10 +47,10 @@ describe('Fetch User Roles And Groups service', () => { }) describe('when the user exists', () => { - it('returns `true` for `userFound`', async () => { + it('returns the user', async () => { const result = await FetchUserRolesAndGroupsService.go(testUser.userId) - expect(result.userFound).to.be.true() + expect(result.user).to.equal(testUser) }) it("returns the user's roles", async () => { @@ -88,10 +88,10 @@ describe('Fetch User Roles And Groups service', () => { }) describe('when the user does not exist', () => { - it('returns `false` for `userFound`', async () => { + it('returns `null` for `user`', async () => { const result = await FetchUserRolesAndGroupsService.go(0) - expect(result.userFound).to.be.false() + expect(result.user).to.be.null() }) it('returns an empty roles array', async () => { From 0fe0844d19b9cd1760107cc6f9e10c1e75060d8a Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 10:32:09 +0100 Subject: [PATCH 17/28] Correct `redirectTo` path in plugin --- app/plugins/authentication.plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index ad47c0be3e..64268eb663 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -30,7 +30,7 @@ const AuthenticationPlugin = { ttl: TWO_HOURS_IN_MS, isHttpOnly: true }, - redirectTo: '/signIn', + redirectTo: '/signin', validate: async (_request, session) => { const { userId } = session // TODO: Look up userId in the IDM to ensure user exists. Also get user role and add to `credentials` From 2ff7d3a20c47a3f82ab7d0260994cbccff8a3d2a Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 10:32:49 +0100 Subject: [PATCH 18/28] Add second test path To help us manually test the route, we add a second test path which allows us to add to the scope --- app/plugins/authentication.plugin.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 64268eb663..7d41dc828a 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -41,7 +41,6 @@ const AuthenticationPlugin = { // We set up our route in the dependency callback as we can't set authentication before the strategy is registered server.route({ method: 'GET', - // TODO: pick a better path path: '/auth-test', handler: (request, _h) => { return { auth: request.auth } @@ -49,7 +48,29 @@ const AuthenticationPlugin = { options: { description: 'Test that authentication is working', app: { excludeFromProd: true }, - auth: 'session' + auth: { + strategy: 'session' + } + } + }) + + // We don't use an option path param (ie. `{role?}`) as this doesn't work with dynamic scope; not entering a role + // would mean that the scope is empty and therefore nobody can access it + server.route({ + method: 'GET', + path: '/auth-test/{role}', + handler: (request, _h) => { + return { auth: request.auth } + }, + options: { + description: 'Test that authentication is working', + app: { excludeFromProd: true }, + auth: { + strategy: 'session', + access: { + scope: ['{params.role}'] + } + } } }) }) From 713f5db0dfea73a89f809091005884596bbfb0cc Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 10:43:24 +0100 Subject: [PATCH 19/28] Integrate `FetchUserRolesAndGroupsService` into plugin --- app/plugins/authentication.plugin.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 7d41dc828a..9a5d7cbd56 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -7,6 +7,8 @@ const AuthenticationConfig = require('../../config/authentication.config.js') +const FetchUserRolesAndGroupsService = require('../services/idm/fetch-user-roles-and-groups.service.js') + const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 /** @@ -33,8 +35,16 @@ const AuthenticationPlugin = { redirectTo: '/signin', validate: async (_request, session) => { const { userId } = session - // TODO: Look up userId in the IDM to ensure user exists. Also get user role and add to `credentials` - return { isValid: !!userId, credentials: { userId } } + + const { user, roles, groups } = await FetchUserRolesAndGroupsService.go(userId) + + // We put each role's name into the scope array; if a path has a `scope` option (which is an array of strings) + // then the user's scope array must contain at least one of those strings for the request to be authorised + const scope = roles.map((role) => { + return role.role + }) + + return { isValid: !!user, credentials: { user, roles, groups, scope } } } }) From 418c227a17bf1a559697049d2b613201cdbb98d6 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 11:09:46 +0100 Subject: [PATCH 20/28] Update docs --- app/plugins/authentication.plugin.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 9a5d7cbd56..6d6bdf629f 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -12,10 +12,27 @@ const FetchUserRolesAndGroupsService = require('../services/idm/fetch-user-roles const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 /** - * Some of our routes serve up pages, and are intended to be called from the UI rather than being hit directly. We do - * not rely on the UI authenticating requests first as there are some pages which we do not wish to have authentication - * on (eg. service status pages). We therefore authenticate pages on the System side, relying on an authenticated cookie - * being passed on by the UI. + * Some of our routes serve up pages, and are intended to be called from the UI via its `/system/` proxy path rather + * than being hit directly. We do not rely on the proxy authenticating requests first as there are some pages which we + * do not wish to have authentication on (eg. service status pages). We therefore authenticate pages on the System side, + * relying on an authenticated cookie being passed on by the UI. A request that is not authenticated is automatically + * redirected to the sign-in page. + * + * If the request is authenticated then we look up the user in the IDM using FetchUserRolesAndGroupsService. This gives + * us a UserModel object, along with RoleModel and GroupModel objects representing the roles and groups the user is + * assigned to. These are all added to the request under request.auth.credentials. We add the user to the credentials + * as controllers and services may need user info such as the email address. The roles and groups are "nice to have" at + * this stage. + * + * We also take the role names and add them to an array request.auth.credentials.scope. This scope array is used for + * authorisation. + * + * Routes can have 'scope' added to them via their options.auth.strategy.scope. This is an array of strings. If a route + * has a scope array then Hapi will check that request.auth.credentials.scope contains at least one of those strings, + * and reject the request with a 403 error if it doesn't. In other words, if we add a role name to a route's scope, we + * can ensure that only users with that role can access the route. + * + * More info on authorisation and scope can be found at https://hapi.dev/api/?v=21.3.2#-routeoptionsauthaccessscope */ const AuthenticationPlugin = { @@ -38,8 +55,7 @@ const AuthenticationPlugin = { const { user, roles, groups } = await FetchUserRolesAndGroupsService.go(userId) - // We put each role's name into the scope array; if a path has a `scope` option (which is an array of strings) - // then the user's scope array must contain at least one of those strings for the request to be authorised + // We put each role's name into the scope array for hapi to use for its scope authorisation const scope = roles.map((role) => { return role.role }) From 9fdedaabddd59325cff5410363abe3df0f35c0ab Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:17:15 +0100 Subject: [PATCH 21/28] Move auth logic into separate `AuthenticationService` Unit testing our plugin logic was proving difficult as we need to inject a valid cookie into our request in order to hit the `validate` handler. However there doesn't seem to be any straightforward way of generating a cookie so we were looking at hardcoding it. Instead of this, we move the plugin logic into a separate `AuthenticationService`, which we can easily unit test. --- app/plugins/authentication.plugin.js | 21 +--- .../plugins/authentication.service.js | 43 ++++++++ .../plugins/authentication.service.test.js | 103 ++++++++++++++++++ 3 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 app/services/plugins/authentication.service.js create mode 100644 test/services/plugins/authentication.service.test.js diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 6d6bdf629f..7521fb4ceb 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -7,7 +7,7 @@ const AuthenticationConfig = require('../../config/authentication.config.js') -const FetchUserRolesAndGroupsService = require('../services/idm/fetch-user-roles-and-groups.service.js') +const AuthenticationService = require('../services/plugins/authentication.service.js') const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 @@ -18,11 +18,9 @@ const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 * relying on an authenticated cookie being passed on by the UI. A request that is not authenticated is automatically * redirected to the sign-in page. * - * If the request is authenticated then we look up the user in the IDM using FetchUserRolesAndGroupsService. This gives - * us a UserModel object, along with RoleModel and GroupModel objects representing the roles and groups the user is - * assigned to. These are all added to the request under request.auth.credentials. We add the user to the credentials - * as controllers and services may need user info such as the email address. The roles and groups are "nice to have" at - * this stage. + * If the request is authenticated then we pass the user id along to AuthenticationService. This will give us a + * UserModel object, along with RoleModel and GroupModel objects representing the roles and groups the user is assigned + * to. These are all added to the request under request.auth.credentials. * * We also take the role names and add them to an array request.auth.credentials.scope. This scope array is used for * authorisation. @@ -51,16 +49,7 @@ const AuthenticationPlugin = { }, redirectTo: '/signin', validate: async (_request, session) => { - const { userId } = session - - const { user, roles, groups } = await FetchUserRolesAndGroupsService.go(userId) - - // We put each role's name into the scope array for hapi to use for its scope authorisation - const scope = roles.map((role) => { - return role.role - }) - - return { isValid: !!user, credentials: { user, roles, groups, scope } } + return AuthenticationService.go(session.userId) } }) diff --git a/app/services/plugins/authentication.service.js b/app/services/plugins/authentication.service.js new file mode 100644 index 0000000000..180522fbcc --- /dev/null +++ b/app/services/plugins/authentication.service.js @@ -0,0 +1,43 @@ +'use strict' + +/** + * Used by `AuthenticationPlugin` to authenticate and authorise users + * @module AuthenticationService + */ + +const FetchUserRolesAndGroupsService = require('../idm/fetch-user-roles-and-groups.service.js') + +/** + * This service is intended to be used by our `AuthenticationPlugin` to authenticate and authorise users. + * + * We take a user id and look it up in the `idm` table using `FetchUserRolesAndGroupsService`. This gives us a user + * object along with arrays of role objects and group objects that the user has been assigned to. + * + * We return an object that indicates whether the user is valid (based on whether the user exists), along with user + * information, their roles and their groups. We also return a "scope" array of strings, which correspond to the names + * of the roles the user has. This is used by hapi when authorising the user against a route; if the route has a scope + * array of strings then the user's scope array must contain at least one of the strings. + * + * @param {Number} userId The user id to be authenticated + * @returns {Object} response + * @returns {Boolean} response.isValid Indicates whether the user was found + * @returns {Object} response.credentials User credentials found in the IDM + * @returns {UserModel} response.credentials.user Object representing the user + * @returns {RoleModel[]} response.credentials.roles Objects representing the roles the user has + * @returns {GroupModel[]} response.credentials.groups Objects representing the groups the user is assigned to + * @returns {String[]} response.credentials.scope The names of the roles the user has, for route authorisation purposes + */ +async function go (userId) { + const { user, roles, groups } = await FetchUserRolesAndGroupsService.go(userId) + + // We put each role's name into the scope array for hapi to use for its scope authorisation + const scope = roles.map((role) => { + return role.role + }) + + return { isValid: !!user, credentials: { user, roles, groups, scope } } +} + +module.exports = { + go +} diff --git a/test/services/plugins/authentication.service.test.js b/test/services/plugins/authentication.service.test.js new file mode 100644 index 0000000000..3c6b0a3c6e --- /dev/null +++ b/test/services/plugins/authentication.service.test.js @@ -0,0 +1,103 @@ +'use strict' + +// Test framework dependencies +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') +const Sinon = require('sinon') + +const { describe, it, beforeEach, afterEach } = exports.lab = Lab.script() +const { expect } = Code + +// Things to stub +const FetchUserRolesAndGroupsService = require('../../../app/services/idm/fetch-user-roles-and-groups.service.js') + +// Thing under test +const AuthenticationService = require('../../../app/services/plugins/authentication.service.js') + +describe('Authentication service', () => { + afterEach(() => { + Sinon.restore() + }) + + describe('when the user id is found', () => { + beforeEach(() => { + Sinon.stub(FetchUserRolesAndGroupsService, 'go') + .resolves({ + user: { name: 'User' }, + roles: [{ role: 'Role' }], + groups: [{ group: 'Group' }] + }) + }) + + it('returns isValid as `true`', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.isValid).to.be.true() + }) + + it('returns the user in credentials.user', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.credentials.user).to.equal({ name: 'User' }) + }) + + it('returns the roles in credentials.roles', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.credentials.roles).to.equal([{ role: 'Role' }]) + }) + + it('returns the groups in credentials.groups', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.credentials.groups).to.equal([{ group: 'Group' }]) + }) + + it('returns the role names in credentials.scope', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.credentials.scope).to.equal(['Role']) + }) + }) + + describe('when the user id is not found', () => { + beforeEach(() => { + Sinon.stub(FetchUserRolesAndGroupsService, 'go') + .resolves({ + user: null, + roles: [], + groups: [] + }) + }) + + it('returns isValid as `false`', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.isValid).to.be.false() + }) + + it('returns `null` in credentials.user', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.credentials.user).to.be.null() + }) + + it('returns an empty array in credentials.roles', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.credentials.roles).to.be.empty() + }) + + it('returns an empty array in credentials.groups', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.credentials.groups).to.be.empty() + }) + + it('returns an empty array in credentials.scope', async () => { + const result = await AuthenticationService.go(12345) + + expect(result.credentials.scope).to.be.empty() + }) + }) +}) From 29f54f32d5e80d64f11a414a5bffba834ce41d92 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:20:35 +0100 Subject: [PATCH 22/28] Add `TODO` to set default auth --- app/plugins/authentication.plugin.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 7521fb4ceb..573a120c84 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -31,6 +31,13 @@ const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 * can ensure that only users with that role can access the route. * * More info on authorisation and scope can be found at https://hapi.dev/api/?v=21.3.2#-routeoptionsauthaccessscope + * + * TODO: Add a line to the plugin to set `session` as the default auth strategy, ie: + * + * server.auth.default('session') + * + * Since this is applied to all routes by default, any routes which don't require authentication (eg. service status) + * will need to have its options.auth.strategy set to `false`. */ const AuthenticationPlugin = { From dbd255e6986daa6c8bf1d33885cf1b64e09404fc Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:21:16 +0100 Subject: [PATCH 23/28] Remove test paths These test paths were used for manual poking of endpoints to ensure we were on the right track. We remove them now that they are no longer needed. --- app/plugins/authentication.plugin.js | 36 ---------------------------- 1 file changed, 36 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 573a120c84..eeb88a59ae 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -59,42 +59,6 @@ const AuthenticationPlugin = { return AuthenticationService.go(session.userId) } }) - - // We set up our route in the dependency callback as we can't set authentication before the strategy is registered - server.route({ - method: 'GET', - path: '/auth-test', - handler: (request, _h) => { - return { auth: request.auth } - }, - options: { - description: 'Test that authentication is working', - app: { excludeFromProd: true }, - auth: { - strategy: 'session' - } - } - }) - - // We don't use an option path param (ie. `{role?}`) as this doesn't work with dynamic scope; not entering a role - // would mean that the scope is empty and therefore nobody can access it - server.route({ - method: 'GET', - path: '/auth-test/{role}', - handler: (request, _h) => { - return { auth: request.auth } - }, - options: { - description: 'Test that authentication is working', - app: { excludeFromProd: true }, - auth: { - strategy: 'session', - access: { - scope: ['{params.role}'] - } - } - } - }) }) } } From 21cf899b0f37bb27170838b635bf42509977c8f4 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:32:00 +0100 Subject: [PATCH 24/28] Fix comment --- app/plugins/authentication.plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index eeb88a59ae..27784ff347 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -25,7 +25,7 @@ const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 * We also take the role names and add them to an array request.auth.credentials.scope. This scope array is used for * authorisation. * - * Routes can have 'scope' added to them via their options.auth.strategy.scope. This is an array of strings. If a route + * Routes can have 'scope' added to them via their options.auth.access.scope. This is an array of strings. If a route * has a scope array then Hapi will check that request.auth.credentials.scope contains at least one of those strings, * and reject the request with a 403 error if it doesn't. In other words, if we add a role name to a route's scope, we * can ensure that only users with that role can access the route. From d09449532c2c5643ca1749779ab80e9ac622b06d Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:37:51 +0100 Subject: [PATCH 25/28] Add `COOKIE_SECRET` to `.env.example` --- .env.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env.example b/.env.example index 904af0bea2..31dd8d4b0c 100644 --- a/.env.example +++ b/.env.example @@ -56,5 +56,9 @@ REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= +# Cookie secret for authenticating requests coming via the UI. Note that the value should be the same as `COOKIE_SECRET` +# on the UI side +COOKIE_SECRET= + # Feature flags ENABLE_REISSUING_BILLING_BATCHES=false From b12342528731ef3f6c298c70e079a6eb851a7a6c Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:41:28 +0100 Subject: [PATCH 26/28] Update app/services/plugins/authentication.service.js Co-authored-by: Alan Cruikshanks --- app/services/plugins/authentication.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/plugins/authentication.service.js b/app/services/plugins/authentication.service.js index 180522fbcc..f50cdb73be 100644 --- a/app/services/plugins/authentication.service.js +++ b/app/services/plugins/authentication.service.js @@ -10,7 +10,7 @@ const FetchUserRolesAndGroupsService = require('../idm/fetch-user-roles-and-grou /** * This service is intended to be used by our `AuthenticationPlugin` to authenticate and authorise users. * - * We take a user id and look it up in the `idm` table using `FetchUserRolesAndGroupsService`. This gives us a user + * We take a user id and look it up in the `idm` schema using `FetchUserRolesAndGroupsService`. This gives us a user * object along with arrays of role objects and group objects that the user has been assigned to. * * We return an object that indicates whether the user is valid (based on whether the user exists), along with user From c28156e7ae500103f6a75554ebc50d05a8f41010 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:42:35 +0100 Subject: [PATCH 27/28] Fix JSdoc --- app/services/idm/fetch-user-roles-and-groups.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/idm/fetch-user-roles-and-groups.service.js b/app/services/idm/fetch-user-roles-and-groups.service.js index 0e7e5eb3e4..e671b44e41 100644 --- a/app/services/idm/fetch-user-roles-and-groups.service.js +++ b/app/services/idm/fetch-user-roles-and-groups.service.js @@ -21,7 +21,7 @@ const UserModel = require('../../models/idm/user.model.js') * @param {Number} userId The user id to get roles and groups for * * @returns {Object} result The resulting roles and groups - * @returns {UseModel[]} result.user Returns the UserModel representing the user, or `null` if the user is not found + * @returns {UserModel} result.user Returns the UserModel representing the user, or `null` if the user is not found * @returns {RoleModel[]} result.roles An array of RoleModel objects representing the roles the user has * @returns {GroupModel[]} result.groups An array of GroupModel objects representing the groups the user is a member of */ From 1439f3be8e83844c951c34c9e4befcc85392ac40 Mon Sep 17 00:00:00 2001 From: Stuart Adair <43574728+StuAA78@users.noreply.github.com> Date: Thu, 7 Sep 2023 16:16:51 +0100 Subject: [PATCH 28/28] Remove `TODO` We previously added a `TODO` to our authentication plugin to flag up that we want to apply our `session` authentication to all routes by default. We have now created an issue within our team repo for this work, so we remove the `TODO` from the codebase --- app/plugins/authentication.plugin.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/plugins/authentication.plugin.js b/app/plugins/authentication.plugin.js index 27784ff347..4e151f8d08 100644 --- a/app/plugins/authentication.plugin.js +++ b/app/plugins/authentication.plugin.js @@ -31,13 +31,6 @@ const TWO_HOURS_IN_MS = 2 * 60 * 60 * 1000 * can ensure that only users with that role can access the route. * * More info on authorisation and scope can be found at https://hapi.dev/api/?v=21.3.2#-routeoptionsauthaccessscope - * - * TODO: Add a line to the plugin to set `session` as the default auth strategy, ie: - * - * server.auth.default('session') - * - * Since this is applied to all routes by default, any routes which don't require authentication (eg. service status) - * will need to have its options.auth.strategy set to `false`. */ const AuthenticationPlugin = {