From 90479be1259bb7667dfdf6b1d156c61a031bc0f9 Mon Sep 17 00:00:00 2001 From: PikkaPikkachu Date: Thu, 3 Aug 2023 18:00:52 +0800 Subject: [PATCH 1/9] Experiment go <> sgid integration (#2247) feat: sgid integration with go --- .ebextensions/25-load-sgid-env.config | 41 +++ Dockerrun.aws.json | 6 + docker-compose.yml | 2 + package-lock.json | 259 ++++++++++++++++-- package.json | 8 +- .../app/components/pages/RootPage/index.tsx | 3 + src/client/app/util/types.ts | 1 + src/client/sgidLogin/index.tsx | 208 ++++++++++++++ src/server/api/index.ts | 1 + src/server/api/sgidLogin/index.ts | 16 ++ src/server/config.ts | 4 + src/server/constants.ts | 1 + src/server/index.ts | 2 + src/server/inversify.config.ts | 7 +- .../modules/auth/SgidLoginController.ts | 95 +++++++ .../auth/__tests__/LoginController.test.ts | 18 +- src/server/modules/auth/index.ts | 1 + .../modules/auth/interfaces/AuthService.ts | 9 + .../modules/auth/services/AuthService.ts | 6 + src/server/services/sgid.ts | 108 ++++++++ 20 files changed, 761 insertions(+), 35 deletions(-) create mode 100644 .ebextensions/25-load-sgid-env.config create mode 100644 src/client/sgidLogin/index.tsx create mode 100644 src/server/api/sgidLogin/index.ts create mode 100644 src/server/modules/auth/SgidLoginController.ts create mode 100644 src/server/services/sgid.ts diff --git a/.ebextensions/25-load-sgid-env.config b/.ebextensions/25-load-sgid-env.config new file mode 100644 index 000000000..97aab3d42 --- /dev/null +++ b/.ebextensions/25-load-sgid-env.config @@ -0,0 +1,41 @@ +# loads SGID environment variables to a .env file from SSM + +commands: + 01-create-env: + command: "/tmp/create-env.sh" + +files: + "/tmp/create-env.sh": + mode: "000755" + content : | + #!/bin/bash + ENV_NAME=$(/opt/elasticbeanstalk/bin/get-config environment -k SSM_PREFIX) + + ENV_VARS=("SGID_CLIENT_ID" "SGID_CLIENT_SECRET" "SGID_PRIVATE_KEY") + + echo "Set AWS region" + aws configure set default.region ap-southeast-1 + + TARGET_DIR=/etc/gogovsg + + echo "Checking if ${TARGET_DIR} exists..." + if [ ! -d ${TARGET_DIR} ]; then + echo "Creating directory ${TARGET_DIR} ..." + mkdir -p ${TARGET_DIR} + if [ $? -ne 0 ]; then + echo 'ERROR: Directory creation failed!' + exit 1 + fi + else + echo "Directory ${TARGET_DIR} already exists!" + fi + + echo "Creating config for ${ENV_NAME} in ${AWS_REGION}" + + for ENV_VAR in "${ENV_VARS[@]}"; do + echo "Running for this ${ENV_NAME}" + echo "Fetching ${ENV_VAR} from SSM" + VALUE=$(aws ssm get-parameter --name "${ENV_NAME}_${ENV_VAR}" --with-decryption --query "Parameter.Value" --output text) + echo "${ENV_VAR}=${VALUE}" >> $TARGET_DIR/.env + echo "Saved ${ENV_VAR}" + done diff --git a/Dockerrun.aws.json b/Dockerrun.aws.json index 894ebd200..8cd0436ac 100644 --- a/Dockerrun.aws.json +++ b/Dockerrun.aws.json @@ -8,5 +8,11 @@ { "ContainerPort": "8080" } + ], + "Volumes": [ + { + "HostDirectory": "/etc/gogovsg/.env", + "ContainerDirectory": "/usr/src/gogovsg/.env" + } ] } diff --git a/docker-compose.yml b/docker-compose.yml index f528987a0..a43fc7d3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,8 @@ services: - REDIS_SAFE_BROWSING_URI=redis://redis:6379/4 - SESSION_SECRET=thiscouldbeanything - GA_TRACKING_ID=UA-139330318-1 + - SGID_API_HOSTNAME=https://api.id.gov.sg + - OG_URL=https://go.gov.sg - VALID_EMAIL_GLOB_EXPRESSION=*.gov.sg - COOKIE_MAX_AGE=86400000 diff --git a/package-lock.json b/package-lock.json index 6aaa8e4b6..6096adc6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "GoGovSG", "version": "1.76.1", "license": "MIT", "dependencies": { @@ -13,6 +12,7 @@ "@hapi/joi": "^17.1.1", "@material-ui/core": "^4.11.4", "@material-ui/lab": "^4.0.0-alpha.61", + "@opengovsg/sgid-client": "^2.0.0", "@sentry/react": "^6.11.0", "@sentry/tracing": "^6.8.0", "@sentry/webpack-plugin": "^1.15.1", @@ -29,6 +29,7 @@ "clean-webpack-plugin": "^3.0.0", "cloudmersive-virus-api-client": "^1.2.2", "connect-redis": "^6.0.0", + "cookie-parser": "^1.4.6", "cookie-session": "^1.4.0", "copy-to-clipboard": "^3.3.1", "core-js": "^3.16.3", @@ -36,6 +37,7 @@ "datadog-winston": "^1.5.1", "date-fns-tz": "^1.3.4", "dd-trace": "^2.30.1", + "dotenv": "^16.3.1", "ejs": "^3.1.7", "express": "^4.17.3", "express-fileupload": "^1.4.0", @@ -103,6 +105,7 @@ "@types/classnames": "^2.3.1", "@types/cloudmersive-virus-api-client": "^1.1.1", "@types/connect-redis": "0.0.17", + "@types/cookie-parser": "^1.4.3", "@types/cookie-session": "^2.0.39", "@types/d3": "^6.7.3", "@types/datadog-winston": "^1.0.5", @@ -4788,6 +4791,16 @@ "node": ">= 8" } }, + "node_modules/@opengovsg/sgid-client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opengovsg/sgid-client/-/sgid-client-2.0.0.tgz", + "integrity": "sha512-zqcVQz03zB7dAwWh2MJVRAmHYjK1EryqOPnbBgrkr8Jx8BjtcjFa4cCrHstwWP1kVkGomhi0C7e3TRvf1qYSFQ==", + "dependencies": { + "jose": "4.9.2", + "node-rsa": "1.1.1", + "openid-client": "5.4.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", @@ -5304,6 +5317,15 @@ "@types/redis": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookie-session": { "version": "2.0.43", "resolved": "https://registry.npmjs.org/@types/cookie-session/-/cookie-session-2.0.43.tgz", @@ -6574,7 +6596,6 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -7018,7 +7039,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "dependencies": { "safer-buffer": "~2.1.0" } @@ -9323,6 +9343,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-session": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-1.4.0.tgz", @@ -10093,7 +10133,6 @@ "version": "2.23.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==", - "dev": true, "engines": { "node": ">=0.11" }, @@ -10994,6 +11033,17 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/dottie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.4.tgz", @@ -17192,6 +17242,14 @@ "node": ">= 0.6.0" } }, + "node_modules/jose": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.9.2.tgz", + "integrity": "sha512-EqKvu2PqJCD3Jrg3PvcYZVS7D21qMVLSYMDAFcOdGUEOpJSLNtJO7NjLANvu3SYHVl6pdP2ff7ve6EZW2nX7Nw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -19218,6 +19276,14 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==" }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "dependencies": { + "asn1": "^0.2.4" + } + }, "node_modules/node-source-walk": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-4.2.0.tgz", @@ -19402,6 +19468,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", @@ -19531,6 +19605,14 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -19581,6 +19663,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.0.tgz", + "integrity": "sha512-hgJa2aQKcM2hn3eyVtN12tEA45ECjTJPXCgUh5YzTzy9qwapCvmDTVPWOcWVL0d34zeQoQ/hbG9lJhl3AYxJlQ==", + "dependencies": { + "jose": "^4.10.0", + "lru-cache": "^6.0.0", + "object-hash": "^2.0.1", + "oidc-token-hash": "^5.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/opentracing": { "version": "0.14.7", "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz", @@ -30709,7 +30813,8 @@ "@material-ui/types": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", - "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==" + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "requires": {} }, "@material-ui/utils": { "version": "4.11.2", @@ -30747,6 +30852,16 @@ "fastq": "^1.6.0" } }, + "@opengovsg/sgid-client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@opengovsg/sgid-client/-/sgid-client-2.0.0.tgz", + "integrity": "sha512-zqcVQz03zB7dAwWh2MJVRAmHYjK1EryqOPnbBgrkr8Jx8BjtcjFa4cCrHstwWP1kVkGomhi0C7e3TRvf1qYSFQ==", + "requires": { + "jose": "4.9.2", + "node-rsa": "1.1.1", + "openid-client": "5.4.0" + } + }, "@opentelemetry/api": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", @@ -31017,7 +31132,7 @@ "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-1.16.0.tgz", "integrity": "sha512-Ax0QZ3a+LFYU876Si2HElPYSj+mX3vinvzH+o9F1g/5T2Z3HqITnX6gg+zVfLFsE819PN9KeLpmoHtO352dlmQ==", "requires": { - "@sentry/cli": "^1.75.2" + "@sentry/cli": "^1.67.1" } }, "@sinonjs/commons": { @@ -31185,6 +31300,15 @@ "@types/redis": "*" } }, + "@types/cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/cookie-session": { "version": "2.0.43", "resolved": "https://registry.npmjs.org/@types/cookie-session/-/cookie-session-2.0.43.tgz", @@ -32309,7 +32433,8 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.4.tgz", "integrity": "sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ==", - "dev": true + "dev": true, + "requires": {} }, "@webpack-cli/info": { "version": "1.3.0", @@ -32324,7 +32449,8 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.1.tgz", "integrity": "sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw==", - "dev": true + "dev": true, + "requires": {} }, "@xtuc/ieee754": { "version": "1.2.0", @@ -32360,8 +32486,7 @@ "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" }, "acorn-globals": { "version": "6.0.0", @@ -32385,13 +32510,15 @@ "acorn-import-assertions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==" + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "7.2.0", @@ -32432,12 +32559,14 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} }, "amdefine": { "version": "1.0.1", @@ -32691,7 +32820,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -34516,6 +34644,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-session": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-1.4.0.tgz", @@ -34764,7 +34908,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", - "dev": true + "dev": true, + "requires": {} }, "coveralls": { "version": "3.1.1", @@ -35130,13 +35275,13 @@ "date-fns": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", - "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==", - "dev": true + "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==" }, "date-fns-tz": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.4.tgz", - "integrity": "sha512-O47vEyz85F2ax/ZdhMBJo187RivZGjH6V0cPjPzpm/yi6YffJg4upD/8ibezO11ezZwP3QYlBHh/t4JhRNx0Ow==" + "integrity": "sha512-O47vEyz85F2ax/ZdhMBJo187RivZGjH6V0cPjPzpm/yi6YffJg4upD/8ibezO11ezZwP3QYlBHh/t4JhRNx0Ow==", + "requires": {} }, "dd-trace": { "version": "2.40.0", @@ -35825,6 +35970,11 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + }, "dottie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.4.tgz", @@ -36321,13 +36471,15 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true + "dev": true, + "requires": {} }, "eslint-import-resolver-alias": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", - "dev": true + "dev": true, + "requires": {} }, "eslint-import-resolver-node": { "version": "0.3.4", @@ -36688,7 +36840,8 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true + "dev": true, + "requires": {} }, "eslint-scope": { "version": "5.1.1", @@ -37043,7 +37196,8 @@ "express-joi-validation": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/express-joi-validation/-/express-joi-validation-4.0.3.tgz", - "integrity": "sha512-XnEyhlllurczZDx1vLPWnaohTAQzxlvaP7ifEbvRf2zvYC5C5ZZrgFH75g0/XcL7OuaZ0XlVtB0J0E/R0O1L4A==" + "integrity": "sha512-XnEyhlllurczZDx1vLPWnaohTAQzxlvaP7ifEbvRf2zvYC5C5ZZrgFH75g0/XcL7OuaZ0XlVtB0J0E/R0O1L4A==", + "requires": {} }, "express-rate-limit": { "version": "5.3.0", @@ -39840,7 +39994,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "26.0.0", @@ -40507,6 +40662,11 @@ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" }, + "jose": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.9.2.tgz", + "integrity": "sha512-EqKvu2PqJCD3Jrg3PvcYZVS7D21qMVLSYMDAFcOdGUEOpJSLNtJO7NjLANvu3SYHVl6pdP2ff7ve6EZW2nX7Nw==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -42109,6 +42269,14 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==" }, + "node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "requires": { + "asn1": "^0.2.4" + } + }, "node-source-walk": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-4.2.0.tgz", @@ -42255,6 +42423,11 @@ } } }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", @@ -42345,6 +42518,11 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -42383,6 +42561,24 @@ "mimic-fn": "^2.1.0" } }, + "openid-client": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.0.tgz", + "integrity": "sha512-hgJa2aQKcM2hn3eyVtN12tEA45ECjTJPXCgUh5YzTzy9qwapCvmDTVPWOcWVL0d34zeQoQ/hbG9lJhl3AYxJlQ==", + "requires": { + "jose": "^4.10.0", + "lru-cache": "^6.0.0", + "object-hash": "^2.0.1", + "oidc-token-hash": "^5.0.1" + }, + "dependencies": { + "jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==" + } + } + }, "opentracing": { "version": "0.14.7", "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz", @@ -42700,7 +42896,8 @@ "pg-pool": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", - "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==" + "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==", + "requires": {} }, "pg-protocol": { "version": "1.5.0", @@ -43499,7 +43696,8 @@ "react-ga": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.0.tgz", - "integrity": "sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ==" + "integrity": "sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ==", + "requires": {} }, "react-i18next": { "version": "11.11.4", @@ -43880,12 +44078,14 @@ "redux-devtools-extension": { "version": "2.13.9", "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz", - "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==" + "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==", + "requires": {} }, "redux-thunk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==" + "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", + "requires": {} }, "reflect-metadata": { "version": "0.1.13", @@ -48340,7 +48540,8 @@ "version": "7.5.3", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 6c6fa924c..8c7e2afd6 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "build": "tsc && webpack --mode production", "postbuild": "copyfiles -u 1 src/server/views/**/* build && copyfiles -u 4 src/server/modules/qr/assets/**/* build/server/modules/qr", "version": "auto-changelog -p && git add CHANGELOG.md", - "start": "node build/server/index.js", + "start": "node -r dotenv/config build/server/index.js", "client-dev": "webpack serve --mode development --host 0.0.0.0 --devtool inline-source-map --hot", - "server-dev": "ts-node-dev --poll --respawn --transpile-only --inspect=0.0.0.0 -- src/server/index.ts", + "server-dev": "ts-node-dev --poll --respawn --transpile-only --inspect=0.0.0.0 -r dotenv/config -- src/server/index.ts", "docker-dev": "concurrently \"npm run server-dev\" \"npm run client-dev\"", "dev": "docker-compose -f docker-compose.yml up --build", "test": "jest --collectCoverage", @@ -40,6 +40,7 @@ "@hapi/joi": "^17.1.1", "@material-ui/core": "^4.11.4", "@material-ui/lab": "^4.0.0-alpha.61", + "@opengovsg/sgid-client": "^2.0.0", "@sentry/react": "^6.11.0", "@sentry/tracing": "^6.8.0", "@sentry/webpack-plugin": "^1.15.1", @@ -56,6 +57,7 @@ "clean-webpack-plugin": "^3.0.0", "cloudmersive-virus-api-client": "^1.2.2", "connect-redis": "^6.0.0", + "cookie-parser": "^1.4.6", "cookie-session": "^1.4.0", "copy-to-clipboard": "^3.3.1", "core-js": "^3.16.3", @@ -63,6 +65,7 @@ "datadog-winston": "^1.5.1", "date-fns-tz": "^1.3.4", "dd-trace": "^2.30.1", + "dotenv": "^16.3.1", "ejs": "^3.1.7", "express": "^4.17.3", "express-fileupload": "^1.4.0", @@ -130,6 +133,7 @@ "@types/classnames": "^2.3.1", "@types/cloudmersive-virus-api-client": "^1.1.1", "@types/connect-redis": "0.0.17", + "@types/cookie-parser": "^1.4.3", "@types/cookie-session": "^2.0.39", "@types/d3": "^6.7.3", "@types/datadog-winston": "^1.0.5", diff --git a/src/client/app/components/pages/RootPage/index.tsx b/src/client/app/components/pages/RootPage/index.tsx index 5e8b51dfa..09476088c 100644 --- a/src/client/app/components/pages/RootPage/index.tsx +++ b/src/client/app/components/pages/RootPage/index.tsx @@ -10,6 +10,7 @@ import { History } from 'history' import PrivateRoute from '../../PrivateRoute' import HomePage from '../../../../home' import LoginPage from '../../../../login' +import SgidLoginPage from '../../../../sgidLogin' import UserPage from '../../../../user' import NotFoundPage from '../NotFoundPage' import DirectoryPage from '../../../../directory' @@ -30,6 +31,7 @@ import { HOME_PAGE, LOGIN_PAGE, NOT_FOUND_PAGE, + SGID_LOGIN_PAGE, USER_PAGE, } from '../../../util/types' import theme from '../../../theme' @@ -47,6 +49,7 @@ const Root: FunctionComponent = ({ store, history }: RootProps) => ( + diff --git a/src/client/app/util/types.ts b/src/client/app/util/types.ts index 131ab6647..7ca360554 100644 --- a/src/client/app/util/types.ts +++ b/src/client/app/util/types.ts @@ -1,5 +1,6 @@ export const HOME_PAGE = '/' export const LOGIN_PAGE = '/login' +export const SGID_LOGIN_PAGE = '/ogp-login' export const USER_PAGE = '/user' export const SEARCH_PAGE = '/search' export const NOT_FOUND_PAGE = '/404/:shortUrl' diff --git a/src/client/sgidLogin/index.tsx b/src/client/sgidLogin/index.tsx new file mode 100644 index 000000000..e361c1cec --- /dev/null +++ b/src/client/sgidLogin/index.tsx @@ -0,0 +1,208 @@ +import React, { useEffect } from 'react' +import i18next from 'i18next' +import { + Button, + Hidden, + Link, + Typography, + createStyles, + makeStyles, +} from '@material-ui/core' +import GoLogo from '@assets/go-logo-graphics/go-main-logo.svg' +import LoginGraphics from '@assets/login-page-graphics/login-page-graphics.svg' +import { useDispatch } from 'react-redux' +import rootActions from '../app/components/pages/RootPage/actions' + +import assetVariant from '../../shared/util/asset-variant' + +import { htmlSanitizer } from '../app/util/format' +import Section from '../app/components/Section' +import BaseLayout from '../app/components/BaseLayout' +import { get } from '../app/util/requests' + +const URL_PREFIX_LENGTH = '#/ogp-login'.length + +const useStyles = makeStyles((theme) => + createStyles({ + container: { + display: 'flex', + flexGrow: 1, + '-ms-flex': '1 1 auto', + }, + loginContainer: { + display: 'flex', + width: '100%', + [theme.breakpoints.up('lg')]: { + width: '50%', + }, + }, + verticalAlign: { + display: 'flex', + width: '100%', + [theme.breakpoints.up('lg')]: { + alignItems: 'center', + }, + }, + signInButton: { + width: '250px', + minWidth: '120px', + marginRight: theme.spacing(2), + }, + loginWrapper: { + display: 'block', + [theme.breakpoints.up('lg')]: { + // Gives the contents slightly more than enough height, + // so that validation messages do not shift the centering. + height: '400px', + maxHeight: '80vh', + }, + }, + headerGroup: { + marginBottom: theme.spacing(4), + }, + logo: { + maxWidth: '130px', + width: '40%', + }, + loginHeader: { + marginTop: theme.spacing(1), + }, + loginReferral: { + fontSize: '0.85rem', + color: '#767676', + marginBottom: theme.spacing(4), + }, + graphicColorFill: { + backgroundColor: theme.palette.primary.dark, + width: '50vw', + height: '100%', + // allocates space for the government masthead when on gov variant + maxHeight: assetVariant === 'gov' ? 'calc(100vh - 28px)' : '100vh', + textAlign: 'center', + overflow: 'hidden', + }, + loginGraphic: { + userDrag: 'none', + height: '100%', + }, + '@media screen\\0': { + // Styles for Internet Explorer compatibility + logoLink: { + marginBottom: '0', + }, + }, + }), +) + +const SgidLoginPage = (): JSX.Element => { + const dispatch = useDispatch() + const classes = useStyles() + const queryParams = new URLSearchParams( + new URL(window.location.href).hash.substring(URL_PREFIX_LENGTH), + ) + + const setLoginStatusMessage = (message: string) => + dispatch(rootActions.setInfoMessage(message)) + + useEffect(() => { + const officerEmail = queryParams.get('officerEmail') + const statusCode = queryParams.get('statusCode') + if (officerEmail) { + setLoginStatusMessage( + `${officerEmail} doesn't look like a valid ${i18next.t( + 'general.emailDomain', + )} email.`, + ) + } else if (statusCode === '403') { + setLoginStatusMessage( + `Unable to fetch a valid work email for authentication.`, + ) + } else if (statusCode === '400') { + setLoginStatusMessage(`Authentication failed. Please try again.`) + } + }, []) + + return ( + +
+
+ +
+ Login page graphic +
+
+
+
+
+
+
+ + + GoGovSG logo + + + {/* NOTE: dangerouslySetInnerHTML is used as copy includes tag */} + + + {i18next.t('login.referrals.1.officerPhrase')} can use their{' '} + {i18next.t('login.referrals.1.emailDomain')} emails at{' '} + + {i18next.t('login.referrals.1.link')} + + , and {i18next.t('login.referrals.2.officerPhrase')} can use + their {i18next.t('login.referrals.2.emailDomain')} emails at{' '} + + {i18next.t('login.referrals.2.link')} + {' '} + to shorten links. + + + +
+
+
+
+
+
+ ) +} + +export default SgidLoginPage diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 30da753ce..261da7355 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -16,6 +16,7 @@ const router = Express.Router() /* Public routes that do not need to be protected */ router.use('/logout', require('./logout')) router.use('/login', require('./login')) +router.use('/sgidLogin', require('./sgidLogin')) router.use('/stats', require('./statistics')) router.use('/sentry', require('./sentry')) router.use('/links', require('./links')) diff --git a/src/server/api/sgidLogin/index.ts b/src/server/api/sgidLogin/index.ts new file mode 100644 index 000000000..3a3ad9ef5 --- /dev/null +++ b/src/server/api/sgidLogin/index.ts @@ -0,0 +1,16 @@ +import Express from 'express' +import { SgidLoginController } from '../../modules/auth' +import { container } from '../../util/inversify' +import { DependencyIds } from '../../constants' + +const router: Express.Router = Express.Router() + +const sgidLoginController = container.get( + DependencyIds.sgidLoginController, +) + +router.get('/authurl', sgidLoginController.generateAuthUrl) + +router.get('/authenticate', sgidLoginController.handleLogin) + +module.exports = router diff --git a/src/server/config.ts b/src/server/config.ts index 6d0cae35c..604eb0752 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -272,3 +272,7 @@ export const ffExternalApi: boolean = process.env.FF_EXTERNAL_API === 'true' export const apiAdmins: string[] = process.env.ADMIN_API_EMAILS ? process.env.ADMIN_API_EMAILS.split(',') : [] +export const sgidClientId = process.env.SGID_CLIENT_ID || '' +export const sgidPrivateKey = process.env.SGID_PRIVATE_KEY || '' +export const sgidClientSecret = process.env.SGID_CLIENT_SECRET || '' +export const sgidApiHostname = process.env.SGID_API_HOSTNAME || '' diff --git a/src/server/constants.ts b/src/server/constants.ts index 27386c928..3525c6b18 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -35,6 +35,7 @@ export const DependencyIds = { linksController: Symbol.for('linksController'), authService: Symbol.for('authService'), loginController: Symbol.for('loginController'), + sgidLoginController: Symbol.for('sgidLoginController'), logoutController: Symbol.for('logoutController'), urlManagementService: Symbol.for('urlManagementService'), userController: Symbol.for('userController'), diff --git a/src/server/index.ts b/src/server/index.ts index 0ee01b901..b4c068bdf 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -7,6 +7,7 @@ import helmet from 'helmet' import morgan from 'morgan' import session from 'express-session' import cookieSession from 'cookie-session' +import cookieParser from 'cookie-parser' import connectRedis from 'connect-redis' import jsonMessage from './util/json' import bindInversifyDependencies from './inversify.config' @@ -94,6 +95,7 @@ if (sentryDns) { } const app = express() +app.use(cookieParser()) app.use( helmet({ contentSecurityPolicy: { diff --git a/src/server/inversify.config.ts b/src/server/inversify.config.ts index 2e6308152..08ab8e7fa 100644 --- a/src/server/inversify.config.ts +++ b/src/server/inversify.config.ts @@ -49,7 +49,11 @@ import { RotatingLinksController } from './modules/display/RotatingLinksControll import { SentryController } from './modules/sentry/SentryController' import { AuthService, CryptographyBcrypt } from './modules/auth/services' -import { LoginController, LogoutController } from './modules/auth' +import { + LoginController, + LogoutController, + SgidLoginController, +} from './modules/auth' import { UrlManagementService } from './modules/user/services' import { UserController } from './modules/user' import { DirectoryController } from './modules/directory' @@ -130,6 +134,7 @@ export default () => { bindIfUnbound(DependencyIds.linksController, RotatingLinksController) bindIfUnbound(DependencyIds.sentryController, SentryController) bindIfUnbound(DependencyIds.loginController, LoginController) + bindIfUnbound(DependencyIds.sgidLoginController, SgidLoginController) bindIfUnbound(DependencyIds.authService, AuthService) bindIfUnbound(DependencyIds.logoutController, LogoutController) bindIfUnbound(DependencyIds.urlManagementService, UrlManagementService) diff --git a/src/server/modules/auth/SgidLoginController.ts b/src/server/modules/auth/SgidLoginController.ts new file mode 100644 index 000000000..d16011636 --- /dev/null +++ b/src/server/modules/auth/SgidLoginController.ts @@ -0,0 +1,95 @@ +import Express, { CookieOptions } from 'express' +import { inject, injectable } from 'inversify' +import { SGID_LOGIN_OAUTH_STATE, SgidAuthService } from '../../services/sgid' +import { DependencyIds } from '../../constants' +import { logger } from '../../config' +import { isValidGovEmail } from '../../util/email' +import { AuthService } from './interfaces' +import jsonMessage from '../../util/json' + +export const OFFICER_EMAIL_SCOPE = 'ogpofficerinfo.work_email' +const SGID_STATE_COOKIE_NAME = 'gogovsg_sgid_state' +const SgidStateCookieConfig: CookieOptions = { + httpOnly: true, +} + +@injectable() +export class SgidLoginController { + private sgidService + + private authService: AuthService + + constructor(@inject(DependencyIds.authService) authService: AuthService) { + this.sgidService = SgidAuthService + this.authService = authService + } + + public generateAuthUrl: ( + req: Express.Request, + res: Express.Response, + ) => void = (_req, res) => { + try { + const { url, codeVerifier, nonce } = this.sgidService.authorizationUrl() + + res.cookie( + SGID_STATE_COOKIE_NAME, + { codeVerifier, nonce }, + SgidStateCookieConfig, + ) + res.send(url) + return + } catch (err) { + logger.error(err) + res.status(400) + res.badRequest( + jsonMessage('SGID login not supported by edu and health domains.'), + ) + return + } + } + + public handleLogin = async (req: Express.Request, res: Express.Response) => { + try { + const { code, state } = req.query + const sessionData = req.cookies[SGID_STATE_COOKIE_NAME] + + if (state !== SGID_LOGIN_OAUTH_STATE) { + res.redirect(`/#/ogp-login?statusCode=400`) + return + } + + const { sub, accessToken } = await this.sgidService.callback( + String(code), + String(sessionData.nonce), + String(sessionData.codeVerifier), + ) + + const { data } = await this.sgidService.userinfo(accessToken, sub) + const officerEmail = data[OFFICER_EMAIL_SCOPE] + + if ( + !officerEmail || + !officerEmail.length || + !isValidGovEmail(officerEmail) + ) { + // redirect back to sgid login page, if authentication fails, + // or officer email is not valid + res.redirect(`/#/ogp-login?statusCode=403&officerEmail=${officerEmail}`) + return + } + const dbUser = await this.authService.genDBUserWithOfficerEmail( + data[OFFICER_EMAIL_SCOPE], + ) + + req.session!.user = dbUser + + res.redirect(`/#/user`) + return + } catch (error) { + logger.error(error) + res.status(500).render('error', { error }) + } + } +} + +export default SgidLoginController diff --git a/src/server/modules/auth/__tests__/LoginController.test.ts b/src/server/modules/auth/__tests__/LoginController.test.ts index e324e2d8f..70433dede 100644 --- a/src/server/modules/auth/__tests__/LoginController.test.ts +++ b/src/server/modules/auth/__tests__/LoginController.test.ts @@ -39,7 +39,11 @@ describe('LoginController', () => { loggerErrorSpy.mockClear() }) describe('getIsLoggedIn', () => { - const authService = { generateOtp: jest.fn(), verifyOtp: jest.fn() } + const authService = { + generateOtp: jest.fn(), + verifyOtp: jest.fn(), + genDBUserWithOfficerEmail: jest.fn(), + } const controller = new LoginController(authService) test('session contains user', () => { @@ -64,7 +68,11 @@ describe('LoginController', () => { }) describe('getLoginMessage', () => { - const authService = { generateOtp: jest.fn(), verifyOtp: jest.fn() } + const authService = { + generateOtp: jest.fn(), + verifyOtp: jest.fn(), + genDBUserWithOfficerEmail: jest.fn(), + } const controller = new LoginController(authService) test('returns login message', () => { @@ -78,7 +86,11 @@ describe('LoginController', () => { }) describe('getEmailDomains', () => { - const authService = { generateOtp: jest.fn(), verifyOtp: jest.fn() } + const authService = { + generateOtp: jest.fn(), + verifyOtp: jest.fn(), + genDBUserWithOfficerEmail: jest.fn(), + } const controller = new LoginController(authService) test('returns domains', () => { diff --git a/src/server/modules/auth/index.ts b/src/server/modules/auth/index.ts index b3025d475..eaaff0719 100644 --- a/src/server/modules/auth/index.ts +++ b/src/server/modules/auth/index.ts @@ -1,5 +1,6 @@ export { LoginController } from './LoginController' export { LogoutController } from './LogoutController' +export { SgidLoginController } from './SgidLoginController' export type EmailProperty = { email: string diff --git a/src/server/modules/auth/interfaces/AuthService.ts b/src/server/modules/auth/interfaces/AuthService.ts index d6e232b96..9218ae7ea 100644 --- a/src/server/modules/auth/interfaces/AuthService.ts +++ b/src/server/modules/auth/interfaces/AuthService.ts @@ -17,4 +17,13 @@ export interface AuthService { * @returns Promise that resolves to the user if the otp is valid. */ verifyOtp(email: string, otp: string): Promise + + /** + * Generates the user with the officer email provided, if the + * user with the specified officer email does not exist then we + * create a new user for the input email. + * @param {string} email Email of the user. + * @returns Promise that creates or find the existing user. + */ + genDBUserWithOfficerEmail(email: string): Promise } diff --git a/src/server/modules/auth/services/AuthService.ts b/src/server/modules/auth/services/AuthService.ts index 6e5178e0c..8331da907 100644 --- a/src/server/modules/auth/services/AuthService.ts +++ b/src/server/modules/auth/services/AuthService.ts @@ -125,6 +125,12 @@ export class AuthService implements interfaces.AuthService { throw new Error('Error creating user.') } } + + public genDBUserWithOfficerEmail: ( + officerEmail: string, + ) => Promise = async (officerEmail) => { + return this.userRepository.findOrCreateWithEmail(officerEmail) + } } export default AuthService diff --git a/src/server/services/sgid.ts b/src/server/services/sgid.ts new file mode 100644 index 000000000..1a74cb296 --- /dev/null +++ b/src/server/services/sgid.ts @@ -0,0 +1,108 @@ +import SgidClient, { generatePkcePair } from '@opengovsg/sgid-client' +import { + logger, + ogUrl, + sgidApiHostname, + sgidClientId, + sgidClientSecret, + sgidPrivateKey, +} from '../config' + +interface SgidServiceOption { + clientId: string + clientSecret: string + privateKey: string + redirectUri: string + hostname: string +} + +export const SGID_LOGIN_OAUTH_STATE = 'login' + +class SgidService { + // SGID client will be null for other domains like edu, health + // so we will need to handle cases for those as well, so that + // they continue to work in prod without sgid client being initialised + private sgidClient: SgidClient | null + + constructor({ + clientId, + clientSecret, + privateKey, + redirectUri, + hostname, + }: SgidServiceOption) { + try { + this.sgidClient = new SgidClient({ + clientId, + clientSecret, + privateKey, + redirectUri, + hostname, + }) + logger.info('SGID client initialised successfully') + } catch (e) { + logger.error('SGID client initialisation failed', e) + this.sgidClient = null + } + } + + /** + * Fetches the url via sgid SDK. + */ + authorizationUrl(): { url: string; codeVerifier: string; nonce?: string } { + if (!this.sgidClient) { + throw new Error('SGID client not initialised') + } + try { + const { codeChallenge, codeVerifier } = generatePkcePair() + const { url, nonce } = this.sgidClient.authorizationUrl({ + state: SGID_LOGIN_OAUTH_STATE, + scope: ['openid', 'ogpofficerinfo.work_email'].join(' '), + codeChallenge, + }) + return { url, codeVerifier, nonce } + } catch (e) { + throw new Error(`Error retrieving url via sgid-client ${e.message}`) + } + } + + /** + * Fetches the token via sgid SDK. + */ + async callback(code: string, nonce: string, codeVerifier: string) { + if (!code || !this.sgidClient) + throw new Error(`code cannot be empty or sgid client not initialised`) + try { + const { sub, accessToken } = await this.sgidClient.callback({ + code, + nonce, + codeVerifier, + }) + return { sub, accessToken } + } catch (e) { + throw new Error('Error retrieving access token via sgid-client') + } + } + + /** + * Fetches the user info via sgid SDK. + */ + async userinfo(accessToken: string, sub: string) { + if (!accessToken || !this.sgidClient) + throw new Error(`accessToken cannot be empty`) + try { + return await this.sgidClient.userinfo({ accessToken, sub }) + } catch (e) { + throw new Error('Error retrieving user info via sgid-client') + } + } +} + +// Initialised the sgidService object with the different environments +export const SgidAuthService = new SgidService({ + clientId: sgidClientId, + clientSecret: sgidClientSecret, + privateKey: sgidPrivateKey, + redirectUri: `${ogUrl}/api/sgidLogin/authenticate`, + hostname: sgidApiHostname, +}) From 6b3e5df49796cc04e6e2e07801083d49da6d381a Mon Sep 17 00:00:00 2001 From: PikkaPikkachu Date: Thu, 3 Aug 2023 18:20:02 +0800 Subject: [PATCH 2/9] 1.77.0 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7aa72ada..73bbacab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,15 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v1.77.0](https://github.com/opengovsg/GoGovSG/compare/v1.76.1...v1.77.0) + +- Experiment go <> sgid integration [`#2247`](https://github.com/opengovsg/GoGovSG/pull/2247) +- [develop] 1.76.1 [`#2241`](https://github.com/opengovsg/GoGovSG/pull/2241) + #### [v1.76.1](https://github.com/opengovsg/GoGovSG/compare/v1.76.0...v1.76.1) +> 13 July 2023 + - build(deps): bump semver from 7.3.8 to 7.5.4 [`#2239`](https://github.com/opengovsg/GoGovSG/pull/2239) - build(deps): bump react-vis from 1.11.7 to 1.11.8 [`#2238`](https://github.com/opengovsg/GoGovSG/pull/2238) - build(deps): bump @sentry/cli from 1.67.2 to 1.75.2 [`#2231`](https://github.com/opengovsg/GoGovSG/pull/2231) diff --git a/package-lock.json b/package-lock.json index 6096adc6d..3fad5af14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "GoGovSG", - "version": "1.76.1", + "version": "1.77.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.76.1", + "version": "1.77.0", "license": "MIT", "dependencies": { "@datadog/browser-rum": "^4.15.0", diff --git a/package.json b/package.json index 8c7e2afd6..cc9258b35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GoGovSG", - "version": "1.76.1", + "version": "1.77.0", "description": "Link shortener for Singapore government.", "main": "src/server/index.js", "scripts": { From 801c4ae5ac998c11c0542cccecba941ed368ff7f Mon Sep 17 00:00:00 2001 From: thamsimun <39295749+thamsimun@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:32:10 +0800 Subject: [PATCH 3/9] fix: stored xss via file upload (#2258) * fix: stored xss via file upload * chore: refactor tests * fix: e2e test * fix: update relative changed file path in e2e --- src/server/api/user/index.ts | 6 +- src/server/constants.ts | 1 + src/server/inversify.config.ts | 8 +- .../modules/threat/FileCheckController.ts | 27 +++--- .../__tests__/FileCheckController.test.ts | 89 ++++++++++++++++--- .../interfaces/FileTypeFilterService.ts | 16 ++-- .../threat/services/FileTypeFilterService.ts | 39 +++++--- .../__tests__/FileTypeFilterService.test.ts | 88 +++++++++++------- test/end-to-end/UrlCreation.test.ts | 2 +- test/end-to-end/util/config.ts | 4 +- 10 files changed, 198 insertions(+), 82 deletions(-) diff --git a/src/server/api/user/index.ts b/src/server/api/user/index.ts index 8895619dc..7d37531b3 100644 --- a/src/server/api/user/index.ts +++ b/src/server/api/user/index.ts @@ -99,7 +99,7 @@ router.post( preprocessFormData, validator.body(urlSchema), fileCheckController.singleFileCheck, - fileCheckController.fileExtensionCheck(), + fileCheckController.fileExtensionAndMimeTypeCheck(), fileCheckController.fileVirusCheck, urlCheckController.singleUrlCheck, userController.createUrl, @@ -117,7 +117,7 @@ router.post( preprocessFormData, validator.body(urlBulkSchema), fileCheckController.singleFileCheck, - fileCheckController.fileExtensionCheck(['csv']), + fileCheckController.fileExtensionAndMimeTypeCheck(['csv']), fileCheckController.fileVirusCheck, bulkController.validateAndParseCsv, urlCheckController.bulkUrlCheck, @@ -144,7 +144,7 @@ router.patch( preprocessFormData, validator.body(urlEditSchema), fileCheckController.singleFileCheck, - fileCheckController.fileExtensionCheck(), + fileCheckController.fileExtensionAndMimeTypeCheck(), fileCheckController.fileVirusCheck, urlCheckController.singleUrlCheck, userController.updateUrl, diff --git a/src/server/constants.ts b/src/server/constants.ts index 3525c6b18..8cb9fd7fe 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -51,6 +51,7 @@ export const DependencyIds = { urlHistoryRepository: Symbol.for('urlHistoryRepository'), deviceCheckService: Symbol.for('deviceCheckService'), allowedFileExtensions: Symbol.for('allowedFileExtensions'), + fileExtensionsMimeTypeMap: Symbol.for('fileExtensionsMimeTypeMap'), fileTypeFilterService: Symbol.for('fileTypeFilterService'), cloudmersiveKey: Symbol.for('cloudmersiveKey'), cloudmersiveClient: Symbol.for('cloudmersiveClient'), diff --git a/src/server/inversify.config.ts b/src/server/inversify.config.ts index 08ab8e7fa..f0ea23481 100644 --- a/src/server/inversify.config.ts +++ b/src/server/inversify.config.ts @@ -72,7 +72,10 @@ import { UrlHistoryRepository } from './modules/audit/repositories' import { SafeBrowsingMapper } from './modules/threat/mappers' import { SafeBrowsingRepository } from './modules/threat/repositories/SafeBrowsingRepository' -import { DEFAULT_ALLOWED_FILE_EXTENSIONS } from './modules/threat/services/FileTypeFilterService' +import { + DEFAULT_ALLOWED_FILE_EXTENSIONS, + FILE_EXTENSION_MIME_TYPE_MAP, +} from './modules/threat/services/FileTypeFilterService' import { CloudmersiveScanService, FileTypeFilterService, @@ -153,6 +156,9 @@ export default () => { container .bind(DependencyIds.allowedFileExtensions) .toConstantValue(DEFAULT_ALLOWED_FILE_EXTENSIONS) + container + .bind(DependencyIds.fileExtensionsMimeTypeMap) + .toConstantValue(FILE_EXTENSION_MIME_TYPE_MAP) bindIfUnbound(DependencyIds.fileTypeFilterService, FileTypeFilterService) if (cloudmersiveKey) { diff --git a/src/server/modules/threat/FileCheckController.ts b/src/server/modules/threat/FileCheckController.ts index 30a9ab821..b77c8e9fa 100644 --- a/src/server/modules/threat/FileCheckController.ts +++ b/src/server/modules/threat/FileCheckController.ts @@ -43,19 +43,26 @@ export class FileCheckController { next() } - public fileExtensionCheck = + public fileExtensionAndMimeTypeCheck = (allowedExtensions?: string[]) => async (req: Request, res: Response, next: NextFunction): Promise => { const file = req.files?.file as fileUpload.UploadedFile | undefined - if ( - file && - !(await this.fileTypeFilterService.hasAllowedType( - file, - allowedExtensions, - )) - ) { - res.unsupportedMediaType(jsonMessage('File type disallowed.')) - return + + if (file) { + const fileTypeData = + await this.fileTypeFilterService.getExtensionAndMimeType(file) + if ( + fileTypeData.extension === '' || + !(await this.fileTypeFilterService.hasAllowedExtensionType( + fileTypeData.extension, + allowedExtensions, + )) + ) { + res.unsupportedMediaType(jsonMessage('File type disallowed.')) + return + } + + file.mimetype = fileTypeData.mimeType } next() diff --git a/src/server/modules/threat/__tests__/FileCheckController.test.ts b/src/server/modules/threat/__tests__/FileCheckController.test.ts index 74de75aa0..24e527efc 100644 --- a/src/server/modules/threat/__tests__/FileCheckController.test.ts +++ b/src/server/modules/threat/__tests__/FileCheckController.test.ts @@ -1,6 +1,7 @@ /* eslint-disable import/prefer-default-export */ import httpMocks from 'node-mocks-http' import { Request } from 'express' +import fileUpload from 'express-fileupload' import { FileCheckController } from '..' @@ -18,19 +19,20 @@ export function createRequestWithFile(file: any): Request { describe('FileCheckController test', () => { const file = { data: Buffer.from('data'), name: 'file.csv' } - const getExtension = jest.fn() - const hasAllowedType = jest.fn() + const getExtensionAndMimeType = jest.fn() + const hasAllowedExtensionType = jest.fn() const scanFile = jest.fn() const controller = new FileCheckController( - { getExtension, hasAllowedType }, + { getExtensionAndMimeType, hasAllowedExtensionType }, { scanFile }, ) const badRequest = jest.fn() beforeEach(() => { - hasAllowedType.mockClear() + hasAllowedExtensionType.mockClear() + getExtensionAndMimeType.mockClear() scanFile.mockClear() badRequest.mockClear() }) @@ -42,10 +44,15 @@ describe('FileCheckController test', () => { const afterFileExtensionCheck = jest.fn() const afterFileVirusCheck = jest.fn() await controller.singleFileCheck(req, res, afterSingleFileCheck) - await controller.fileExtensionCheck()(req, res, afterFileExtensionCheck) + await controller.fileExtensionAndMimeTypeCheck()( + req, + res, + afterFileExtensionCheck, + ) await controller.fileVirusCheck(req, res, afterFileVirusCheck) - expect(hasAllowedType).not.toHaveBeenCalled() + expect(hasAllowedExtensionType).not.toHaveBeenCalled() + expect(getExtensionAndMimeType).not.toHaveBeenCalled() expect(scanFile).not.toHaveBeenCalled() expect(afterSingleFileCheck).toHaveBeenCalled() expect(afterFileExtensionCheck).toHaveBeenCalled() @@ -72,12 +79,22 @@ describe('FileCheckController test', () => { const res = httpMocks.createResponse() as any const afterFileExtensionCheck = jest.fn() - hasAllowedType.mockResolvedValue(false) + getExtensionAndMimeType.mockResolvedValue({ + extension: 'svg', + mimeType: 'text/plain', + }) + hasAllowedExtensionType.mockResolvedValue(false) + res.unsupportedMediaType = badRequest - await controller.fileExtensionCheck()(req, res, afterFileExtensionCheck) + await controller.fileExtensionAndMimeTypeCheck()( + req, + res, + afterFileExtensionCheck, + ) - expect(hasAllowedType).toHaveBeenCalled() + expect(hasAllowedExtensionType).toHaveBeenCalled() + expect(getExtensionAndMimeType).toHaveBeenCalled() expect(res.unsupportedMediaType).toHaveBeenCalled() expect(afterFileExtensionCheck).not.toHaveBeenCalled() }) @@ -120,7 +137,12 @@ describe('FileCheckController test', () => { const afterFileExtensionCheck = jest.fn() const afterFileVirusCheck = jest.fn() - hasAllowedType.mockResolvedValue(true) + hasAllowedExtensionType.mockResolvedValue(true) + getExtensionAndMimeType.mockResolvedValue({ + extension: 'csv', + mimeType: 'text/csv', + }) + scanFile.mockResolvedValue(false) res.badRequest = badRequest @@ -129,11 +151,20 @@ describe('FileCheckController test', () => { res.unsupportedMediaType = badRequest await controller.singleFileCheck(req, res, afterSingleFileCheck) - await controller.fileExtensionCheck()(req, res, afterFileExtensionCheck) + await controller.fileExtensionAndMimeTypeCheck()( + req, + res, + afterFileExtensionCheck, + ) await controller.fileVirusCheck(req, res, afterFileVirusCheck) - - expect(hasAllowedType).toHaveBeenCalled() + expect(getExtensionAndMimeType).toHaveBeenCalled() + expect(hasAllowedExtensionType).toHaveBeenCalled() expect(scanFile).toHaveBeenCalled() + const fileUploaded = req.files?.file as fileUpload.UploadedFile | undefined + expect(fileUploaded).toBeDefined() + if (fileUploaded) { + expect(fileUploaded.mimetype).toEqual('text/csv') + } expect(res.badRequest).not.toHaveBeenCalled() expect(res.serverError).not.toHaveBeenCalled() @@ -144,4 +175,36 @@ describe('FileCheckController test', () => { expect(afterFileExtensionCheck).toHaveBeenCalled() expect(afterFileVirusCheck).toHaveBeenCalled() }) + + it('unsupportedMediaType when file extension is empty', async () => { + const req = createRequestWithFile(file) + const res = httpMocks.createResponse() as any + + const afterSingleFileCheck = jest.fn() + const afterFileExtensionCheck = jest.fn() + + scanFile.mockResolvedValue(false) + + res.badRequest = badRequest + res.serverError = badRequest + res.unprocessableEntity = badRequest + res.unsupportedMediaType = badRequest + + getExtensionAndMimeType.mockResolvedValue({ + extension: '', + mimeType: 'text/plain', + }) + + await controller.singleFileCheck(req, res, afterSingleFileCheck) + await controller.fileExtensionAndMimeTypeCheck()( + req, + res, + afterFileExtensionCheck, + ) + + expect(getExtensionAndMimeType).toHaveBeenCalled() + expect(hasAllowedExtensionType).not.toHaveBeenCalled() + expect(res.unsupportedMediaType).toHaveBeenCalled() + expect(afterFileExtensionCheck).not.toHaveBeenCalled() + }) }) diff --git a/src/server/modules/threat/interfaces/FileTypeFilterService.ts b/src/server/modules/threat/interfaces/FileTypeFilterService.ts index 8750ed0ac..a7b3bd275 100644 --- a/src/server/modules/threat/interfaces/FileTypeFilterService.ts +++ b/src/server/modules/threat/interfaces/FileTypeFilterService.ts @@ -1,16 +1,18 @@ export interface FileTypeFilterService { - getExtension: (file: { + getExtensionAndMimeType: (file: { name: string data: Buffer - }) => Promise + }) => Promise - hasAllowedType: ( - file: { - name: string - data: Buffer - }, + hasAllowedExtensionType: ( + extension: string, allowedExtensions?: string[], ) => Promise } +export interface FileTypeData { + extension: string + mimeType: string +} + export default FileTypeFilterService diff --git a/src/server/modules/threat/services/FileTypeFilterService.ts b/src/server/modules/threat/services/FileTypeFilterService.ts index 3b78a2036..db09b3b0c 100644 --- a/src/server/modules/threat/services/FileTypeFilterService.ts +++ b/src/server/modules/threat/services/FileTypeFilterService.ts @@ -2,6 +2,7 @@ import FileType from 'file-type' import { inject, injectable } from 'inversify' import * as interfaces from '../interfaces' import { DependencyIds } from '../../../constants' +import { FileTypeData } from '../interfaces/FileTypeFilterService' export const DEFAULT_ALLOWED_FILE_EXTENSIONS = [ 'avi', @@ -28,35 +29,49 @@ export const DEFAULT_ALLOWED_FILE_EXTENSIONS = [ 'zip', ] +export const FILE_EXTENSION_MIME_TYPE_MAP = new Map([ + ['csv', 'text/csv'], + ['dwf', 'application/x-dwf'], + ['dxf', 'application/dxf'], +]) + @injectable() export class FileTypeFilterService implements interfaces.FileTypeFilterService { allowedFileExtensions: string[] + fileExtensionsMimeTypeMap: Map + constructor( @inject(DependencyIds.allowedFileExtensions) allowedFileExtensions: string[], + @inject(DependencyIds.fileExtensionsMimeTypeMap) + fileExtensionsMimeTypeMap: Map, ) { this.allowedFileExtensions = allowedFileExtensions + this.fileExtensionsMimeTypeMap = fileExtensionsMimeTypeMap } - getExtension: (file: { + getExtensionAndMimeType: (file: { name: string data: Buffer - }) => Promise = async ({ name, data }) => { + }) => Promise = async ({ name, data }) => { const fileType = await FileType.fromBuffer(data) - return fileType?.ext || name.split('.').pop() + let ext: string | undefined = fileType?.ext + let mimeType: string | undefined = fileType?.mime + if (!ext || !mimeType) { + ext = name.split('.').pop() + mimeType = this.fileExtensionsMimeTypeMap.get(ext ?? '') + } + return { + extension: ext ?? '', + mimeType: mimeType ?? 'text/plain', + } } - hasAllowedType: ( - file: { - name: string - data: Buffer - }, + hasAllowedExtensionType: ( + extension: string, allowedExtensions?: string[], - ) => Promise = async (file, allowedExtensions) => { - const extension = await this.getExtension(file) - if (!extension) return false - + ) => Promise = async (extension, allowedExtensions) => { if (allowedExtensions && allowedExtensions.length > 0) { return allowedExtensions.includes(extension) } diff --git a/src/server/modules/threat/services/__tests__/FileTypeFilterService.test.ts b/src/server/modules/threat/services/__tests__/FileTypeFilterService.test.ts index fc1f2a7ff..fce2889ac 100644 --- a/src/server/modules/threat/services/__tests__/FileTypeFilterService.test.ts +++ b/src/server/modules/threat/services/__tests__/FileTypeFilterService.test.ts @@ -1,60 +1,82 @@ import { FileTypeFilterService } from '..' describe('FileTypeFilterService', () => { - const service = new FileTypeFilterService(['csv', 'xml']) + const service = new FileTypeFilterService( + ['csv', 'xml'], + new Map([ + ['csv', 'text/csv'], + ['dwf', 'application/x-dwf'], + ['dxf', 'application/dxf'], + ]), + ) - it('allows files not detected by file-type but via extension', async () => { - const result = await service.hasAllowedType({ + it('get extension and mime type from csv file not detected by file-type', async () => { + const fileTypeData = await service.getExtensionAndMimeType({ data: Buffer.from('name,phone\nabc,123\n'), name: 'file.csv', }) - expect(result).toBeTruthy() + + expect(fileTypeData.extension).toEqual('csv') + expect(fileTypeData.mimeType).toEqual('text/csv') }) - it('allows files detected by file-type', async () => { - const result = await service.hasAllowedType({ - data: Buffer.from(''), - name: 'file.notreallyxml', + it('get extension and mime type from dwf file not detected by file-type', async () => { + const fileTypeData = await service.getExtensionAndMimeType({ + data: Buffer.from('(DWF V06.00)PK'), + name: 'test.dwf', }) - expect(result).toBeTruthy() + + expect(fileTypeData.extension).toEqual('dwf') + expect(fileTypeData.mimeType).toEqual('application/x-dwf') }) - it('disallows files not via file-type but via extension', async () => { - const result = await service.hasAllowedType({ - data: Buffer.from(''), - name: 'file.jpg', + it('get extension and mime type from dxf file not detected by file-type', async () => { + const fileTypeData = await service.getExtensionAndMimeType({ + data: Buffer.from('0\nSECTION\n2\nENTITIES\n0\nLINE\n8'), + name: 'test.dxf', }) - expect(result).toBeFalsy() + + expect(fileTypeData.extension).toEqual('dxf') + expect(fileTypeData.mimeType).toEqual('application/dxf') + }) + + it('get extension and mime type from svg file not detected by file-type', async () => { + const fileTypeData = await service.getExtensionAndMimeType({ + data: Buffer.from(''), + name: 'test.svg', + }) + + expect(fileTypeData.extension).toEqual('svg') + expect(fileTypeData.mimeType).toEqual('text/plain') }) - it('disallows files via file-type', async () => { - const noXML = new FileTypeFilterService(['csv']) - const result = await noXML.hasAllowedType({ + it('get extension and mime type from file detected by file', async () => { + const fileTypeData = await service.getExtensionAndMimeType({ data: Buffer.from(''), name: 'file.notreallyxml', }) + + expect(fileTypeData.extension).toEqual('xml') + expect(fileTypeData.mimeType).toEqual('application/xml') + }) + + it('allows file extension type that are in allowed extension type', async () => { + const result = await service.hasAllowedExtensionType('csv') + expect(result).toBeTruthy() + }) + + it('disallows file extension type that are not in allowed extension type', async () => { + const result = await service.hasAllowedExtensionType('svg') expect(result).toBeFalsy() }) - it('allow files not detected by file-type from custom file extensions', async () => { - const result = await service.hasAllowedType( - { - data: Buffer.from('name,phone\nabc,123\n'), - name: 'file.png', - }, - ['png'], - ) + it('allow file extension type that are from custom file extensions', async () => { + const result = await service.hasAllowedExtensionType('png', ['png']) expect(result).toBeTruthy() }) - it('does not allow files not detected by file-type from custom file extensions', async () => { - const result = await service.hasAllowedType( - { - data: Buffer.from('name,phone\nabc,123\n'), - name: 'file.jpeg', - }, - ['png'], - ) + it('does not allow file extension type not from custom file extensions', async () => { + const result = await service.hasAllowedExtensionType('jpeg', ['png']) expect(result).toBeFalsy() }) }) diff --git a/test/end-to-end/UrlCreation.test.ts b/test/end-to-end/UrlCreation.test.ts index 8d7622060..be5aa87f2 100644 --- a/test/end-to-end/UrlCreation.test.ts +++ b/test/end-to-end/UrlCreation.test.ts @@ -325,7 +325,7 @@ test('The update file test', async (t) => { const generatedfileUrl = await shortUrlTextField.value const fileRow = Selector(`h6[title="${generatedfileUrl}"]`) - const directoryPath = `${process.env.HOME}/Downloads/${generatedfileUrl}.pdf` + const directoryPath = `${process.env.HOME}/Downloads/${generatedfileUrl}.csv` // Generate 1mb file await createEmptyFileOfSize(dummyFilePath, smallFileSize) diff --git a/test/end-to-end/util/config.ts b/test/end-to-end/util/config.ts index 9fa3fbb7e..69b8c9634 100644 --- a/test/end-to-end/util/config.ts +++ b/test/end-to-end/util/config.ts @@ -16,8 +16,8 @@ export const dummyMaliciousFilePath = './test/end-to-end/eicar.com.txt' export const dummyMaliciousRelativePath = './eicar.com.txt' export const dummyFilePath = './test/end-to-end/anotherDummy.txt' export const dummyRelativePath = './anotherDummy.txt' -export const dummyChangedFilePath = './test/end-to-end/changedDummy.pdf' -export const dummyRelativeChangedFilePath = './changedDummy.pdf' +export const dummyChangedFilePath = './test/end-to-end/changedDummy.csv' +export const dummyRelativeChangedFilePath = './changedDummy.csv' export const dummyBulkCsv = './test/end-to-end/bulkCsv.csv' export const dummyBulkCsvRelativePath = './bulkCsv.csv' export const smallFileSize = 1024 * 1024 * 1 From feab32cb216e5bd6d9ac7e65fa8be5212a094ce3 Mon Sep 17 00:00:00 2001 From: thamsimun Date: Thu, 7 Sep 2023 15:08:16 +0800 Subject: [PATCH 4/9] 1.77.1 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73bbacab2..065b870c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,15 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v1.77.1](https://github.com/opengovsg/GoGovSG/compare/v1.77.0...v1.77.1) + +- fix: stored xss via file upload [`#2258`](https://github.com/opengovsg/GoGovSG/pull/2258) +- Develop 1.77.0 [`#2250`](https://github.com/opengovsg/GoGovSG/pull/2250) + #### [v1.77.0](https://github.com/opengovsg/GoGovSG/compare/v1.76.1...v1.77.0) +> 3 August 2023 + - Experiment go <> sgid integration [`#2247`](https://github.com/opengovsg/GoGovSG/pull/2247) - [develop] 1.76.1 [`#2241`](https://github.com/opengovsg/GoGovSG/pull/2241) diff --git a/package-lock.json b/package-lock.json index 3fad5af14..fd3f788b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "GoGovSG", - "version": "1.77.0", + "version": "1.77.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.77.0", + "version": "1.77.1", "license": "MIT", "dependencies": { "@datadog/browser-rum": "^4.15.0", diff --git a/package.json b/package.json index cc9258b35..0fe54477f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GoGovSG", - "version": "1.77.0", + "version": "1.77.1", "description": "Link shortener for Singapore government.", "main": "src/server/index.js", "scripts": { From c6321f2a0258bf69f2068e4de660c19a44dfa0dc Mon Sep 17 00:00:00 2001 From: halfwhole <41856541+halfwhole@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:01:04 +0800 Subject: [PATCH 5/9] build(deps): bump import-in-the-middle from 1.4.1 to 1.4.2 (#2259) --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd3f788b1..127807602 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14373,9 +14373,9 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.1.tgz", - "integrity": "sha512-hGG0PcCsykVo8MBVH8l0uEWLWW6DXMgJA9jvC0yps6M3uIJ8L/tagTCbyF8Ud5TtqJ8/jmZL1YkyySyeVkVQrA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", "dependencies": { "acorn": "^8.8.2", "acorn-import-assertions": "^1.9.0", @@ -38545,9 +38545,9 @@ } }, "import-in-the-middle": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.1.tgz", - "integrity": "sha512-hGG0PcCsykVo8MBVH8l0uEWLWW6DXMgJA9jvC0yps6M3uIJ8L/tagTCbyF8Ud5TtqJ8/jmZL1YkyySyeVkVQrA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", "requires": { "acorn": "^8.8.2", "acorn-import-assertions": "^1.9.0", From ae740a339008aa5bf660450ad4b0dd3e83aadfce Mon Sep 17 00:00:00 2001 From: gweiying <39231249+gweiying@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:09:28 +0800 Subject: [PATCH 6/9] [Snyk] Security upgrade sharp from 0.30.7 to 0.32.6 (#2277) * fix: package.json to reduce vulnerabilities --------- Co-authored-by: snyk-bot Co-authored-by: PikkaPikkachu --- package-lock.json | 178 ++++++++++++++++++++++++++++++++-------------- package.json | 2 +- 2 files changed, 125 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index 127807602..bf7fe94cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,7 +84,7 @@ "regenerator-runtime": "^0.13.8", "sanitize-html": "^2.7.1", "sequelize": "^6.29.3", - "sharp": "^0.30.7", + "sharp": "^0.32.6", "ua-parser-js": "^0.7.28", "uuid": "^8.3.2", "validator": "^13.7.0", @@ -4487,14 +4487,6 @@ "node": ">=10" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "engines": { - "node": ">=8" - } - }, "node_modules/@mapbox/node-pre-gyp/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7231,6 +7223,11 @@ "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", "dev": true }, + "node_modules/b4a": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" + }, "node_modules/babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -10673,6 +10670,14 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -12882,6 +12887,11 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-glob": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", @@ -20514,14 +20524,6 @@ "node": ">=10" } }, - "node_modules/prebuild-install/node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "engines": { - "node": ">=8" - } - }, "node_modules/precinct": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/precinct/-/precinct-8.3.0.tgz", @@ -21039,6 +21041,11 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -22882,33 +22889,50 @@ } }, "node_modules/sharp": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", - "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.1", - "node-addon-api": "^5.0.0", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", - "semver": "^7.3.7", + "semver": "^7.5.4", "simple-get": "^4.0.1", - "tar-fs": "^2.1.1", + "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" }, "engines": { - "node": ">=12.13.0" + "node": ">=14.15.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/sharp/node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "engines": { - "node": ">=8" + "node_modules/sharp/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node_modules/sharp/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/sharp/node_modules/tar-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", + "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, "node_modules/shebang-command": { @@ -23549,6 +23573,15 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", + "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -30661,11 +30694,6 @@ "readable-stream": "^3.6.0" } }, - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -32968,6 +32996,11 @@ "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", "dev": true }, + "b4a": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" + }, "babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -35681,6 +35714,11 @@ "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==", "dev": true }, + "detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -37368,6 +37406,11 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "fast-glob": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", @@ -43204,13 +43247,6 @@ "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" - }, - "dependencies": { - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - } } }, "precinct": { @@ -43604,6 +43640,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -45053,24 +45094,44 @@ } }, "sharp": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", - "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", "requires": { "color": "^4.2.3", - "detect-libc": "^2.0.1", - "node-addon-api": "^5.0.0", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", - "semver": "^7.3.7", + "semver": "^7.5.4", "simple-get": "^4.0.1", - "tar-fs": "^2.1.1", + "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" }, "dependencies": { - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + "node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "requires": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "tar-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", + "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } } } }, @@ -45590,6 +45651,15 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" }, + "streamx": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", + "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", + "requires": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", diff --git a/package.json b/package.json index 0fe54477f..4fa6b6170 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "regenerator-runtime": "^0.13.8", "sanitize-html": "^2.7.1", "sequelize": "^6.29.3", - "sharp": "^0.30.7", + "sharp": "^0.32.6", "ua-parser-js": "^0.7.28", "uuid": "^8.3.2", "validator": "^13.7.0", From b3a5bee1c0c7f694618ceb2549d8b976724089c2 Mon Sep 17 00:00:00 2001 From: PikkaPikkachu Date: Mon, 2 Oct 2023 10:14:56 +0800 Subject: [PATCH 7/9] 1.77.2 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 065b870c2..c7e9ed9ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,16 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v1.77.2](https://github.com/opengovsg/GoGovSG/compare/v1.77.1...v1.77.2) + +- [Snyk] Security upgrade sharp from 0.30.7 to 0.32.6 [`#2277`](https://github.com/opengovsg/GoGovSG/pull/2277) +- build(deps): bump import-in-the-middle from 1.4.1 to 1.4.2 [`#2259`](https://github.com/opengovsg/GoGovSG/pull/2259) +- [Develop] 1.77.1 [`#2267`](https://github.com/opengovsg/GoGovSG/pull/2267) + #### [v1.77.1](https://github.com/opengovsg/GoGovSG/compare/v1.77.0...v1.77.1) +> 7 September 2023 + - fix: stored xss via file upload [`#2258`](https://github.com/opengovsg/GoGovSG/pull/2258) - Develop 1.77.0 [`#2250`](https://github.com/opengovsg/GoGovSG/pull/2250) diff --git a/package-lock.json b/package-lock.json index bf7fe94cc..125b8a16c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "GoGovSG", - "version": "1.77.1", + "version": "1.77.2", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.77.1", + "version": "1.77.2", "license": "MIT", "dependencies": { "@datadog/browser-rum": "^4.15.0", diff --git a/package.json b/package.json index 4fa6b6170..6b560107f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GoGovSG", - "version": "1.77.1", + "version": "1.77.2", "description": "Link shortener for Singapore government.", "main": "src/server/index.js", "scripts": { From a8ae4079a210484ed77997a0ffa8faf3e6808644 Mon Sep 17 00:00:00 2001 From: halfwhole <41856541+halfwhole@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:33:02 +0800 Subject: [PATCH 8/9] feat: remove verify message button linking to checkwho (#2284) --- public/locales/gov/en/translation.json | 3 +-- .../app/base-layout/checkwho-icon.svg | 3 --- .../app/base-layout/checkwho-icon.svg | 3 --- .../app/base-layout/checkwho-icon.svg | 3 --- .../BaseLayout/BaseLayoutHeader.tsx | 26 ------------------- 5 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 src/client/app/assets/edu/components/app/base-layout/checkwho-icon.svg delete mode 100644 src/client/app/assets/gov/components/app/base-layout/checkwho-icon.svg delete mode 100644 src/client/app/assets/health/components/app/base-layout/checkwho-icon.svg diff --git a/public/locales/gov/en/translation.json b/public/locales/gov/en/translation.json index 501ced066..fdac3373e 100644 --- a/public/locales/gov/en/translation.json +++ b/public/locales/gov/en/translation.json @@ -28,8 +28,7 @@ "builtBy": "https://open.gov.sg", "linkedin": "https://sg.linkedin.com/company/open-government-products", "facebook": "https://www.facebook.com/opengovsg", - "apiDoc": "https://guide.go.gov.sg/developer-guide/api-documentation", - "verifyMessages": "https://check.go.gov.sg" + "apiDoc": "https://guide.go.gov.sg/developer-guide/api-documentation" }, "builtBy": "Built by Open Government Products" }, diff --git a/src/client/app/assets/edu/components/app/base-layout/checkwho-icon.svg b/src/client/app/assets/edu/components/app/base-layout/checkwho-icon.svg deleted file mode 100644 index 235db5eff..000000000 --- a/src/client/app/assets/edu/components/app/base-layout/checkwho-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/client/app/assets/gov/components/app/base-layout/checkwho-icon.svg b/src/client/app/assets/gov/components/app/base-layout/checkwho-icon.svg deleted file mode 100644 index 235db5eff..000000000 --- a/src/client/app/assets/gov/components/app/base-layout/checkwho-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/client/app/assets/health/components/app/base-layout/checkwho-icon.svg b/src/client/app/assets/health/components/app/base-layout/checkwho-icon.svg deleted file mode 100644 index 235db5eff..000000000 --- a/src/client/app/assets/health/components/app/base-layout/checkwho-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/client/app/components/BaseLayout/BaseLayoutHeader.tsx b/src/client/app/components/BaseLayout/BaseLayoutHeader.tsx index 5ac124a6b..f26a98e63 100644 --- a/src/client/app/components/BaseLayout/BaseLayoutHeader.tsx +++ b/src/client/app/components/BaseLayout/BaseLayoutHeader.tsx @@ -18,7 +18,6 @@ import GoLogoMiniLight from '@assets/go-logo-graphics/go-main-logo-mini-light.sv import helpIcon from '@assets/shared/help-icon.svg' import logoutIcon from '@assets/components/app/base-layout/logout-icon.svg' import logoutWhiteIcon from '@assets/components/app/base-layout/logout-white-icon.svg' -import checkwhoIcon from '@assets/components/app/base-layout/checkwho-icon.svg' import directoryIcon from '@assets/components/app/base-layout/directory-icon.svg' import feedbackIcon from '@assets/components/app/base-layout/feedback-icon.svg' import githubIcon from '@assets/components/app/base-layout/github-icon.svg' @@ -28,7 +27,6 @@ import homeIcon from '@assets/components/app/base-layout/home-icon.svg' import Section from '../Section' import loginActions from '../../../login/actions' import { GoGovReduxState } from '../../reducers/types' -import assetVariant from '../../../../shared/util/asset-variant' type StyleProps = { isLoggedIn: boolean @@ -121,21 +119,6 @@ type BaseLayoutHeaderProps = { toStick: boolean } -type HeaderButtonProps = { - text: string - link: string - public: boolean - icon: string - mobileOrder?: number - internalLink?: boolean - displayNotEnabledForVariant?: string[] -} - -function isEnabledForAssetVariant(header: HeaderButtonProps) { - if (header.displayNotEnabledForVariant === undefined) return true - return !header.displayNotEnabledForVariant.includes(assetVariant) -} - const BaseLayoutHeader: FunctionComponent = ({ backgroundType, hideNavButtons = false, @@ -211,14 +194,6 @@ const BaseLayoutHeader: FunctionComponent = ({ icon: feedbackIcon, mobileOrder: 5, }, - { - text: 'Verify Messages', - link: i18next.t('general.links.verifyMessages'), - public: true, - icon: checkwhoIcon, - mobileOrder: 6, - displayNotEnabledForVariant: ['edu', 'health'], - }, ] const appBarBtn = isLoggedIn ? ( @@ -302,7 +277,6 @@ const BaseLayoutHeader: FunctionComponent = ({ {!hideNavButtons && headers.map( (header) => - isEnabledForAssetVariant(header) && (header.public ? !isLoggedIn : isLoggedIn) && (