1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-12 20:04:36 +00:00

Compare commits

..

10 Commits

Author SHA1 Message Date
Saad Jutt
9ed10109c1 chore: specified domain for cookie 2022-10-09 19:24:42 +05:00
Saad Jutt
ef9cca575f fix: added domain check if provided 2022-10-04 23:47:41 +05:00
Saad Jutt
efbfd3f392 chore(web): switched built tool, webpack to Vite 2022-10-02 03:23:09 +05:00
semantic-release-bot
69ddf313b8 chore(release): 0.21.7 [skip ci]
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)

### Bug Fixes

* csrf package is changed to pillarjs-csrf ([fe3e508](fe3e5088f8))
2022-09-30 21:44:16 +00:00
Saad Jutt
65e404cdbd Merge pull request #294 from sasjs/csrf-package-migration
fix: csrf package is changed to pillarjs-csrf
2022-10-01 02:39:06 +05:00
Saad Jutt
fda6ad6356 chore(csrf): removed _csrf completely 2022-09-30 03:07:21 +05:00
Saad Jutt
fe3e5088f8 fix: csrf package is changed to pillarjs-csrf 2022-09-29 20:33:30 +05:00
semantic-release-bot
375f924f45 chore(release): 0.21.6 [skip ci]
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)

### Bug Fixes

* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](40f95f9072))
2022-09-23 09:33:49 +00:00
Allan Bowe
72329e30ed Merge pull request #291 from sasjs/issue-290
fix: in getTokensFromDB handle the scenario when tokens are expired
2022-09-23 10:29:51 +01:00
40f95f9072 fix: in getTokensFromDB handle the scenario when tokens are expired 2022-09-23 09:35:30 +05:00
29 changed files with 2077 additions and 6473 deletions

View File

@@ -1,3 +1,17 @@
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)
### Bug Fixes
* csrf package is changed to pillarjs-csrf ([fe3e508](https://github.com/sasjs/server/commit/fe3e5088f8dfff50042ec8e8aac9ba5ba1394deb))
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)
### Bug Fixes
* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](https://github.com/sasjs/server/commit/40f95f9072c8685910138d88fd2410f8704fc975))
## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22) ## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22)

View File

@@ -1,5 +1,6 @@
MODE=[desktop|server] default considered as desktop MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
ALLOWED_DOMAINS=<space separated urls, each starting with protocol `http` or `https`>
WHITELIST=<space separated urls, each starting with protocol `http` or `https`> WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http PROTOCOL=[http|https] default considered as http

97
api/package-lock.json generated
View File

@@ -14,7 +14,6 @@
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.17.2", "express-session": "^1.17.2",
"helmet": "^5.0.2", "helmet": "^5.0.2",
@@ -37,7 +36,6 @@
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/express-session": "^1.17.4", "@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
@@ -50,6 +48,7 @@
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5", "@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"csrf": "^3.1.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1", "http-headers-validation": "^0.0.1",
"jest": "^27.0.6", "jest": "^27.0.6",
@@ -1833,15 +1832,6 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true "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": { "node_modules/@types/express": {
"version": "4.17.12", "version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
@@ -3336,6 +3326,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"dev": true,
"dependencies": { "dependencies": {
"rndm": "1.2.0", "rndm": "1.2.0",
"tsscmp": "1.0.6", "tsscmp": "1.0.6",
@@ -3369,40 +3360,6 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true "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": { "node_modules/csv-stringify": {
"version": "5.6.5", "version": "5.6.5",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
@@ -8377,7 +8334,8 @@
"node_modules/rndm": { "node_modules/rndm": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=",
"dev": true
}, },
"node_modules/rotating-file-stream": { "node_modules/rotating-file-stream": {
"version": "3.0.4", "version": "3.0.4",
@@ -9389,6 +9347,7 @@
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
"dev": true,
"engines": { "engines": {
"node": ">=0.6.x" "node": ">=0.6.x"
} }
@@ -11361,15 +11320,6 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true "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": { "@types/express": {
"version": "4.17.12", "version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
@@ -12584,6 +12534,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"dev": true,
"requires": { "requires": {
"rndm": "1.2.0", "rndm": "1.2.0",
"tsscmp": "1.0.6", "tsscmp": "1.0.6",
@@ -12613,36 +12564,6 @@
} }
} }
}, },
"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": { "csv-stringify": {
"version": "5.6.5", "version": "5.6.5",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
@@ -16379,7 +16300,8 @@
"rndm": { "rndm": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=",
"dev": true
}, },
"rotating-file-stream": { "rotating-file-stream": {
"version": "3.0.4", "version": "3.0.4",
@@ -17134,7 +17056,8 @@
"tsscmp": { "tsscmp": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
"dev": true
}, },
"tunnel-agent": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",

View File

@@ -53,7 +53,6 @@
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.17.2", "express-session": "^1.17.2",
"helmet": "^5.0.2", "helmet": "^5.0.2",
@@ -73,7 +72,6 @@
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/express-session": "^1.17.4", "@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
@@ -86,6 +84,7 @@
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5", "@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"csrf": "^3.1.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1", "http-headers-validation": "^0.0.1",
"jest": "^27.0.6", "jest": "^27.0.6",

View File

@@ -3,10 +3,21 @@ import cors from 'cors'
import { CorsType } from '../utils' import { CorsType } from '../utils'
export const configureCors = (app: Express) => { export const configureCors = (app: Express) => {
const { CORS } = process.env
if (CORS === CorsType.ENABLED) {
const whiteList = getWhiteListed()
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
}
export const getWhiteListed = (): string[] => {
const whiteList: string[] = []
const { CORS, WHITELIST } = process.env const { CORS, WHITELIST } = process.env
if (CORS === CorsType.ENABLED) { if (CORS === CorsType.ENABLED) {
const whiteList: string[] = []
WHITELIST?.split(' ') WHITELIST?.split(' ')
?.filter((url) => !!url) ?.filter((url) => !!url)
.forEach((url) => { .forEach((url) => {
@@ -14,8 +25,6 @@ export const configureCors = (app: Express) => {
// removing trailing slash of URLs listing for CORS // removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, '')) whiteList.push(url.replace(/\/$/, ''))
}) })
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
} }
return whiteList
} }

View File

@@ -0,0 +1,29 @@
import { Express } from 'express'
import { checkDomain } from '../middlewares'
import { getWhiteListed } from './configureCors'
export const configureDomains = (app: Express) => {
// const domains: string[] = []
const domains = new Set<string>()
const { ALLOWED_DOMAINS } = process.env
const allowedDomains = ALLOWED_DOMAINS?.trim().split(' ') ?? []
const whiteListed = getWhiteListed()
const combinedUrls = [...allowedDomains, ...whiteListed]
combinedUrls
.filter((domainName) => !!domainName)
.forEach((url) => {
try {
const domain = new URL(url)
domains.add(domain.hostname)
} catch (_) {}
})
if (domains.size) {
process.allowedDomains = [...domains]
console.log('Allowed Domain(s):', process.allowedDomains)
app.use(checkDomain)
}
}

View File

@@ -1,10 +1,9 @@
import { Express } from 'express' import { Express, CookieOptions } from 'express'
import mongoose from 'mongoose' import mongoose from 'mongoose'
import session from 'express-session' import session from 'express-session'
import MongoStore from 'connect-mongo' import MongoStore from 'connect-mongo'
import { ModeType } from '../utils' import { ModeType, ProtocolType } from '../utils'
import { cookieOptions } from '../app'
export const configureExpressSession = (app: Express) => { export const configureExpressSession = (app: Express) => {
const { MODE } = process.env const { MODE } = process.env
@@ -19,6 +18,15 @@ export const configureExpressSession = (app: Express) => {
}) })
} }
const { PROTOCOL } = process.env
const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: 'sas.4gl.io'
}
app.use( app.use(
session({ session({
secret: process.secrets.SESSION_SECRET, secret: process.secrets.SESSION_SECRET,

View File

@@ -1,4 +1,5 @@
export * from './configureCors' export * from './configureCors'
export * from './configureDomains'
export * from './configureExpressSession' export * from './configureExpressSession'
export * from './configureLogger' export * from './configureLogger'
export * from './configureSecurity' export * from './configureSecurity'

View File

@@ -1,6 +1,5 @@
import path from 'path' import path from 'path'
import express, { ErrorRequestHandler } from 'express' import express, { ErrorRequestHandler } from 'express'
import csrf, { CookieOptions } from 'csurf'
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import dotenv from 'dotenv' import dotenv from 'dotenv'
@@ -9,7 +8,6 @@ import {
getWebBuildFolder, getWebBuildFolder,
instantiateLogger, instantiateLogger,
loadAppStreamConfig, loadAppStreamConfig,
ProtocolType,
ReturnCode, ReturnCode,
setProcessVariables, setProcessVariables,
setupFolders, setupFolders,
@@ -17,6 +15,7 @@ import {
} from './utils' } from './utils'
import { import {
configureCors, configureCors,
configureDomains,
configureExpressSession, configureExpressSession,
configureLogger, configureLogger,
configureSecurity configureSecurity
@@ -30,24 +29,7 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express() const app = express()
const { PROTOCOL } = process.env
export const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
/***********************************
* CSRF Protection *
***********************************/
export const csrfProtection = csrf({ cookie: cookieOptions })
const onError: ErrorRequestHandler = (err, req, res, next) => { const onError: ErrorRequestHandler = (err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
console.error(err.stack) console.error(err.stack)
res.status(500).send('Something broke!') res.status(500).send('Something broke!')
} }
@@ -67,6 +49,11 @@ export default setProcessVariables().then(async () => {
***********************************/ ***********************************/
configureCors(app) configureCors(app)
/***********************************
* Allowed Domains *
***********************************/
configureDomains(app)
/*********************************** /***********************************
* DB Connection & * * DB Connection & *
* Express Sessions * * Express Sessions *

View File

@@ -1,6 +1,6 @@
import { RequestHandler, Request, Response, NextFunction } from 'express' import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app' import { csrfProtection } from './'
import { import {
fetchLatestAutoExec, fetchLatestAutoExec,
ModeType, ModeType,

View File

@@ -10,9 +10,7 @@ import { getPath, isPublicRoute } from '../utils'
export const authorize: RequestHandler = async (req, res, next) => { export const authorize: RequestHandler = async (req, res, next) => {
const { user } = req const { user } = req
if (!user) { if (!user) return res.sendStatus(401)
return res.sendStatus(401)
}
// no need to check for permissions when user is admin // no need to check for permissions when user is admin
if (user.isAdmin) return next() if (user.isAdmin) return next()

View File

@@ -0,0 +1,17 @@
import { RequestHandler } from 'express'
export const checkDomain: RequestHandler = (req, res, next) => {
const { allowedDomains } = process
// pass if no allowed domain is specified
if (!allowedDomains.length) return next()
if (allowedDomains.includes(req.hostname)) return next()
console.log('allowedDomains', allowedDomains)
console.log('hostname not allowed', req.hostname)
res.writeHead(404, {
'Content-Type': 'text/plain'
})
return res.end('Not found')
}

View File

@@ -0,0 +1,32 @@
import { RequestHandler } from 'express'
import csrf from 'csrf'
const csrfTokens = new csrf()
const secret = csrfTokens.secretSync()
export const generateCSRFToken = () => csrfTokens.create(secret)
export const csrfProtection: RequestHandler = (req, res, next) => {
if (req.method === 'GET') return next()
// Reads the token from the following locations, in order:
// req.body.csrf_token - typically generated by the body-parser module.
// req.query.csrf_token - a built-in from Express.js to read from the URL query string.
// req.headers['csrf-token'] - the CSRF-Token HTTP request header.
// req.headers['xsrf-token'] - the XSRF-Token HTTP request header.
// req.headers['x-csrf-token'] - the X-CSRF-Token HTTP request header.
// req.headers['x-xsrf-token'] - the X-XSRF-Token HTTP request header.
const token =
req.body?.csrf_token ||
req.query?.csrf_token ||
req.headers['csrf-token'] ||
req.headers['xsrf-token'] ||
req.headers['x-csrf-token'] ||
req.headers['x-xsrf-token']
if (!csrfTokens.verify(secret, token)) {
return res.status(400).send('Invalid CSRF token!')
}
next()
}

View File

@@ -1,5 +1,7 @@
export * from './authenticateToken' export * from './authenticateToken'
export * from './authorize'
export * from './checkDomain'
export * from './csrfProtection'
export * from './desktop' export * from './desktop'
export * from './verifyAdmin' export * from './verifyAdmin'
export * from './verifyAdminIfNeeded' export * from './verifyAdminIfNeeded'
export * from './authorize'

View File

@@ -49,10 +49,9 @@ describe('web', () => {
describe('SASLogon/login', () => { describe('SASLogon/login', () => {
let csrfToken: string let csrfToken: string
let cookies: string
beforeAll(async () => { beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app)) ;({ csrfToken } = await getCSRF(app))
}) })
afterEach(async () => { afterEach(async () => {
@@ -66,7 +65,6 @@ describe('web', () => {
const res = await request(app) const res = await request(app)
.post('/SASLogon/login') .post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken) .set('x-xsrf-token', csrfToken)
.send({ .send({
username: user.username, username: user.username,
@@ -82,15 +80,45 @@ describe('web', () => {
isAdmin: user.isAdmin isAdmin: user.isAdmin
}) })
}) })
it('should respond with Bad Request if CSRF Token is not present', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if CSRF Token is invalid', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
}) })
describe('SASLogon/authorize', () => { describe('SASLogon/authorize', () => {
let csrfToken: string let csrfToken: string
let cookies: string
let authCookies: string let authCookies: string
beforeAll(async () => { beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app)) ;({ csrfToken } = await getCSRF(app))
await userController.createUser(user) await userController.createUser(user)
@@ -99,12 +127,7 @@ describe('web', () => {
password: user.password password: user.password
} }
;({ cookies: authCookies } = await performLogin( ;({ authCookies } = await performLogin(app, credentials, csrfToken))
app,
credentials,
cookies,
csrfToken
))
}) })
afterAll(async () => { afterAll(async () => {
@@ -116,17 +139,28 @@ describe('web', () => {
it('should respond with authorization code', async () => { it('should respond with authorization code', async () => {
const res = await request(app) const res = await request(app)
.post('/SASLogon/authorize') .post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; ')) .set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken) .set('x-xsrf-token', csrfToken)
.send({ clientId }) .send({ clientId })
expect(res.body).toHaveProperty('code') expect(res.body).toHaveProperty('code')
}) })
it('should respond with Bad Request if CSRF Token is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.send({ clientId })
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientId is missing', async () => { it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app) const res = await request(app)
.post('/SASLogon/authorize') .post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; ')) .set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken) .set('x-xsrf-token', csrfToken)
.send({}) .send({})
.expect(400) .expect(400)
@@ -138,7 +172,7 @@ describe('web', () => {
it('should respond with Forbidden if clientId is incorrect', async () => { it('should respond with Forbidden if clientId is incorrect', async () => {
const res = await request(app) const res = await request(app)
.post('/SASLogon/authorize') .post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; ')) .set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken) .set('x-xsrf-token', csrfToken)
.send({ .send({
clientId: 'WrongClientID' clientId: 'WrongClientID'
@@ -153,27 +187,22 @@ describe('web', () => {
const getCSRF = async (app: Express) => { const getCSRF = async (app: Express) => {
// make request to get CSRF // make request to get CSRF
const { header, text } = await request(app).get('/') const { text } = await request(app).get('/')
const cookies = header['set-cookie'].join()
const csrfToken = extractCSRF(text) return { csrfToken: extractCSRF(text) }
return { csrfToken, cookies }
} }
const performLogin = async ( const performLogin = async (
app: Express, app: Express,
credentials: { username: string; password: string }, credentials: { username: string; password: string },
cookies: string,
csrfToken: string csrfToken: string
) => { ) => {
const { header } = await request(app) const { header } = await request(app)
.post('/SASLogon/login') .post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken) .set('x-xsrf-token', csrfToken)
.send(credentials) .send(credentials)
const newCookies: string = header['set-cookie'].join() return { authCookies: header['set-cookie'].join() }
return { cookies: newCookies }
} }
const extractCSRF = (text: string) => const extractCSRF = (text: string) =>

View File

@@ -1,6 +1,6 @@
import path from 'path' import path from 'path'
import express, { Request } from 'express' import express, { Request } from 'express'
import { authenticateAccessToken } from '../../middlewares' import { authenticateAccessToken, generateCSRFToken } from '../../middlewares'
import { folderExists } from '@sasjs/utils' import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils' import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
@@ -13,7 +13,7 @@ const router = express.Router()
router.get('/', authenticateAccessToken, async (req, res) => { router.get('/', authenticateAccessToken, async (req, res) => {
const content = appStreamHtml(process.appStreamConfig) const content = appStreamHtml(process.appStreamConfig)
res.cookie('XSRF-TOKEN', req.csrfToken()) res.cookie('XSRF-TOKEN', generateCSRFToken())
return res.send(content) return res.send(content)
}) })

View File

@@ -4,7 +4,7 @@ import webRouter from './web'
import apiRouter from './api' import apiRouter from './api'
import appStreamRouter from './appStream' import appStreamRouter from './appStream'
import { csrfProtection } from '../app' import { csrfProtection } from '../middlewares'
export const setupRoutes = (app: Express) => { export const setupRoutes = (app: Express) => {
app.use('/SASjsApi', apiRouter) app.use('/SASjsApi', apiRouter)

View File

@@ -1,4 +1,5 @@
import express from 'express' import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers' import { WebController } from '../../controllers'
import { MockSas9Controller } from '../../controllers/mock-sas9' import { MockSas9Controller } from '../../controllers/mock-sas9'
@@ -15,7 +16,7 @@ sas9WebRouter.get('/', async (req, res) => {
} catch (_) { } catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>' response = '<html><head></head><body>Web Build is not present</body></html>'
} finally { } finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>` const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace( const injectedContent = response?.replace(
'</head>', '</head>',
`${codeToInject}</head>` `${codeToInject}</head>`

View File

@@ -1,4 +1,5 @@
import express from 'express' import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web' import { WebController } from '../../controllers/web'
const sasViyaWebRouter = express.Router() const sasViyaWebRouter = express.Router()
@@ -11,7 +12,7 @@ sasViyaWebRouter.get('/', async (req, res) => {
} catch (_) { } catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>' response = '<html><head></head><body>Web Build is not present</body></html>'
} finally { } finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>` const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace( const injectedContent = response?.replace(
'</head>', '</head>',
`${codeToInject}</head>` `${codeToInject}</head>`

View File

@@ -1,4 +1,5 @@
import express from 'express' import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web' import { WebController } from '../../controllers/web'
import { authenticateAccessToken, desktopRestrict } from '../../middlewares' import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils' import { authorizeValidation, loginWebValidation } from '../../utils'
@@ -13,7 +14,7 @@ webRouter.get('/', async (req, res) => {
} catch (_) { } catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>' response = '<html><head></head><body>Web Build is not present</body></html>'
} finally { } finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>` const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace( const injectedContent = response?.replace(
'</head>', '</head>',
`${codeToInject}</head>` `${codeToInject}</head>`

View File

@@ -12,5 +12,6 @@ declare namespace NodeJS {
logger: import('@sasjs/utils/logger').Logger logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[] runTimes: import('../../utils').RunTimeType[]
secrets: import('../../model/Configuration').ConfigurationType secrets: import('../../model/Configuration').ConfigurationType
allowedDomains: string[]
} }
} }

View File

@@ -7,7 +7,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
const { user, accessToken } = req const { user, accessToken } = req
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN'] const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
const sessionId = req.cookies['connect.sid'] const sessionId = req.cookies['connect.sid']
const { _csrf } = req.cookies
const httpHeaders: string[] = [] const httpHeaders: string[] = []
@@ -16,7 +15,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
const cookies: string[] = [] const cookies: string[] = []
if (sessionId) cookies.push(`connect.sid=${sessionId}`) if (sessionId) cookies.push(`connect.sid=${sessionId}`)
if (_csrf) cookies.push(`_csrf=${_csrf}`)
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`) if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)

View File

@@ -1,6 +1,27 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import User from '../model/User' import User from '../model/User'
const isValidToken = async (
token: string,
key: string,
userId: number,
clientId: string
) => {
const promise = new Promise<boolean>((resolve, reject) =>
jwt.verify(token, key, (err, decoded) => {
if (err) return reject(false)
if (decoded?.userId === userId && decoded?.clientId === clientId) {
return resolve(true)
}
return reject(false)
})
)
return await promise.then(() => true).catch(() => false)
}
export const getTokensFromDB = async (userId: number, clientId: string) => { export const getTokensFromDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ id: userId }) const user = await User.findOne({ id: userId })
if (!user) return if (!user) return
@@ -13,22 +34,22 @@ export const getTokensFromDB = async (userId: number, clientId: string) => {
const accessToken = currentTokenObj.accessToken const accessToken = currentTokenObj.accessToken
const refreshToken = currentTokenObj.refreshToken const refreshToken = currentTokenObj.refreshToken
const verifiedAccessToken: any = jwt.verify( const isValidAccessToken = await isValidToken(
accessToken, accessToken,
process.secrets.ACCESS_TOKEN_SECRET process.secrets.ACCESS_TOKEN_SECRET,
userId,
clientId
) )
const verifiedRefreshToken: any = jwt.verify( const isValidRefreshToken = await isValidToken(
refreshToken, refreshToken,
process.secrets.REFRESH_TOKEN_SECRET process.secrets.REFRESH_TOKEN_SECRET,
userId,
clientId
) )
if ( if (isValidAccessToken && isValidRefreshToken) {
verifiedAccessToken?.userId === userId &&
verifiedAccessToken?.clientId === clientId &&
verifiedRefreshToken?.userId === userId &&
verifiedRefreshToken?.clientId === clientId
)
return { accessToken, refreshToken } return { accessToken, refreshToken }
}
} }
} }

View File

@@ -252,7 +252,7 @@ const verifyRUN_TIMES = (): string[] => {
return errors return errors
} }
const verifyExecutablePaths = () => { const verifyExecutablePaths = (): string[] => {
const errors: string[] = [] const errors: string[] = []
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } = const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
process.env process.env

View File

@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "ES6",
"module": "commonjs", "module": "commonjs",
"rootDir": "./", "rootDir": "./",
"outDir": "./build", "outDir": "./build",

8105
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "webpack-dev-server --config webpack.dev.ts --hot", "start": "vite serve --port 3000",
"build": "webpack --config webpack.prod.ts" "build": "vite build"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
@@ -30,38 +30,21 @@
"react-toastify": "^9.0.1" "react-toastify": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.0",
"@babel/node": "^7.16.0",
"@babel/plugin-proposal-class-properties": "^7.16.0",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/preset-typescript": "^7.16.0",
"@types/dotenv-webpack": "^7.0.3",
"@types/prismjs": "^1.16.6",
"@types/react": "^17.0.37", "@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.2", "@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.1", "@types/react-router-dom": "^5.3.1",
"babel-loader": "^8.2.3", "@vitejs/plugin-react": "^2.1.0",
"babel-plugin-prismjs": "^2.1.0",
"copy-webpack-plugin": "^10.0.0",
"css-loader": "^6.5.1",
"dotenv-webpack": "^7.1.0",
"eslint": "^8.5.0", "eslint": "^8.5.0",
"eslint-config-react-app": "^7.0.0", "eslint-config-react-app": "^7.0.0",
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "5.5.0",
"path": "0.12.7", "path": "0.12.7",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"sass": "^1.44.0", "sass": "^1.44.0",
"sass-loader": "^12.3.0",
"style-loader": "^3.3.1",
"ts-loader": "^9.2.6",
"typescript": "^4.5.2", "typescript": "^4.5.2",
"webpack": "5.64.3", "vite": "^3.1.4",
"webpack-cli": "^4.9.2", "vite-plugin-env-compatible": "^1.1.1",
"webpack-dev-server": "4.7.4" "vite-plugin-html": "^3.2.0",
"vite-plugin-monaco-editor": "^1.1.0"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@@ -35,5 +35,6 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
<script type="module" src="/src/index.tsx"></script>
</body> </body>
</html> </html>

17
web/vite.config.js Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { createHtmlPlugin } from 'vite-plugin-html'
import envCompatible from 'vite-plugin-env-compatible'
export default defineConfig({
build: {
outDir: './build'
},
plugins: [
react(),
createHtmlPlugin({
template: './src/index.html'
}),
envCompatible({ prefix: '' })
]
})