From 48ce79b716f584f3abb995f8b6b0cd84c338d1e8 Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Mon, 8 Jul 2024 19:02:02 +0100 Subject: [PATCH 1/8] Replace basic authentication with a password page and a cookie. --- app.js | 18 ++++--- lib/prototype-admin/password.html | 58 +++++++++++++++++++++ lib/utils.js | 10 ++++ middleware/authentication.js | 77 +++++++++++++++++----------- middleware/prototype-admin-routes.js | 37 +++++++++++++ package-lock.json | 57 ++++++++++++-------- package.json | 2 +- 7 files changed, 201 insertions(+), 58 deletions(-) create mode 100644 lib/prototype-admin/password.html create mode 100644 middleware/prototype-admin-routes.js diff --git a/app.js b/app.js index c2810d78..5ed521de 100755 --- a/app.js +++ b/app.js @@ -4,6 +4,7 @@ const fs = require('fs'); // External dependencies const bodyParser = require('body-parser'); +const cookieParser = require('cookie-parser'); const dotenv = require('dotenv'); const express = require('express'); const nunjucks = require('nunjucks'); @@ -23,6 +24,8 @@ const routes = require('./app/routes'); const documentationRoutes = require('./docs/documentation_routes'); const utils = require('./lib/utils'); +const prototypeAdminRoutes = require('./middleware/prototype-admin-routes'); + // Set configuration variables const port = process.env.PORT || config.port; const useDocumentation = process.env.SHOW_DOCS || config.useDocumentation; @@ -42,11 +45,15 @@ app.locals.useAutoStoreData = (useAutoStoreData === 'true'); app.locals.useCookieSessionStore = (useCookieSessionStore === 'true'); app.locals.serviceName = config.serviceName; +// Use cookie middleware to parse cookies +app.use(cookieParser()); + // Nunjucks configuration for application const appViews = [ path.join(__dirname, 'app/views/'), path.join(__dirname, 'node_modules/nhsuk-frontend/packages/components'), path.join(__dirname, 'docs/views/'), + path.join(__dirname, 'lib/prototype-admin/'), ]; const nunjucksConfig = { @@ -71,6 +78,9 @@ const sessionOptions = { }, }; +// Authentication +app.use(authentication); + // Support session data in cookie or memory if (useCookieSessionStore === 'true' && !onlyDocumentation) { app.use(sessionInCookie(Object.assign(sessionOptions, { @@ -132,12 +142,6 @@ if (!sessionDataDefaultsFileExists) { .pipe(fs.createWriteStream(sessionDataDefaultsFile)); } -// Check if the app is documentation only -if (onlyDocumentation !== 'true') { - // Require authentication if not - app.use(authentication); -} - // Local variables app.use(locals(config)); @@ -213,6 +217,8 @@ app.post('/examples/passing-data/clear-data', (req, res) => { res.render('examples/passing-data/clear-data-success'); }); +app.use('/prototype-admin', prototypeAdminRoutes); + // Redirect all POSTs to GETs - this allows users to use POST for autoStoreData app.post(/^\/([^.]+)$/, (req, res) => { res.redirect(`/${req.params[0]}`); diff --git a/lib/prototype-admin/password.html b/lib/prototype-admin/password.html new file mode 100644 index 00000000..1cead78a --- /dev/null +++ b/lib/prototype-admin/password.html @@ -0,0 +1,58 @@ +{% extends 'layout.html' %} + +{% block pageTitle %} + {% if error == "wrong-password" %} + Error: + {% endif %} + Sign in - NHS Prototype Kit +{% endblock %} + +{% block content %} + +
+
+
+ + {% if error == "wrong-password" %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: [ + { + text: "The password is not correct", + href: "#password" + } + ] + })}} + {% endif %} + +

+ This is a prototype used for research +

+ +

+ It is not a real service. You should only continue if you have been invited to test this prototype. +

+ + {{ input({ + classes: "nhsuk-input--width-10", + name: "password", + id: "password", + type: "password", + errorMessage: { + text: "The password is not correct" + } if error == "wrong-password", + label:{ + text: "Password" + } + }) }} + + + + {{ button({ + text: "Continue" + }) }} + +
+
+
+{% endblock %} diff --git a/lib/utils.js b/lib/utils.js index e14fff7e..937d412b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,7 @@ // NPM dependencies const { get: getKeypath } = require('lodash'); const path = require('path'); +const crypto = require('crypto'); // Require core and custom filters, merges to one object // and then add the methods to Nunjucks environment @@ -310,3 +311,12 @@ exports.handleCookies = function (app) { } } */ + +function encryptPassword(password) { + if (!password) { return undefined; } + const hash = crypto.createHash('sha256'); + hash.update(password); + return hash.digest('hex'); +} + +exports.encryptPassword = encryptPassword; diff --git a/middleware/authentication.js b/middleware/authentication.js index 7e6a67f3..e8438e70 100644 --- a/middleware/authentication.js +++ b/middleware/authentication.js @@ -1,36 +1,51 @@ -/** - * Simple basic auth middleware for use with Express 4.x. - * - * Based on template found at: http://www.danielstjules.com/2014/08/03/basic-auth-with-express-4/ - * - * @example - * const authentication = required('authentication'); - * app.use(authentication); - * - * @param {string} req Express Request object - * @param {string} res Express Response object - * @returns {function} Express 4 middleware requiring the given credentials - */ -// External dependencies -const basicAuth = require('basic-auth'); +const url = require('url'); +const { encryptPassword } = require('../lib/utils'); -module.exports = function (req, res, next) { /* eslint-disable-line func-names,consistent-return */ - // Set configuration variables - const env = (process.env.NODE_ENV || 'development').toLowerCase(); - const username = process.env.PROTOTYPE_USERNAME; - const password = process.env.PROTOTYPE_PASSWORD; +const allowedPathsWhenUnauthenticated = [ + '/prototype-admin/password', + '/css/main.css', + '/nhsuk-frontend/nhsuk.min.js', + '/js/auto-store-data.js', + '/js/jquery-3.5.1.min.js', + '/js/main.js', +]; - if (env === 'production' || env === 'staging') { - if (!username || !password) { - return res.send('

Username or password not set in environment variables.

'); - } +const encryptedPassword = encryptPassword(process.env.PASSWORD); +const nodeEnv = process.env.NODE_ENV || 'test'; - const user = basicAuth(req); +// Redirect the user to the password page, with +// the current page path set as the returnURL in a query +// string so the user can be redirected back after successfully +// entering a password +function sendUserToPasswordPage(req, res) { + const returnURL = url.format({ + pathname: req.path, + query: req.query, + }); + const passwordPageURL = url.format({ + pathname: '/prototype-admin/password', + query: { returnURL }, + }); + res.redirect(passwordPageURL); +} - if (!user || user.name !== username || user.pass !== password) { - res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); - return res.sendStatus(401); - } +// Give the user some instructions on how to set a password +function showNoPasswordError(res) { + return res.send('

Error:

Password not set. See guidance for setting a password.

'); +} + +function authentication(req, res, next) { + if (nodeEnv !== 'production') { + next(); + } else if (!process.env.PASSWORD) { + showNoPasswordError(res); + } else if (allowedPathsWhenUnauthenticated.includes(req.path)) { + next(); + } else if (req.cookies.authentication === encryptedPassword) { + next(); + } else { + sendUserToPasswordPage(req, res); } - next(); -}; +} + +module.exports = authentication; diff --git a/middleware/prototype-admin-routes.js b/middleware/prototype-admin-routes.js new file mode 100644 index 00000000..8ffd93f0 --- /dev/null +++ b/middleware/prototype-admin-routes.js @@ -0,0 +1,37 @@ +const express = require('express'); + +const router = express.Router(); + +const password = process.env.PASSWORD; + +const { encryptPassword } = require('../lib/utils'); + +router.get('/password', (req, res) => { + const returnURL = req.query.returnURL || '/'; + const { error } = req.query; + res.render('password', { + returnURL, + error, + }); +}); + +// Check authentication password +router.post('/password', (req, res) => { + const submittedPassword = req.body.password; + const { returnURL } = req.body; + + if (submittedPassword === password) { + // see lib/middleware/authentication.js for explanation + res.cookie('authentication', encryptPassword(password), { + maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days + sameSite: 'None', // Allows GET and POST requests from other domains + httpOnly: true, + secure: true, + }); + res.redirect(returnURL); + } else { + res.redirect(`/prototype-admin/password?error=wrong-password&returnURL=${encodeURIComponent(returnURL)}`); + } +}); + +module.exports = router; diff --git a/package-lock.json b/package-lock.json index ddf730a2..b2377904 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,10 @@ "dependencies": { "@babel/core": "^7.24.7", "@babel/preset-env": "^7.24.7", - "basic-auth": "^2.0.1", "body-parser": "^1.20.2", "browser-sync": "^3.0.2", "client-sessions": "^0.8.0", + "cookie-parser": "^1.4.6", "dotenv": "^16.4.5", "express": "^4.19.2", "express-session": "^1.18.0", @@ -3602,17 +3602,6 @@ "node": "^4.5.0 || >= 5.9" } }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -4475,6 +4464,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -16887,14 +16896,6 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - } - }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -17539,6 +17540,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" }, + "cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/package.json b/package.json index fa38b6c3..a5cb36db 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "dependencies": { "@babel/core": "^7.24.7", "@babel/preset-env": "^7.24.7", - "basic-auth": "^2.0.1", "body-parser": "^1.20.2", "browser-sync": "^3.0.2", "client-sessions": "^0.8.0", + "cookie-parser": "^1.4.6", "dotenv": "^16.4.5", "express": "^4.19.2", "express-session": "^1.18.0", From a1a78096a0f60dd458c0666ff0aff9d4e53ebdd5 Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Mon, 8 Jul 2024 22:47:17 +0100 Subject: [PATCH 2/8] Add url for guidance page --- middleware/authentication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/authentication.js b/middleware/authentication.js index e8438e70..7e490bb9 100644 --- a/middleware/authentication.js +++ b/middleware/authentication.js @@ -31,7 +31,7 @@ function sendUserToPasswordPage(req, res) { // Give the user some instructions on how to set a password function showNoPasswordError(res) { - return res.send('

Error:

Password not set. See guidance for setting a password.

'); + return res.send('

Error:

Password not set. See guidance for setting a password.

'); } function authentication(req, res, next) { From 8cb813bf5696e81123e0c58ff90454d4a7238c43 Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Tue, 13 Aug 2024 10:50:21 +0100 Subject: [PATCH 3/8] Default to development environment --- middleware/authentication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/authentication.js b/middleware/authentication.js index 7e490bb9..eb282691 100644 --- a/middleware/authentication.js +++ b/middleware/authentication.js @@ -11,7 +11,7 @@ const allowedPathsWhenUnauthenticated = [ ]; const encryptedPassword = encryptPassword(process.env.PASSWORD); -const nodeEnv = process.env.NODE_ENV || 'test'; +const nodeEnv = process.env.NODE_ENV || 'development'; // Redirect the user to the password page, with // the current page path set as the returnURL in a query From 12b693fbcbfa27695f7ce484ac19d2a5ff21e84d Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Tue, 13 Aug 2024 13:55:00 +0100 Subject: [PATCH 4/8] Update page title --- lib/prototype-admin/password.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/prototype-admin/password.html b/lib/prototype-admin/password.html index 1cead78a..f88854b3 100644 --- a/lib/prototype-admin/password.html +++ b/lib/prototype-admin/password.html @@ -4,7 +4,7 @@ {% if error == "wrong-password" %} Error: {% endif %} - Sign in - NHS Prototype Kit + Enter password - NHS prototype kit {% endblock %} {% block content %} From fe0af7253d878cee5648447ed6259806d13f90a8 Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Tue, 13 Aug 2024 13:56:00 +0100 Subject: [PATCH 5/8] Change env var to PROTOTYPE_PASSWORD --- middleware/authentication.js | 4 ++-- middleware/prototype-admin-routes.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/middleware/authentication.js b/middleware/authentication.js index eb282691..24995d8a 100644 --- a/middleware/authentication.js +++ b/middleware/authentication.js @@ -10,7 +10,7 @@ const allowedPathsWhenUnauthenticated = [ '/js/main.js', ]; -const encryptedPassword = encryptPassword(process.env.PASSWORD); +const encryptedPassword = encryptPassword(process.env.PROTOTYPE_PASSWORD); const nodeEnv = process.env.NODE_ENV || 'development'; // Redirect the user to the password page, with @@ -37,7 +37,7 @@ function showNoPasswordError(res) { function authentication(req, res, next) { if (nodeEnv !== 'production') { next(); - } else if (!process.env.PASSWORD) { + } else if (!process.env.PROTOTYPE_PASSWORD) { showNoPasswordError(res); } else if (allowedPathsWhenUnauthenticated.includes(req.path)) { next(); diff --git a/middleware/prototype-admin-routes.js b/middleware/prototype-admin-routes.js index 8ffd93f0..c4de31ed 100644 --- a/middleware/prototype-admin-routes.js +++ b/middleware/prototype-admin-routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); -const password = process.env.PASSWORD; +const password = process.env.PROTOTYPE_PASSWORD; const { encryptPassword } = require('../lib/utils'); From 1fb2c1a25a313ab741dee79642020eb046c0c98c Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Tue, 13 Aug 2024 13:59:03 +0100 Subject: [PATCH 6/8] Update documentation --- docs/views/how-tos/publish-your-prototype-online.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/views/how-tos/publish-your-prototype-online.html b/docs/views/how-tos/publish-your-prototype-online.html index c8f7f220..0f8748ef 100644 --- a/docs/views/how-tos/publish-your-prototype-online.html +++ b/docs/views/how-tos/publish-your-prototype-online.html @@ -33,13 +33,13 @@

Hosting services

Some hosting services may automatically deploy your prototype every time you merge new changes in the connected GitHub repository. You may be able to enable automatic deploys. If it hasn't automatically update your prototype, you will need to do this manually.

Setting a password

-

When running the prototype kit online, you must set a username and password. This is to stop anyone accidentally finding your prototype and mistaking it for a real service.

+

When running the prototype kit online, you must set a password. This is to stop anyone accidentally finding your prototype and mistaking it for a real service.

To set a password, check your hosting services documentation on how to set "environment variables". (It may have a slightly different name like "config vars" or "variables".)

-

Set environment variables. For example:

-

PROTOTYPE_USERNAMEnameofservice

-

PROTOTYPE_PASSWORDyourpassword123

+

The password should be set using the variable PROTOTYPE_USERNAME.

-

In Railway, you also need to create:

+

If you are using an older version of the prototype kit (before v4.12.9) you will also need to set a username using the variable PROTOTYPE_USERNAME.

+ +

In Railway, you also need to create:

  • a variable called NODE-ENV and set the value to production
  • a variable called USE_AUTH with the value of true
  • From 0ec58cd568beb311e17b7a7c5816f52c44fc1f72 Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Wed, 14 Aug 2024 12:27:06 +0100 Subject: [PATCH 7/8] Fix variable name --- docs/views/how-tos/publish-your-prototype-online.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/views/how-tos/publish-your-prototype-online.html b/docs/views/how-tos/publish-your-prototype-online.html index 0f8748ef..23998ed9 100644 --- a/docs/views/how-tos/publish-your-prototype-online.html +++ b/docs/views/how-tos/publish-your-prototype-online.html @@ -35,7 +35,7 @@

    Hosting services

    Setting a password

    When running the prototype kit online, you must set a password. This is to stop anyone accidentally finding your prototype and mistaking it for a real service.

    To set a password, check your hosting services documentation on how to set "environment variables". (It may have a slightly different name like "config vars" or "variables".)

    -

    The password should be set using the variable PROTOTYPE_USERNAME.

    +

    The password should be set using the variable PROTOTYPE_PASSWORD.

    If you are using an older version of the prototype kit (before v4.12.9) you will also need to set a username using the variable PROTOTYPE_USERNAME.

    From 5b4ac5565fa9fe034888effa4fefd08ad5efea7f Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Wed, 14 Aug 2024 13:41:49 +0100 Subject: [PATCH 8/8] Fix release number --- docs/views/how-tos/publish-your-prototype-online.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/views/how-tos/publish-your-prototype-online.html b/docs/views/how-tos/publish-your-prototype-online.html index 23998ed9..fcf0ad9b 100644 --- a/docs/views/how-tos/publish-your-prototype-online.html +++ b/docs/views/how-tos/publish-your-prototype-online.html @@ -37,7 +37,7 @@

    Setting a password

    To set a password, check your hosting services documentation on how to set "environment variables". (It may have a slightly different name like "config vars" or "variables".)

    The password should be set using the variable PROTOTYPE_PASSWORD.

    -

    If you are using an older version of the prototype kit (before v4.12.9) you will also need to set a username using the variable PROTOTYPE_USERNAME.

    +

    If you are using an older version of the prototype kit (before v4.12.0) you will also need to set a username using the variable PROTOTYPE_USERNAME.

    In Railway, you also need to create: