From 007e8c4442be584789560f476fcdd1cb4beb1ec0 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 25 Apr 2023 23:46:21 +0800 Subject: [PATCH] initial setup for google signin signup integration --- .env.example | 6 + backend/package-lock.json | 255 ++++++++++++++++++ backend/package.json | 6 +- backend/src/config/index.ts | 3 + backend/src/controllers/v1/authController.ts | 4 + backend/src/index.ts | 8 +- backend/src/middleware/requestErrorHandler.ts | 2 +- backend/src/models/user.ts | 10 +- backend/src/routes/v1/auth.ts | 62 ++++- backend/src/types/express/index.d.ts | 7 + frontend/package-lock.json | 11 + frontend/package.json | 1 + .../components/utilities/SecurityClient.ts | 12 + frontend/src/config/request.ts | 5 + frontend/src/const.ts | 3 +- frontend/src/hooks/useProviderAuth.ts | 47 ++++ frontend/src/pages/login/provider/success.tsx | 16 ++ frontend/src/pages/signup.tsx | 4 +- nginx/default.dev.conf | 9 +- 19 files changed, 455 insertions(+), 16 deletions(-) create mode 100644 frontend/src/hooks/useProviderAuth.ts create mode 100644 frontend/src/pages/login/provider/success.tsx diff --git a/.env.example b/.env.example index df316753e4..69271e3d06 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 +JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201 # JWT lifetime # Optional lifetimes for JWT tokens expressed in seconds or a string @@ -15,6 +16,7 @@ JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 JWT_AUTH_LIFETIME= JWT_REFRESH_LIFETIME= JWT_SIGNUP_LIFETIME= +JWT_PROVIDER_AUTH_LIFETIME= # MongoDB # Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref @@ -67,3 +69,7 @@ STRIPE_PRODUCT_STARTER= STRIPE_PRODUCT_TEAM= STRIPE_PRODUCT_PRO= NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +SESSION_SECRET= diff --git a/backend/package-lock.json b/backend/package-lock.json index 0423f0cb0b..3d9a148a64 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -30,6 +30,7 @@ "dotenv": "^16.0.1", "express": "^4.18.1", "express-rate-limit": "^6.7.0", + "express-session": "^1.17.3", "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", @@ -41,6 +42,8 @@ "lodash": "^4.17.21", "mongoose": "^6.10.4", "nodemailer": "^6.8.0", + "passport": "^0.6.0", + "passport-google-oidc": "^0.1.0", "posthog-node": "^2.6.0", "query-string": "^7.1.3", "request-ip": "^3.3.0", @@ -63,11 +66,13 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", + "@types/express-session": "^1.17.7", "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", + "@types/passport": "^1.0.12", "@types/supertest": "^2.0.12", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", @@ -3938,6 +3943,15 @@ "@types/range-parser": "*" } }, + "node_modules/@types/express-session": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz", + "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -4032,6 +4046,15 @@ "@types/node": "*" } }, + "node_modules/@types/passport": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", + "integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/prettier": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", @@ -6141,6 +6164,37 @@ "express": "^4 || ^5" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/express-validator": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.15.0.tgz", @@ -11122,6 +11176,11 @@ "set-blocking": "^2.0.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -11149,6 +11208,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11274,6 +11341,62 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oidc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/passport-google-oidc/-/passport-google-oidc-0.1.0.tgz", + "integrity": "sha512-/TtFXaWvmubm5kXNoJMyzBfxhnZ0lnBPA6w6rmQMP9klmHZf0ArE8IrIEt3yAHoiDzGx4eTO7YasKQFbPsNtVA==", + "dependencies": { + "passport-openidconnect": "0.1.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-openidconnect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", + "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", + "dependencies": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11320,6 +11443,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -11607,6 +11735,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -12732,6 +12868,17 @@ "node": ">=0.8.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -16242,6 +16389,15 @@ "@types/range-parser": "*" } }, + "@types/express-session": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz", + "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -16336,6 +16492,15 @@ "@types/node": "*" } }, + "@types/passport": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", + "integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/prettier": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", @@ -17892,6 +18057,36 @@ "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", "requires": {} }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "express-validator": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.15.0.tgz", @@ -21498,6 +21693,11 @@ "set-blocking": "^2.0.0" } }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -21516,6 +21716,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -21605,6 +21810,38 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-google-oidc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/passport-google-oidc/-/passport-google-oidc-0.1.0.tgz", + "integrity": "sha512-/TtFXaWvmubm5kXNoJMyzBfxhnZ0lnBPA6w6rmQMP9klmHZf0ArE8IrIEt3yAHoiDzGx4eTO7YasKQFbPsNtVA==", + "requires": { + "passport-openidconnect": "0.1.x" + } + }, + "passport-openidconnect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz", + "integrity": "sha512-r0QJiWEzwCg2MeCIXVP5G6YxVRqnEsZ2HpgKRthZ9AiQHJrgGUytXpsdcGF9BRwd3yMrEesb/uG/Yxb86rrY0g==", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -21639,6 +21876,11 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -21840,6 +22082,11 @@ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -22660,6 +22907,14 @@ "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", "optional": true }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/backend/package.json b/backend/package.json index 084f6b08e1..1f4aa7f494 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,6 @@ "@octokit/rest": "^19.0.5", "@sentry/node": "^7.45.0", "@sentry/tracing": "^7.46.0", - "@sentry/node": "^7.41.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "argon2": "^0.30.3", @@ -22,6 +21,7 @@ "dotenv": "^16.0.1", "express": "^4.18.1", "express-rate-limit": "^6.7.0", + "express-session": "^1.17.3", "express-validator": "^6.14.2", "handlebars": "^4.7.7", "helmet": "^5.1.1", @@ -33,6 +33,8 @@ "lodash": "^4.17.21", "mongoose": "^6.10.4", "nodemailer": "^6.8.0", + "passport": "^0.6.0", + "passport-google-oidc": "^0.1.0", "posthog-node": "^2.6.0", "query-string": "^7.1.3", "request-ip": "^3.3.0", @@ -82,11 +84,13 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", + "@types/express-session": "^1.17.7", "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", + "@types/passport": "^1.0.12", "@types/supertest": "^2.0.12", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.3", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 29f62cdc67..870c7b2bb0 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,6 +12,8 @@ export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!; export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!; export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m'; export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!; +export const getJwtProviderAuthSecret = () => infisical.get('JWT_PROVIDER_AUTH_SECRET')!; +export const getJwtProviderAuthLifetime = () => infisical.get('JWT_PROVIDER_AUTH_LIFETIME')! || '15m'; export const getMongoURL = () => infisical.get('MONGO_URL')!; export const getNodeEnv = () => infisical.get('NODE_ENV')! || 'production'; export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true; @@ -32,6 +34,7 @@ export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!; export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com'; export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; export const getSentryDSN = () => infisical.get('SENTRY_DSN')!; +export const getSessionSecret = () => infisical.get('SESSION_SECRET')!; export const getSiteURL = () => infisical.get('SITE_URL')!; export const getSmtpHost = () => infisical.get('SMTP_HOST')!; export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false; diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 84404bc74f..c186c9cc08 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -267,3 +267,7 @@ export const getNewToken = async (req: Request, res: Response) => { }); } }; + +export const handleGoogleCallback = (req: Request, res: Response) => { + res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 0636bb5a3f..3e2248463e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5,6 +5,7 @@ import infisical from 'infisical-node'; import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; +import session from 'express-session'; import * as Sentry from '@sentry/node'; import { DatabaseService } from './services'; import { setUpHealthEndpoint } from './services/health'; @@ -75,7 +76,7 @@ import { getPort, getSentryDSN, getSiteURL, - getSmtpHost + getSessionSecret, } from './config'; const main = async () => { @@ -111,6 +112,11 @@ const main = async () => { ); app.use(requestIp.mw()); + app.use(session({ + secret: getSessionSecret(), + resave: false, // don't save session if unmodified + saveUninitialized: false, // don't create session until something stored + })); if (getNodeEnv() === 'production') { // enable app-wide rate-limiting + helmet security diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 202a38e831..f42b2dd747 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -21,7 +21,7 @@ export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | E //* Set Sentry user identification if req.user is populated if (req.user !== undefined && req.user !== null) { - Sentry.setUser({ email: req.user.email }) + Sentry.setUser({ email: (req.user as any).email }) } //* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL' //* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 545c5256d5..8cabb60a22 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -2,6 +2,8 @@ import { Schema, model, Types, Document } from 'mongoose'; export interface IUser extends Document { _id: Types.ObjectId; + authId?: string; + authProvider?: string; email: string; firstName?: string; lastName?: string; @@ -26,9 +28,15 @@ export interface IUser extends Document { const userSchema = new Schema( { + authId: { + type: String, + }, + authProvider: { + type: String, + }, email: { type: String, - required: true + required: true, }, firstName: { type: String diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index a3558f3416..4ce4bb6f0e 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -1,10 +1,50 @@ import express from 'express'; const router = express.Router(); import { body } from 'express-validator'; +import passport from 'passport'; import { requireAuth, validateRequest } from '../../middleware'; import { authController } from '../../controllers/v1'; import { authLimiter } from '../../helpers/rateLimiter'; import { AUTH_MODE_JWT } from '../../variables'; +import { User } from '../../models'; +import { createToken } from '../../helpers/auth'; +import { getJwtProviderAuthLifetime, getJwtProviderAuthSecret } from '../../config'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const GoogleStrategy = require('passport-google-oidc'); + +passport.use(new GoogleStrategy({ + passReqToCallback: true, + clientID: process.env['GOOGLE_CLIENT_ID'], + clientSecret: process.env['GOOGLE_CLIENT_SECRET'], + callbackURL: '/api/v1/auth/google/callback', +}, async (req: express.Request, issuer: any, profile: any, cb: any) => { + const email = profile.emails[0].value; + let user = await User.findOne({ + authProvider: issuer, + authId: profile.id, + }) + + if (!user) { + user = await new User({ + email, + authProvider: issuer, + authId: profile.id, + }).save(); + } + + const providerAuthToken = createToken({ + payload: { + userId: user._id.toString(), + email: user.email, + }, + expiresIn: getJwtProviderAuthLifetime(), + secret: getJwtProviderAuthSecret(), + }); + + req.providerAuthToken = providerAuthToken; + cb(null, profile); +})); router.post('/token', validateRequest, authController.getNewToken); @@ -27,20 +67,34 @@ router.post( // deprecated (moved to api/v2/auth/login2) ); router.post( - '/logout', + '/logout', authLimiter, requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT] - }), + }), authController.logout ); router.post( - '/checkAuth', + '/checkAuth', requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT] - }), + }), authController.checkAuth ); +router.get( + '/login/federated/google', + authLimiter, + passport.authenticate('google', { + scope: ['profile', 'email'], + }), +) + +router.get( + '/google/callback', + passport.authenticate('google', { failureRedirect: '/error', session: false }), + authController.handleGoogleCallback, +) + export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 6d2acd606a..7cd5700205 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -9,6 +9,12 @@ import { AuthData } from '../../interfaces/middleware'; +declare module 'express' { + interface Request { + user?: any; + } +} + // TODO: fix (any) types declare global { namespace Express { @@ -18,6 +24,7 @@ declare global { workspace: any; membership: any; targetMembership: any; + providerAuthToken: any; organization: any; membershipOrg: any; integration: any; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 75db3beb5e..f50df97dbb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -51,6 +51,7 @@ "infisical-node": "^1.0.37", "jspdf": "^2.5.1", "jsrp": "^0.2.4", + "jwt-decode": "^3.1.2", "markdown-it": "^13.0.1", "next": "^12.3.4", "next-i18next": "^13.0.2", @@ -15007,6 +15008,11 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -33554,6 +33560,11 @@ "object.assign": "^4.1.3" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index f80df68192..2f96c19420 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,6 +58,7 @@ "infisical-node": "^1.0.37", "jspdf": "^2.5.1", "jsrp": "^0.2.4", + "jwt-decode": "^3.1.2", "markdown-it": "^13.0.1", "next": "^12.3.4", "next-i18next": "^13.0.2", diff --git a/frontend/src/components/utilities/SecurityClient.ts b/frontend/src/components/utilities/SecurityClient.ts index 1b513a3ffa..6e686be3b8 100644 --- a/frontend/src/components/utilities/SecurityClient.ts +++ b/frontend/src/components/utilities/SecurityClient.ts @@ -4,8 +4,20 @@ import { setMfaTempToken, setSignupTempToken} from '@app/reactQuery'; + +export const PROVIDER_AUTH_TOKEN_KEY = 'infisical__provider-auth-token'; + // depreciated: go for apiRequest module in config/api export default class SecurityClient { + + static setProviderAuthToken(tokenStr: string) { + localStorage.setItem(PROVIDER_AUTH_TOKEN_KEY, tokenStr) + } + + static getProviderAuthToken() { + return localStorage.getItem(PROVIDER_AUTH_TOKEN_KEY); + } + static setSignupToken(tokenStr: string) { setSignupTempToken(tokenStr); } diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index ee44dc1473..f0088a5676 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -1,5 +1,6 @@ import axios from 'axios'; +import SecurityClient from '@app/components/utilities/SecurityClient'; import { getAuthToken, getMfaTempToken, @@ -16,6 +17,7 @@ apiRequest.interceptors.request.use((config) => { const signupTempToken = getSignupTempToken(); const mfaTempToken = getMfaTempToken(); const token = getAuthToken(); + const providerAuthToken = SecurityClient.getProviderAuthToken(); if (signupTempToken && config.headers) { // eslint-disable-next-line no-param-reassign @@ -26,6 +28,9 @@ apiRequest.interceptors.request.use((config) => { } else if (token && config.headers) { // eslint-disable-next-line no-param-reassign config.headers.Authorization = `Bearer ${token}`; + } else if(providerAuthToken && config.headers) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${providerAuthToken}`; } return config; }); diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 291435f361..8d0044b2a0 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -16,7 +16,8 @@ export const publicPaths = [ `/terms`, `/subprocessors`, `/verify-email`, - `/password-reset` + `/password-reset`, + `/login/provider/success` ]; export const languageMap = { diff --git a/frontend/src/hooks/useProviderAuth.ts b/frontend/src/hooks/useProviderAuth.ts new file mode 100644 index 0000000000..0cb3fb0f79 --- /dev/null +++ b/frontend/src/hooks/useProviderAuth.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import jwt_decode from 'jwt-decode'; + +import SecurityClient, { PROVIDER_AUTH_TOKEN_KEY } from '@app/components/utilities/SecurityClient'; + +export const useProviderAuth = () => { + const [email, setEmail] = useState(''); + const [userId, setUserId] = useState(''); + const [providerAuthToken, setProviderAuthToken] = useState( + SecurityClient.getProviderAuthToken() || '' + ); + + useEffect(() => { + const handleStorageChange = (event: StorageEvent) => { + if (event.storageArea === localStorage && event.key === PROVIDER_AUTH_TOKEN_KEY) { + if (event.newValue) { + const token = event.newValue; + const { userId: resultUserId, email: resultEmail } = jwt_decode(token) as any; + + setProviderAuthToken(token); + setEmail(resultEmail); + setUserId(resultUserId); + } else { + setProviderAuthToken(''); + setEmail(''); + setUserId(''); + } + setProviderAuthToken(event.newValue || ''); + } + }; + + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }); + + return { + email, + providerAuthToken, + userId, + setProviderAuthToken, + setEmail, + setUserId, + }; +}; diff --git a/frontend/src/pages/login/provider/success.tsx b/frontend/src/pages/login/provider/success.tsx new file mode 100644 index 0000000000..11f6364480 --- /dev/null +++ b/frontend/src/pages/login/provider/success.tsx @@ -0,0 +1,16 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router" + +import SecurityClient from '@app/components/utilities/SecurityClient'; + +export default function LoginProviderSuccess() { + const router = useRouter(); + + useEffect(() => { + const { token } = router.query; + SecurityClient.setProviderAuthToken(token as string); + window.close(); + }, []) + + return
+} diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index 1f1099c7b8..86affcc775 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -30,9 +30,7 @@ export default function SignUp() { const [codeError, setCodeError] = useState(false); const [step, setStep] = useState(1); const router = useRouter(); - const {data: serverDetails } = useFetchServerStatus() - - + const { data: serverDetails } = useFetchServerStatus(); const { t } = useTranslation(); useEffect(() => { diff --git a/nginx/default.dev.conf b/nginx/default.dev.conf index 94c5d7b67c..aeb6db2dad 100644 --- a/nginx/default.dev.conf +++ b/nginx/default.dev.conf @@ -4,16 +4,17 @@ server { location /api { proxy_set_header X-Real-RIP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; proxy_pass http://backend:4000; - proxy_redirect off; + proxy_redirect off; - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + proxy_cookie_path / "/; secure; HttpOnly; SameSite=lax"; } - + location / { include /etc/nginx/mime.types;