diff --git a/README.md b/README.md index 3c74816..5049e24 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ FULL_CHAIN=fullchain.pem ACCESS_TOKEN_SECRET= REFRESH_TOKEN_SECRET= AUTH_CODE_SECRET= +SESSION_SECRET= DB_CONNECT=mongodb+srv://:@/?retryWrites=true&w=majority # SAS Options @@ -107,7 +108,7 @@ Normally the server process will stop when your terminal dies. To keep it going Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`. -You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 ` command. You can see your sessions using `top -u `. Type `c` to see the commands being run against each pid. +You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 ` command. You can see your sessions using `top -u `. Type `c` to see the commands being run against each pid. ### PM2 diff --git a/api/package-lock.json b/api/package-lock.json index d5d3bcb..a0c9dd0 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -14,11 +14,11 @@ "connect-mongo": "^4.6.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "csurf": "^1.11.0", "express": "^4.17.1", "express-session": "^1.17.2", "joi": "^17.4.2", "jsonwebtoken": "^8.5.1", - "mongodb": "^4.1.4", "mongoose": "^6.0.12", "mongoose-sequence": "^5.3.1", "morgan": "^1.10.0", @@ -32,6 +32,7 @@ "@types/bcryptjs": "^2.4.2", "@types/cookie-parser": "^1.4.2", "@types/cors": "^2.8.12", + "@types/csurf": "^1.11.2", "@types/express": "^4.17.12", "@types/express-session": "^1.17.4", "@types/jest": "^26.0.24", @@ -1837,6 +1838,15 @@ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "dev": true }, + "node_modules/@types/csurf": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz", + "integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*" + } + }, "node_modules/@types/express": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", @@ -3427,6 +3437,19 @@ "node": ">=8" } }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -3451,6 +3474,40 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "node_modules/csv-stringify": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", @@ -8688,6 +8745,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9673,6 +9735,14 @@ "yarn": ">=1.9.4" } }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -11677,6 +11747,15 @@ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "dev": true }, + "@types/csurf": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz", + "integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*" + } + }, "@types/express": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", @@ -12984,6 +13063,16 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "requires": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + } + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -13007,6 +13096,36 @@ } } }, + "csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "requires": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + } + } + }, "csv-stringify": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", @@ -16942,6 +17061,11 @@ "glob": "^7.1.3" } }, + "rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -17679,6 +17803,11 @@ "@tsoa/runtime": "^3.13.0" } }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/api/package.json b/api/package.json index aff24b2..4587d7c 100644 --- a/api/package.json +++ b/api/package.json @@ -53,6 +53,7 @@ "connect-mongo": "^4.6.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "csurf": "^1.11.0", "express": "^4.17.1", "express-session": "^1.17.2", "joi": "^17.4.2", @@ -67,6 +68,7 @@ "@types/bcryptjs": "^2.4.2", "@types/cookie-parser": "^1.4.2", "@types/cors": "^2.8.12", + "@types/csurf": "^1.11.2", "@types/express": "^4.17.12", "@types/express-session": "^1.17.4", "@types/jest": "^26.0.24", diff --git a/api/src/app.ts b/api/src/app.ts index d588bea..c7263d1 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -1,5 +1,6 @@ import path from 'path' import express, { ErrorRequestHandler } from 'express' +import csrf from 'csurf' import session from 'express-session' import MongoStore from 'connect-mongo' import morgan from 'morgan' @@ -20,8 +21,25 @@ dotenv.config() const app = express() -const { MODE, CORS, WHITELIST } = process.env +app.use(cookieParser()) +app.use(morgan('tiny')) +const { MODE, CORS, WHITELIST, PROTOCOL } = process.env + +export const cookieOptions = { + secure: PROTOCOL === 'https', + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000 // 24 hours +} + +/*********************************** + * CSRF Protection * + ***********************************/ +export const csrfProtection = csrf({ cookie: cookieOptions }) + +/*********************************** + * Enabling CORS * + ***********************************/ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') { const whiteList: string[] = [] WHITELIST?.split(' ') @@ -36,35 +54,35 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') { app.use(cors({ credentials: true, origin: whiteList })) } +/*********************************** + * DB Connection & * + * Express Sessions * + * With Mongo Store * + ***********************************/ if (MODE?.trim() === 'server') { // NOTE: when exporting app.js as agent for supertest // we should exclude connecting to the real database if (process.env.NODE_ENV !== 'test') { const clientPromise = connectDB().then((conn) => conn!.getClient() as any) - const { PROTOCOL } = process.env - app.use( session({ secret: process.env.SESSION_SECRET as string, saveUninitialized: false, // don't create session until something stored resave: false, //don't save session if unmodified store: MongoStore.create({ clientPromise, collectionName: 'sessions' }), - cookie: { - secure: PROTOCOL === 'https', - maxAge: 24 * 60 * 60 * 1000 // 24 hours - } + cookie: cookieOptions }) ) } } - -app.use(cookieParser()) -app.use(morgan('tiny')) app.use(express.json({ limit: '100mb' })) app.use(express.static(path.join(__dirname, '../public'))) const onError: ErrorRequestHandler = (err, req, res, next) => { + if (err.code === 'EBADCSRFTOKEN') + return res.status(400).send('Invalid CSRF token!') + console.error(err.stack) res.status(500).send('Something broke!') } diff --git a/api/src/routes/setupRoutes.ts b/api/src/routes/setupRoutes.ts index b393ffc..38895dd 100644 --- a/api/src/routes/setupRoutes.ts +++ b/api/src/routes/setupRoutes.ts @@ -5,7 +5,6 @@ import apiRouter from './api' import appStreamRouter from './appStream' export const setupRoutes = (app: Express) => { - app.use('/', webRouter) app.use('/SASjsApi', apiRouter) app.use('/AppStream', function (req, res, next) { @@ -13,4 +12,6 @@ export const setupRoutes = (app: Express) => { // whatever the current router is appStreamRouter(req, res, next) }) + + app.use('/', webRouter) } diff --git a/api/src/routes/web/index.ts b/api/src/routes/web/index.ts index 6f75c11..98b5a85 100644 --- a/api/src/routes/web/index.ts +++ b/api/src/routes/web/index.ts @@ -1,8 +1,9 @@ import express from 'express' +import { csrfProtection } from '../../app' import webRouter from './web' const router = express.Router() -router.use('/', webRouter) +router.use('/', csrfProtection, webRouter) export default router diff --git a/api/src/routes/web/web.ts b/api/src/routes/web/web.ts index f9889b0..696b459 100644 --- a/api/src/routes/web/web.ts +++ b/api/src/routes/web/web.ts @@ -14,6 +14,11 @@ webRouter.get('/', async (_, res) => { return res.send('Web Build is not present') }) +webRouter.get('/form', function (req, res) { + // pass the csrfToken to the view + res.send({ csrfToken: req.csrfToken() }) +}) + webRouter.post('/login', async (req, res) => { const { error, value: body } = loginWebValidation(req.body) if (error) return res.status(400).send(error.details[0].message) diff --git a/web/src/components/login.tsx b/web/src/components/login.tsx index cca13e2..7d8bfcd 100644 --- a/web/src/components/login.tsx +++ b/web/src/components/login.tsx @@ -10,7 +10,13 @@ const getAuthCode = async (credentials: any) => axios.post('/SASjsApi/auth/authorize', credentials).then((res) => res.data) const login = async (payload: { username: string; password: string }) => - axios.post('/login', payload).then((res) => res.data) + axios.get('/form').then((res1) => + axios + .post('/login', payload, { + headers: { 'csrf-token': res1.data.csrfToken } + }) + .then((res2) => res2.data) + ) const Login = ({ getCodeOnly }: any) => { const location = useLocation()