From 007e8c4442be584789560f476fcdd1cb4beb1ec0 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 25 Apr 2023 23:46:21 +0800 Subject: [PATCH 01/60] 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 df316753e..69271e3d0 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 0423f0cb0..3d9a148a6 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 084f6b08e..1f4aa7f49 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 29f62cdc6..870c7b2bb 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 84404bc74..c186c9cc0 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 0636bb5a3..3e2248463 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 202a38e83..f42b2dd74 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 545c5256d..8cabb60a2 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 a3558f341..4ce4bb6f0 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 6d2acd606..7cd570020 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 75db3beb5..f50df97db 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 f80df6819..2f96c1942 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 1b513a3ff..6e686be3b 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 ee44dc147..f0088a567 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 291435f36..8d0044b2a 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 000000000..0cb3fb0f7 --- /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 000000000..11f636448 --- /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 1f1099c7b..86affcc77 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 94c5d7b67..aeb6db2da 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; From f956170820efc443d22a2a1a01756da9bf95e191 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 27 Apr 2023 01:38:06 +0800 Subject: [PATCH 02/60] added auth v3 endpoints for login1 and login2 --- backend/src/controllers/v3/authController.ts | 274 +++++++++++++++++++ backend/src/controllers/v3/index.ts | 6 +- backend/src/index.ts | 5 +- backend/src/models/loginSRPDetail.ts | 6 +- backend/src/routes/v3/auth.ts | 29 ++ backend/src/routes/v3/index.ts | 4 +- 6 files changed, 318 insertions(+), 6 deletions(-) create mode 100644 backend/src/controllers/v3/authController.ts create mode 100644 backend/src/routes/v3/auth.ts diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts new file mode 100644 index 000000000..3e4b3e48a --- /dev/null +++ b/backend/src/controllers/v3/authController.ts @@ -0,0 +1,274 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import * as Sentry from '@sentry/node'; +import * as bigintConversion from 'bigint-conversion'; +const jsrp = require('jsrp'); +import { User, LoginSRPDetail } from '../../models'; +import { issueAuthTokens, createToken } from '../../helpers/auth'; +import { checkUserDevice } from '../../helpers/user'; +import { sendMail } from '../../helpers/nodemailer'; +import { TokenService } from '../../services'; +import { EELogService } from '../../ee/services'; +import { BadRequestError, InternalServerError } from '../../utils/errors'; +import { + TOKEN_EMAIL_MFA, + ACTION_LOGIN +} from '../../variables'; +import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this +import { + getJwtMfaLifetime, + getJwtMfaSecret, + getHttpsEnabled, + getJwtProviderAuthSecret +} from '../../config'; + +declare module 'jsonwebtoken' { + export interface ProviderAuthJwtPayload extends jwt.JwtPayload { + userId: string; + } +} + +/** + * Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol + * @param req + * @param res + * @returns + */ +export const login1 = async (req: Request, res: Response) => { + try { + const { + email, + providerAuthToken, + clientPublicKey + }: { + email?: string; + clientPublicKey: string, + providerAuthToken?: string; + } = req.body; + + let userId = ''; + if (providerAuthToken) { + const decodedToken = ( + jwt.verify(providerAuthToken, getJwtProviderAuthSecret()) + ); + userId = decodedToken.userId; + } + + const filter = userId ? { + _id: userId, + } : { + email, + } + + const user = await User.findOne(filter).select('+salt +verifier'); + + if (!user) throw new Error('Failed to find user'); + + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + async () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); + const identifier = userId ? { + userId, + } : { + email, + } + + await LoginSRPDetail.findOneAndReplace(filter, { + ...identifier, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt), + }, { upsert: true, returnNewDocument: false }); + + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to start authentication process' + }); + } +}; + +/** + * Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted) + * private key + * @param req + * @param res + * @returns + */ +export const login2 = async (req: Request, res: Response) => { + try { + + if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); + + const { email, clientProof, providerAuthToken } = req.body; + + let userId = ''; + if (providerAuthToken) { + const decodedToken = ( + jwt.verify(providerAuthToken, getJwtProviderAuthSecret()) + ); + userId = decodedToken.userId; + } + + const filter = userId ? { + _id: userId, + } : { + email, + } + + const user = await User.findOne(filter).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); + + if (!user) throw new Error('Failed to find user'); + + const identifier = userId ? { + userId, + } : { + email, + } + + const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ ...identifier }); + + if (!loginSRPDetail) { + return BadRequestError(Error("Failed to find login details for SRP")) + } + + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: loginSRPDetail.serverBInt + }, + async () => { + server.setClientPublicKey(loginSRPDetail.clientPublicKey); + + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + + if (user.isMfaEnabled) { + // case: user has MFA enabled + + // generate temporary MFA token + const token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: getJwtMfaLifetime(), + secret: getJwtMfaSecret() + }); + + const code = await TokenService.createToken({ + type: TOKEN_EMAIL_MFA, + email + }); + + // send MFA code [code] to [email] + await sendMail({ + template: 'emailMfa.handlebars', + subjectLine: 'Infisical MFA code', + recipients: [user.email], + substitutions: { + code + } + }); + + return res.status(200).send({ + mfaEnabled: true, + token + }); + } + + await checkUserDevice({ + user, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' + }); + + // issue tokens + const tokens = await issueAuthTokens({ userId: user._id.toString() }); + + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: getHttpsEnabled() + }); + + // case: user does not have MFA enablgged + // return (access) token in response + + interface ResponseData { + mfaEnabled: boolean; + encryptionVersion: any; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey?: string; + encryptedPrivateKey?: string; + iv?: string; + tag?: string; + } + + const response: ResponseData = { + mfaEnabled: false, + encryptionVersion: user.encryptionVersion, + token: tokens.token, + publicKey: user.publicKey, + encryptedPrivateKey: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag + } + + if ( + user?.protectedKey && + user?.protectedKeyIV && + user?.protectedKeyTag + ) { + response.protectedKey = user.protectedKey; + response.protectedKeyIV = user.protectedKeyIV + response.protectedKeyTag = user.protectedKeyTag; + } + + const loginAction = await EELogService.createAction({ + name: ACTION_LOGIN, + userId: user._id + }); + + loginAction && await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getChannelFromUserAgent(req.headers['user-agent']), + ipAddress: req.ip + }); + + return res.status(200).send(response); + } + + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } +}; diff --git a/backend/src/controllers/v3/index.ts b/backend/src/controllers/v3/index.ts index 1cbb36595..ffe25a6c2 100644 --- a/backend/src/controllers/v3/index.ts +++ b/backend/src/controllers/v3/index.ts @@ -1,7 +1,9 @@ import * as secretsController from './secretsController'; import * as workspacesController from './workspacesController'; +import * as authController from './authController'; export { + authController, secretsController, - workspacesController -} \ No newline at end of file + workspacesController, +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 3e2248463..3b3091f92 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,4 +1,3 @@ -import mongoose from 'mongoose'; import dotenv from 'dotenv'; dotenv.config(); import infisical from 'infisical-node'; @@ -63,6 +62,7 @@ import { tags as v2TagsRouter, } from './routes/v2'; import { + auth as v3AuthRouter, secrets as v3SecretsRouter, workspaces as v3WorkspacesRouter } from './routes/v3'; @@ -164,8 +164,9 @@ const main = async () => { app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new app.use('/api/v2/api-key', v2APIKeyDataRouter); - + // v3 routes (experimental) + app.use('/api/v3/auth', v3AuthRouter); app.use('/api/v3/secrets', v3SecretsRouter); app.use('/api/v3/workspaces', v3WorkspacesRouter); diff --git a/backend/src/models/loginSRPDetail.ts b/backend/src/models/loginSRPDetail.ts index 6f3b579bd..0cd3ed1d8 100644 --- a/backend/src/models/loginSRPDetail.ts +++ b/backend/src/models/loginSRPDetail.ts @@ -5,6 +5,7 @@ export interface ILoginSRPDetail { clientPublicKey: string; email: string; serverBInt: mongoose.Schema.Types.Buffer; + userId: string; expireAt: Date; } @@ -16,10 +17,13 @@ const loginSRPDetailSchema = new Schema( }, email: { type: String, - required: true, unique: true }, serverBInt: { type: mongoose.Schema.Types.Buffer }, + userId: { + type: String, + unique: true, + }, expireAt: { type: Date } } ); diff --git a/backend/src/routes/v3/auth.ts b/backend/src/routes/v3/auth.ts new file mode 100644 index 000000000..dbe7481a4 --- /dev/null +++ b/backend/src/routes/v3/auth.ts @@ -0,0 +1,29 @@ +import express from 'express'; +import { body } from 'express-validator'; +import { validateRequest } from '../../middleware'; +import { authController } from '../../controllers/v3'; +import { authLimiter } from '../../helpers/rateLimiter'; + +const router = express.Router(); + +router.post( + '/login1', + authLimiter, + body('email').isString().trim(), + body('providerAuthToken').isString().trim(), + body('clientPublicKey').isString().trim().notEmpty(), + validateRequest, + authController.login1 +); + +router.post( + '/login2', + authLimiter, + body('email').isString().trim(), + body('providerAuthToken').isString().trim(), + body('clientProof').isString().trim().notEmpty(), + validateRequest, + authController.login2 +); + +export default router; diff --git a/backend/src/routes/v3/index.ts b/backend/src/routes/v3/index.ts index e66723133..55f6ec120 100644 --- a/backend/src/routes/v3/index.ts +++ b/backend/src/routes/v3/index.ts @@ -1,7 +1,9 @@ +import auth from './auth'; import secrets from './secrets'; import workspaces from './workspaces'; export { + auth, secrets, workspaces -} \ No newline at end of file +} From 2dd15702005fdd6d03a2b8b3986737e80b22fc8b Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 27 Apr 2023 02:00:16 +0800 Subject: [PATCH 03/60] updated use of environment variables to utilize await --- backend/src/controllers/v3/authController.ts | 11 ++++++----- backend/src/index.ts | 2 +- backend/src/routes/v1/auth.ts | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 3e4b3e48a..ccbded266 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -50,8 +50,9 @@ export const login1 = async (req: Request, res: Response) => { let userId = ''; if (providerAuthToken) { const decodedToken = ( - jwt.verify(providerAuthToken, getJwtProviderAuthSecret()) + jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) ); + userId = decodedToken.userId; } @@ -118,7 +119,7 @@ export const login2 = async (req: Request, res: Response) => { let userId = ''; if (providerAuthToken) { const decodedToken = ( - jwt.verify(providerAuthToken, getJwtProviderAuthSecret()) + jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) ); userId = decodedToken.userId; } @@ -166,8 +167,8 @@ export const login2 = async (req: Request, res: Response) => { payload: { userId: user._id.toString() }, - expiresIn: getJwtMfaLifetime(), - secret: getJwtMfaSecret() + expiresIn: await getJwtMfaLifetime(), + secret: await getJwtMfaSecret() }); const code = await TokenService.createToken({ @@ -205,7 +206,7 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getHttpsEnabled() + secure: await getHttpsEnabled() }); // case: user does not have MFA enablgged diff --git a/backend/src/index.ts b/backend/src/index.ts index a250fc6bc..e63ec0e96 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -107,7 +107,7 @@ const main = async () => { app.use(requestIp.mw()); app.use(session({ - secret: getSessionSecret(), + secret: await getSessionSecret(), resave: false, // don't save session if unmodified saveUninitialized: false, // don't create session until something stored })); diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index 4ce4bb6f0..0999e9a2d 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -38,8 +38,8 @@ passport.use(new GoogleStrategy({ userId: user._id.toString(), email: user.email, }, - expiresIn: getJwtProviderAuthLifetime(), - secret: getJwtProviderAuthSecret(), + expiresIn: await getJwtProviderAuthLifetime(), + secret: await getJwtProviderAuthSecret(), }); req.providerAuthToken = providerAuthToken; From dfb84e99328f5f3287f89e4b542d0421dd1cee6c Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 27 Apr 2023 23:10:27 +0800 Subject: [PATCH 04/60] developed initial version of new login page --- backend/src/routes/v1/auth.ts | 2 +- frontend/src/components/login/LoginStep.tsx | 5 +- .../components/login/PasswordInputStep.tsx | 138 ++++++++++++++++++ .../src/components/utilities/attemptLogin.ts | 30 +++- frontend/src/pages/api/auth/Login1.ts | 11 +- frontend/src/pages/api/auth/Login2.ts | 11 +- frontend/src/pages/login.tsx | 86 +++++++---- 7 files changed, 239 insertions(+), 44 deletions(-) create mode 100644 frontend/src/components/login/PasswordInputStep.tsx diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index 0999e9a2d..2ff0e6d27 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -84,7 +84,7 @@ router.post( ); router.get( - '/login/federated/google', + '/login/google', authLimiter, passport.authenticate('google', { scope: ['profile', 'email'], diff --git a/frontend/src/components/login/LoginStep.tsx b/frontend/src/components/login/LoginStep.tsx index b19bafdd9..a45ebe3e5 100644 --- a/frontend/src/components/login/LoginStep.tsx +++ b/frontend/src/components/login/LoginStep.tsx @@ -47,7 +47,10 @@ export default function LoginStep ({ } setIsLoading(true); - const isLoginSuccessful = await attemptLogin(email, password); + const isLoginSuccessful = await attemptLogin({ + email, + password, + }); if (isLoginSuccessful && isLoginSuccessful.success) { // case: login was successful diff --git a/frontend/src/components/login/PasswordInputStep.tsx b/frontend/src/components/login/PasswordInputStep.tsx new file mode 100644 index 000000000..b3c881a03 --- /dev/null +++ b/frontend/src/components/login/PasswordInputStep.tsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useTranslation } from 'next-i18next'; + +import Button from '@app/components/basic/buttons/Button'; +import Error from '@app/components/basic/Error'; +import InputField from '@app/components/basic/InputField'; +import attemptLogin from '@app/components/utilities/attemptLogin'; +import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; + +import SecurityClient from '../utilities/SecurityClient'; + +export default function PasswordInputStep({ + userId, + email, + password, + setPassword, + setProviderAuthToken, + setStep +}: { + email: string; + userId: string; + password: string; + setPassword: (password: string) => void; + setProviderAuthToken: (value: string) => void; + setStep: (step: number) => void; +}) { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [loginError, setLoginError] = useState(false); + + const { t } = useTranslation(); + + const handleLogin = async () => { + try { + if (!userId || !password) { + return; + } + + setIsLoading(true); + const isLoginSuccessful = await attemptLogin({ + userId, + email, + password + }); + + if (isLoginSuccessful && isLoginSuccessful.success) { + // case: login was successful + + if (isLoginSuccessful.mfaEnabled) { + // case: login requires MFA step + setStep(2); + setIsLoading(false); + return; + } + + // case: login does not require MFA step + router.push(`/dashboard/${localStorage.getItem('projectData.id')}`); + } + } catch (err) { + setLoginError(true); + } + + setIsLoading(false); + }; + + return ( +
e.preventDefault()}> +
+

+ {t('login:login')} +

+
+ +
+ + + +
+
+ {!isLoading && loginError && } +
+
+
+
+
+
+

{t('login:need-account')}

+ + + +
+ +
+ +
+
+ ); +} + +export const getStaticProps = getTranslatedStaticProps(['auth', 'login']); diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index e92a65e77..08bef1441 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -26,20 +26,33 @@ interface IsLoginSuccessful { * @param {string} password - password of user to log in */ const attemptLogin = async ( - email: string, - password: string + { + email, + password, + userId, + }: { + email: string; + userId?: string; + password: string; + } ): Promise => { + + const username = userId ?? email; const telemetry = new Telemetry().getInstance(); return new Promise((resolve, reject) => { client.init( { - username: email, + username, password }, async () => { try { const clientPublicKey = client.getPublicKey(); - const { serverPublicKey, salt } = await login1(email, clientPublicKey); + const { serverPublicKey, salt } = await login1({ + email, + clientPublicKey, + userId, + }); client.setSalt(salt); client.setServerPublicKey(serverPublicKey); @@ -57,8 +70,11 @@ const attemptLogin = async ( iv, tag } = await login2( - email, - clientProof + { + email, + userId, + clientProof, + } ); if (mfaEnabled) { @@ -137,4 +153,4 @@ const attemptLogin = async ( }); }; -export default attemptLogin; \ No newline at end of file +export default attemptLogin; diff --git a/frontend/src/pages/api/auth/Login1.ts b/frontend/src/pages/api/auth/Login1.ts index bb7afc82b..2685a770c 100644 --- a/frontend/src/pages/api/auth/Login1.ts +++ b/frontend/src/pages/api/auth/Login1.ts @@ -9,16 +9,17 @@ interface Login1 { * @param {*} clientPublicKey * @returns */ -const login1 = async (email: string, clientPublicKey: string) => { +const login1 = async (loginDetails: { + email: string; + clientPublicKey: string; + userId?: string; +}) => { const response = await fetch("/api/v2/auth/login1", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ - email, - clientPublicKey, - }), + body: JSON.stringify(loginDetails), }); // need precise error handling about the status code if (response?.status === 200) { diff --git a/frontend/src/pages/api/auth/Login2.ts b/frontend/src/pages/api/auth/Login2.ts index e3d625950..724a8b1d4 100644 --- a/frontend/src/pages/api/auth/Login2.ts +++ b/frontend/src/pages/api/auth/Login2.ts @@ -17,16 +17,17 @@ interface Login2Response { * @param {*} clientPublicKey * @returns */ -const login2 = async (email: string, clientProof: string) => { +const login2 = async (loginDetails: { + email: string; + clientProof: string; + userId?: string; +}) => { const response = await fetch('/api/v2/auth/login2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email, - clientProof - }), + body: JSON.stringify(loginDetails), credentials: 'include' }); // need precise error handling about the status code diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 64f1caca5..1794b834b 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -8,7 +8,9 @@ import { useTranslation } from 'next-i18next'; import ListBox from '@app/components/basic/Listbox'; import LoginStep from '@app/components/login/LoginStep'; import MFAStep from '@app/components/login/MFAStep'; +import PasswordInputStep from '@app/components/login/PasswordInputStep'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; +import { useProviderAuth } from '@app/hooks/useProviderAuth'; import { isLoggedIn } from '@app/reactQuery'; import getWorkspaces from './api/workspace/getWorkspaces'; @@ -20,8 +22,14 @@ export default function Login() { const [step, setStep] = useState(1); const { t } = useTranslation(); const lang = router.locale ?? 'en'; + const [isLoginWithEmail, setIsLoginWithEmail] = useState(false); + const { + providerAuthToken, + userId, + email: providerEmail, + setProviderAuthToken + } = useProviderAuth(); - const setLanguage = async (to: string) => { router.push('/login', '/login', { locale: to }); localStorage.setItem('lang', to); @@ -44,30 +52,58 @@ export default function Login() { } }, []); - const renderStep = (loginStep: number) => { - // TODO: add MFA step - switch (loginStep) { - case 1: - return ( - - ); - case 2: - // TODO: add MFA step - return ( - - ); - default: - return
+ const renderView = (loginStep: number) => { + + if (providerAuthToken && step === 1) { + return ( + + ) } + + if (isLoginWithEmail && loginStep === 1) { + return ( + + ) + } + + if (!isLoginWithEmail && loginStep === 1) { + return ( + <> + + + + ) + } + + if (step === 2) { + + } + + return
} return ( @@ -84,7 +120,7 @@ export default function Login() { long logo
- {renderStep(step)} + {renderView(step)}
Date: Wed, 3 May 2023 18:58:32 +0800 Subject: [PATCH 05/60] implemented comments --- .env.example | 4 +- backend/src/config/index.ts | 2 + backend/src/controllers/v1/authController.ts | 4 - backend/src/controllers/v1/index.ts | 2 + backend/src/controllers/v1/oauthController.ts | 5 + backend/src/controllers/v3/authController.ts | 75 +++++------- backend/src/helpers/auth.ts | 27 +++++ backend/src/index.ts | 6 + backend/src/models/index.ts | 3 +- backend/src/models/user.ts | 7 +- backend/src/routes/v1/auth.ts | 53 --------- backend/src/routes/v1/index.ts | 4 +- backend/src/routes/v1/oauth.ts | 21 ++++ backend/src/utils/auth.ts | 58 ++++++++- frontend/src/pages/login.tsx | 16 +-- frontend/src/pages/signup.tsx | 111 +++++++++++++----- 16 files changed, 247 insertions(+), 151 deletions(-) create mode 100644 backend/src/controllers/v1/oauthController.ts create mode 100644 backend/src/routes/v1/oauth.ts diff --git a/.env.example b/.env.example index 69271e3d0..c23a3354a 100644 --- a/.env.example +++ b/.env.example @@ -70,6 +70,6 @@ STRIPE_PRODUCT_TEAM= STRIPE_PRODUCT_PRO= NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= +CLIENT_ID_GOOGLE= +CLIENT_SECRET_GOOGLE= SESSION_SECRET= diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index efbc90df8..a3f74caaa 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -29,12 +29,14 @@ export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_ export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue; export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue; export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue; +export const getClientIdGoogle = async () => (await client.getSecret('CLIENT_ID_GOOGLE')).secretValue; export const getClientSecretAzure = async () => (await client.getSecret('CLIENT_SECRET_AZURE')).secretValue; export const getClientSecretHeroku = async () => (await client.getSecret('CLIENT_SECRET_HEROKU')).secretValue; export const getClientSecretVercel = async () => (await client.getSecret('CLIENT_SECRET_VERCEL')).secretValue; export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue; export const getClientSecretGitHub = async () => (await client.getSecret('CLIENT_SECRET_GITHUB')).secretValue; export const getClientSecretGitLab = async () => (await client.getSecret('CLIENT_SECRET_GITLAB')).secretValue; +export const getClientSecretGoogle = async () => (await client.getSecret('CLIENT_SECRET_GOOGLE')).secretValue; export const getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue; export const getPostHogHost = async () => (await client.getSecret('POSTHOG_HOST')).secretValue || 'https://app.posthog.com'; export const getPostHogProjectApiKey = async () => (await client.getSecret('POSTHOG_PROJECT_API_KEY')).secretValue || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 15341341f..e60002e9d 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -267,7 +267,3 @@ 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/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index 1da61835f..8a8b1d54d 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -5,6 +5,7 @@ import * as integrationController from './integrationController'; import * as keyController from './keyController'; import * as membershipController from './membershipController'; import * as membershipOrgController from './membershipOrgController'; +import * as oauthController from './oauthController'; import * as organizationController from './organizationController'; import * as passwordController from './passwordController'; import * as secretController from './secretController'; @@ -23,6 +24,7 @@ export { keyController, membershipController, membershipOrgController, + oauthController, organizationController, passwordController, secretController, diff --git a/backend/src/controllers/v1/oauthController.ts b/backend/src/controllers/v1/oauthController.ts new file mode 100644 index 000000000..5f1511e39 --- /dev/null +++ b/backend/src/controllers/v1/oauthController.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express'; + +export const handleAuthProviderCallback = (req: Request, res: Response) => { + res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); +} diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index ccbded266..7066dc866 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node'; import * as bigintConversion from 'bigint-conversion'; const jsrp = require('jsrp'); import { User, LoginSRPDetail } from '../../models'; -import { issueAuthTokens, createToken } from '../../helpers/auth'; +import { issueAuthTokens, createToken, validateProviderAuthToken } from '../../helpers/auth'; import { checkUserDevice } from '../../helpers/user'; import { sendMail } from '../../helpers/nodemailer'; import { TokenService } from '../../services'; @@ -20,12 +20,14 @@ import { getJwtMfaLifetime, getJwtMfaSecret, getHttpsEnabled, - getJwtProviderAuthSecret } from '../../config'; +import { AuthProvider } from '../../models/user'; declare module 'jsonwebtoken' { export interface ProviderAuthJwtPayload extends jwt.JwtPayload { userId: string; + email: string; + authProvider: AuthProvider; } } @@ -42,30 +44,25 @@ export const login1 = async (req: Request, res: Response) => { providerAuthToken, clientPublicKey }: { - email?: string; + email: string; clientPublicKey: string, providerAuthToken?: string; } = req.body; - let userId = ''; - if (providerAuthToken) { - const decodedToken = ( - jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) - ); - - userId = decodedToken.userId; - } - - const filter = userId ? { - _id: userId, - } : { + const user = await User.findOne({ email, - } - - const user = await User.findOne(filter).select('+salt +verifier'); + }).select('+salt +verifier'); if (!user) throw new Error('Failed to find user'); + if (user.authProvider) { + await validateProviderAuthToken({ + email, + user, + providerAuthToken, + }) + } + const server = new jsrp.server(); server.init( { @@ -75,14 +72,10 @@ export const login1 = async (req: Request, res: Response) => { async () => { // generate server-side public key const serverPublicKey = server.getPublicKey(); - const identifier = userId ? { - userId, - } : { - email, - } - - await LoginSRPDetail.findOneAndReplace(filter, { - ...identifier, + await LoginSRPDetail.findOneAndReplace({ + userId: user.id, + }, { + userId: user.id, clientPublicKey: clientPublicKey, serverBInt: bigintConversion.bigintToBuf(server.bInt), }, { upsert: true, returnNewDocument: false }); @@ -115,32 +108,22 @@ export const login2 = async (req: Request, res: Response) => { if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); const { email, clientProof, providerAuthToken } = req.body; - - let userId = ''; - if (providerAuthToken) { - const decodedToken = ( - jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) - ); - userId = decodedToken.userId; - } - const filter = userId ? { - _id: userId, - } : { + const user = await User.findOne({ email, - } - - const user = await User.findOne(filter).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); + }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); if (!user) throw new Error('Failed to find user'); - - const identifier = userId ? { - userId, - } : { - email, + + if (user.authProvider) { + await validateProviderAuthToken({ + email, + user, + providerAuthToken, + }) } - const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ ...identifier }); + const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ userId: user.id }); if (!loginSRPDetail) { return BadRequestError(Error("Failed to find login details for SRP")) diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index fcc3ba8ff..134349e0c 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -20,6 +20,7 @@ import { import { getJwtAuthLifetime, getJwtAuthSecret, + getJwtProviderAuthSecret, getJwtRefreshLifetime, getJwtRefreshSecret } from '../config'; @@ -319,8 +320,34 @@ const createToken = ({ }); }; +const validateProviderAuthToken = async ({ + email, + user, + providerAuthToken, +}: { + email: string; + user: IUser, + providerAuthToken?: string; +}) => { + if (!providerAuthToken) { + throw new Error('Invalid authentication request.'); + } + + const decodedToken = ( + jwt.verify(providerAuthToken, await getJwtProviderAuthSecret()) + ); + + if ( + decodedToken.authProvider !== user.authProvider || + decodedToken.email !== email + ) { + throw new Error('Invalid authentication credentials.') + } +} + export { validateAuthMode, + validateProviderAuthToken, getAuthUserPayload, getAuthSTDPayload, getAuthSAAKPayload, diff --git a/backend/src/index.ts b/backend/src/index.ts index e63ec0e96..004a4378f 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -37,6 +37,7 @@ import { membership as v1MembershipRouter, key as v1KeyRouter, inviteOrg as v1InviteOrgRouter, + oauth as v1OAuth, user as v1UserRouter, userAction as v1UserActionRouter, secret as v1SecretRouter, @@ -78,6 +79,7 @@ import { getSiteURL, getSessionSecret, } from './config'; +import { initializePassport } from './utils/auth'; const main = async () => { TelemetryService.logTelemetryMessage(); @@ -95,6 +97,9 @@ const main = async () => { patchRouterParam(); const app = express(); + + await initializePassport(); + app.enable('trust proxy'); app.use(express.json()); app.use(cookieParser()); @@ -129,6 +134,7 @@ const main = async () => { // v1 routes (default) app.use('/api/v1/signup', v1SignupRouter); app.use('/api/v1/auth', v1AuthRouter); + app.use('/api/v1/oauth', v1OAuth); app.use('/api/v1/bot', v1BotRouter); app.use('/api/v1/user', v1UserRouter); app.use('/api/v1/user-action', v1UserActionRouter); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 6cd6a01da..843296b55 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -16,7 +16,7 @@ import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; // import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from './serviceAccountOrganizationPermission'; // new import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from './serviceAccountWorkspacePermission'; // new import TokenData, { ITokenData } from './tokenData'; -import User, { IUser } from './user'; +import User,{ AuthProvider, IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; import ServiceTokenData, { IServiceTokenData } from './serviceTokenData'; @@ -24,6 +24,7 @@ import APIKeyData, { IAPIKeyData } from './apiKeyData'; import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail'; export { + AuthProvider, BackupPrivateKey, IBackupPrivateKey, Bot, diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 8cabb60a2..bb5b38d53 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -1,9 +1,13 @@ import { Schema, model, Types, Document } from 'mongoose'; +export enum AuthProvider { + GOOGLE = 'google', +} + export interface IUser extends Document { _id: Types.ObjectId; authId?: string; - authProvider?: string; + authProvider?: AuthProvider; email: string; firstName?: string; lastName?: string; @@ -33,6 +37,7 @@ const userSchema = new Schema( }, authProvider: { type: String, + enum: AuthProvider, }, email: { type: String, diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index 2ff0e6d27..2125aaf80 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -1,50 +1,10 @@ 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: await getJwtProviderAuthLifetime(), - secret: await getJwtProviderAuthSecret(), - }); - - req.providerAuthToken = providerAuthToken; - cb(null, profile); -})); router.post('/token', validateRequest, authController.getNewToken); @@ -83,18 +43,5 @@ router.post( authController.checkAuth ); -router.get( - '/login/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/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 4b9dcafca..a38a5bfb2 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -15,7 +15,8 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; -import secretsFolder from './secretsFolder' +import secretsFolder from './secretsFolder'; +import oauth from './oauth'; export { signup, @@ -29,6 +30,7 @@ export { membership, key, inviteOrg, + oauth, secret, serviceToken, password, diff --git a/backend/src/routes/v1/oauth.ts b/backend/src/routes/v1/oauth.ts new file mode 100644 index 000000000..4bd764ca3 --- /dev/null +++ b/backend/src/routes/v1/oauth.ts @@ -0,0 +1,21 @@ +import express from 'express'; +const router = express.Router(); +import passport from 'passport'; +import { oauthController } from '../../controllers/v1'; +import { authLimiter } from '../../helpers/rateLimiter'; + +router.get( + '/redirect/google', + authLimiter, + passport.authenticate('google', { + scope: ['profile', 'email'], + }), + ) + +router.get( + '/callback/google', + passport.authenticate('google', { failureRedirect: '/error', session: false }), + oauthController.handleAuthProviderCallback, +) + +export default router; diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index bfb51e6d2..fac1af5b4 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -1,10 +1,22 @@ +import express from 'express'; +import passport from 'passport'; import { AuthData } from '../interfaces/middleware'; import { + AuthProvider, User, ServiceAccount, ServiceTokenData, - ServiceToken } from '../models'; +import { createToken } from '../helpers/auth'; +import { + getClientIdGoogle, + getClientSecretGoogle, + getJwtProviderAuthLifetime, + getJwtProviderAuthSecret +} from '../config'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const GoogleStrategy = require('passport-google-oidc'); // TODO: find a more optimal folder structure to store these types of functions @@ -48,7 +60,47 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => { } } +const initializePassport = async () => { + const googleClientSecret = await getClientSecretGoogle(); + const googleClientId = await getClientIdGoogle(); + + passport.use(new GoogleStrategy({ + passReqToCallback: true, + clientID: googleClientId, + clientSecret: googleClientSecret, + callbackURL: '/api/v1/oauth/callback/google', + }, async (req: express.Request, issuer: any, profile: any, cb: any) => { + const email = profile.emails[0].value; + let user = await User.findOne({ + authProvider: AuthProvider.GOOGLE, + authId: profile.id, + }) + + if (!user) { + user = await new User({ + email, + authProvider: AuthProvider.GOOGLE, + authId: profile.id, + }).save(); + } + + const providerAuthToken = createToken({ + payload: { + userId: user._id.toString(), + email: user.email, + authProvider: user.authProvider, + }, + expiresIn: await getJwtProviderAuthLifetime(), + secret: await getJwtProviderAuthSecret(), + }); + + req.providerAuthToken = providerAuthToken; + cb(null, profile); + })); +} + export { getAuthDataPayloadIdObj, - getAuthDataPayloadUserObj -} \ No newline at end of file + getAuthDataPayloadUserObj, + initializePassport, +} diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 1794b834b..958328b34 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -24,12 +24,12 @@ export default function Login() { const lang = router.locale ?? 'en'; const [isLoginWithEmail, setIsLoginWithEmail] = useState(false); const { - providerAuthToken, - userId, - email: providerEmail, - setProviderAuthToken + providerAuthToken, + userId, + email: providerEmail, + setProviderAuthToken } = useProviderAuth(); - + const setLanguage = async (to: string) => { router.push('/login', '/login', { locale: to }); localStorage.setItem('lang', to); @@ -51,7 +51,7 @@ export default function Login() { redirectToDashboard(); } }, []); - + const renderView = (loginStep: number) => { if (providerAuthToken && step === 1) { @@ -83,12 +83,12 @@ export default function Login() { return ( <> diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index 86affcc77..5febfa69b 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -14,10 +14,12 @@ import UserInfoStep from '@app/components/signup/UserInfoStep'; import SecurityClient from '@app/components/utilities/SecurityClient'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; import { useFetchServerStatus } from '@app/hooks/api/serverDetails'; +import { useProviderAuth } from '@app/hooks/useProviderAuth'; import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode'; import getWorkspaces from './api/workspace/getWorkspaces'; + /** * @returns the signup page */ @@ -31,7 +33,13 @@ export default function SignUp() { const [step, setStep] = useState(1); const router = useRouter(); const { data: serverDetails } = useFetchServerStatus(); + const [isSignupWithEmail, setIsSignupWithEmail] = useState(false); const { t } = useTranslation(); + const { providerAuthToken } = useProviderAuth(); + + if (providerAuthToken && step < 3) { + setStep(3); + } useEffect(() => { const tryAuth = async () => { @@ -60,7 +68,7 @@ export default function SignUp() { // Checking if the code matches the email. const response = await checkEmailVerificationCode({ email, code }); if (response.status === 200) { - const {token} = await response.json(); + const { token } = await response.json(); SecurityClient.setSignupToken(token); setStep(3); } else { @@ -71,17 +79,83 @@ export default function SignUp() { // when email service is not configured, skip step 2 and 5 useEffect(() => { - if (!serverDetails?.emailConfigured && step === 2){ + if (!serverDetails?.emailConfigured && step === 2) { incrementStep() } - if (!serverDetails?.emailConfigured && step === 5){ - getWorkspaces().then((userWorkspaces)=>{ + if (!serverDetails?.emailConfigured && step === 5) { + getWorkspaces().then((userWorkspaces) => { router.push(`/dashboard/${userWorkspaces[0]._id}`); }); } }, [step]); + const renderView = (registerStep: number) => { + if (isSignupWithEmail && registerStep === 1) { + return + } + + if (!isSignupWithEmail && registerStep === 1) { + return ( + <> + + + + ) + } + + if (registerStep === 2) { + return ( + + ) + } + + if (registerStep === 3) { + return ( + + ) + } + + if (registerStep === 4) { + return ( + + ) + } + + if (serverDetails?.emailConfigured) { + return + } + + return "" + } + return (
@@ -98,34 +172,7 @@ export default function SignUp() {
e.preventDefault()}> - {step === 1 ? ( - - ) : step === 2 ? ( - - ) : step === 3 ? ( - - ) : step === 4 ? ( - - ) : (serverDetails?.emailConfigured ? : "")} + {renderView(step)}
From 1172726e74e98a4eade8ece7518a5b0cdcfce154 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 4 May 2023 01:32:40 +0800 Subject: [PATCH 06/60] added signup v3 endpoints and developed initial new signup flow --- backend/src/controllers/v3/index.ts | 2 + .../src/controllers/v3/signupController.ts | 177 ++++++++++++++++++ backend/src/index.ts | 4 +- backend/src/models/user.ts | 1 + backend/src/routes/v3/index.ts | 4 +- backend/src/routes/v3/signup.ts | 28 +++ .../components/login/PasswordInputStep.tsx | 7 - .../src/components/signup/UserInfoStep.tsx | 17 +- .../src/components/utilities/attemptLogin.ts | 7 +- frontend/src/hooks/useProviderAuth.ts | 8 +- .../auth/CompleteAccountInformationSignup.ts | 9 +- frontend/src/pages/api/auth/Login1.ts | 1 - frontend/src/pages/api/auth/Login2.ts | 1 - frontend/src/pages/login.tsx | 2 - frontend/src/pages/signup.tsx | 9 +- 15 files changed, 243 insertions(+), 34 deletions(-) create mode 100644 backend/src/controllers/v3/signupController.ts create mode 100644 backend/src/routes/v3/signup.ts diff --git a/backend/src/controllers/v3/index.ts b/backend/src/controllers/v3/index.ts index ffe25a6c2..9d3f118d3 100644 --- a/backend/src/controllers/v3/index.ts +++ b/backend/src/controllers/v3/index.ts @@ -1,9 +1,11 @@ import * as secretsController from './secretsController'; import * as workspacesController from './workspacesController'; import * as authController from './authController'; +import * as signupController from './signupController'; export { authController, secretsController, + signupController, workspacesController, } diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts new file mode 100644 index 000000000..5be22b308 --- /dev/null +++ b/backend/src/controllers/v3/signupController.ts @@ -0,0 +1,177 @@ +import jwt from 'jsonwebtoken'; +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { User, MembershipOrg } from '../../models'; +import { completeAccount } from '../../helpers/user'; +import { + initializeDefaultOrg +} from '../../helpers/signup'; +import { issueAuthTokens, validateProviderAuthToken } from '../../helpers/auth'; +import { INVITED, ACCEPTED } from '../../variables'; +import request from '../../config/request'; +import { getLoopsApiKey, getHttpsEnabled, getJwtSignupSecret } from '../../config'; +import { BadRequestError } from '../../utils/errors'; + +/** + * Complete setting up user by adding their personal and auth information as part of the + * signup flow + * @param req + * @param res + * @returns + */ +export const completeAccountSignup = async (req: Request, res: Response) => { + let user, token, refreshToken; + try { + const { + email, + firstName, + lastName, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier, + organizationName, + providerAuthToken, + }: { + email: string; + firstName: string; + lastName: string; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + publicKey: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + organizationName: string; + providerAuthToken?: string; + } = req.body; + + user = await User.findOne({ email }); + + if (!user || (user && user?.publicKey)) { + // case 1: user doesn't exist. + // case 2: user has already completed account + return res.status(403).send({ + error: 'Failed to complete account for complete user' + }); + } + + if (providerAuthToken) { + await validateProviderAuthToken({ + email, + providerAuthToken, + user, + }); + } else { + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) { + throw BadRequestError({message: `Missing Authorization Header in the request header.`}); + } + if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') { + throw BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}) + } + if(AUTH_TOKEN_VALUE === null) { + throw BadRequestError({ + message: 'Missing Authorization Body in the request header', + }) + } + + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) + ); + + if (decodedToken.userId !== user.id) { + throw BadRequestError(); + } + } + + // complete setting up user's account + user = await completeAccount({ + userId: user._id.toString(), + firstName, + lastName, + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + }); + + if (!user) + throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null + + // initialize default organization and workspace + await initializeDefaultOrg({ + organizationName, + user + }); + + // update organization membership statuses that are + // invited to completed with user attached + await MembershipOrg.updateMany( + { + inviteEmail: email, + status: INVITED + }, + { + user, + status: ACCEPTED + } + ); + + // issue tokens + const tokens = await issueAuthTokens({ + userId: user._id.toString() + }); + + token = tokens.token; + + // sending a welcome email to new users + if (await getLoopsApiKey()) { + await request.post("https://app.loops.so/api/v1/events/send", { + "email": email, + "eventName": "Sign Up", + "firstName": firstName, + "lastName": lastName + }, { + headers: { + "Accept": "application/json", + "Authorization": "Bearer " + (await getLoopsApiKey()) + }, + }); + } + + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: await getHttpsEnabled() + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to complete account setup' + }); + } + + return res.status(200).send({ + message: 'Successfully set up account', + user, + token + }); +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index 004a4378f..de89c6f7c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -65,7 +65,8 @@ import { import { auth as v3AuthRouter, secrets as v3SecretsRouter, - workspaces as v3WorkspacesRouter + signup as v3SignupRouter, + workspaces as v3WorkspacesRouter, } from './routes/v3'; import { healthCheck } from './routes/status'; import { getLogger } from './utils/logger'; @@ -170,6 +171,7 @@ const main = async () => { app.use('/api/v3/auth', v3AuthRouter); app.use('/api/v3/secrets', v3SecretsRouter); app.use('/api/v3/workspaces', v3WorkspacesRouter); + app.use('/api/v3/signup', v3SignupRouter); // api docs app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index bb5b38d53..843730e0c 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -42,6 +42,7 @@ const userSchema = new Schema( email: { type: String, required: true, + unique: true, }, firstName: { type: String diff --git a/backend/src/routes/v3/index.ts b/backend/src/routes/v3/index.ts index 55f6ec120..2560a8f82 100644 --- a/backend/src/routes/v3/index.ts +++ b/backend/src/routes/v3/index.ts @@ -1,9 +1,11 @@ import auth from './auth'; import secrets from './secrets'; import workspaces from './workspaces'; +import signup from './signup'; export { auth, secrets, - workspaces + signup, + workspaces, } diff --git a/backend/src/routes/v3/signup.ts b/backend/src/routes/v3/signup.ts new file mode 100644 index 000000000..52cf4899e --- /dev/null +++ b/backend/src/routes/v3/signup.ts @@ -0,0 +1,28 @@ +import express from 'express'; +const router = express.Router(); +import { body } from 'express-validator'; +import { signupController } from '../../controllers/v3'; +import { authLimiter } from '../../helpers/rateLimiter'; +import { validateRequest } from '../../middleware'; + +router.post( + '/complete-account/signup', + authLimiter, + body('email').exists().isString().trim().notEmpty().isEmail(), + body('firstName').exists().isString().trim().notEmpty(), + body('lastName').exists().isString().trim().notEmpty(), + body('protectedKey').exists().isString().trim().notEmpty(), + body('protectedKeyIV').exists().isString().trim().notEmpty(), + body('protectedKeyTag').exists().isString().trim().notEmpty(), + body('publicKey').exists().isString().trim().notEmpty(), + body('encryptedPrivateKey').exists().isString().trim().notEmpty(), + body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), + body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), + body('salt').exists().isString().trim().notEmpty(), + body('verifier').exists().isString().trim().notEmpty(), + body('organizationName').exists().isString().trim().notEmpty(), + validateRequest, + signupController.completeAccountSignup, +); + +export default router; diff --git a/frontend/src/components/login/PasswordInputStep.tsx b/frontend/src/components/login/PasswordInputStep.tsx index b3c881a03..d817fea24 100644 --- a/frontend/src/components/login/PasswordInputStep.tsx +++ b/frontend/src/components/login/PasswordInputStep.tsx @@ -12,7 +12,6 @@ import { getTranslatedStaticProps } from '@app/components/utilities/withTranslat import SecurityClient from '../utilities/SecurityClient'; export default function PasswordInputStep({ - userId, email, password, setPassword, @@ -20,7 +19,6 @@ export default function PasswordInputStep({ setStep }: { email: string; - userId: string; password: string; setPassword: (password: string) => void; setProviderAuthToken: (value: string) => void; @@ -34,13 +32,8 @@ export default function PasswordInputStep({ const handleLogin = async () => { try { - if (!userId || !password) { - return; - } - setIsLoading(true); const isLoginSuccessful = await attemptLogin({ - userId, email, password }); diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 32504cb2c..dc49ce02f 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -32,6 +32,7 @@ interface UserInfoStepProps { setFirstName: (value: string) => void; lastName: string; setLastName: (value: string) => void; + providerAuthToken?: string; } /** @@ -55,7 +56,8 @@ export default function UserInfoStep({ firstName, setFirstName, lastName, - setLastName + setLastName, + providerAuthToken, }: UserInfoStepProps): JSX.Element { const [firstNameError, setFirstNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false); @@ -118,11 +120,11 @@ export default function UserInfoStep({ parallelism: 1, hashLen: 32 }); - + if (!derivedKey) throw new Error('Failed to derive key from password'); const key = crypto.randomBytes(32); - + // create encrypted private key by encrypting the private // key with the symmetric key [key] const { @@ -133,7 +135,7 @@ export default function UserInfoStep({ text: privateKey, secret: key }); - + // create the protected key by encrypting the symmetric key // [key] with the derived key const { @@ -144,7 +146,7 @@ export default function UserInfoStep({ text: key.toString('hex'), secret: Buffer.from(derivedKey.hash) }); - + const response = await completeAccountInformationSignup({ email, firstName, @@ -156,11 +158,12 @@ export default function UserInfoStep({ encryptedPrivateKey, encryptedPrivateKeyIV, encryptedPrivateKeyTag, + providerAuthToken, salt: result.salt, verifier: result.verifier, - organizationName: `${firstName}'s organization` + organizationName: `${firstName}'s organization`, }); - + // unset signup JWT token and set JWT token SecurityClient.setSignupToken(''); SecurityClient.setToken(response.token); diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index 08bef1441..90a9876ea 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -29,20 +29,17 @@ const attemptLogin = async ( { email, password, - userId, }: { email: string; - userId?: string; password: string; } ): Promise => { - const username = userId ?? email; const telemetry = new Telemetry().getInstance(); return new Promise((resolve, reject) => { client.init( { - username, + username: email, password }, async () => { @@ -51,7 +48,6 @@ const attemptLogin = async ( const { serverPublicKey, salt } = await login1({ email, clientPublicKey, - userId, }); client.setSalt(salt); @@ -72,7 +68,6 @@ const attemptLogin = async ( } = await login2( { email, - userId, clientProof, } ); diff --git a/frontend/src/hooks/useProviderAuth.ts b/frontend/src/hooks/useProviderAuth.ts index 0cb3fb0f7..dc380d7c1 100644 --- a/frontend/src/hooks/useProviderAuth.ts +++ b/frontend/src/hooks/useProviderAuth.ts @@ -31,10 +31,16 @@ export const useProviderAuth = () => { window.addEventListener('storage', handleStorageChange); + if (providerAuthToken) { + const { userId: resultUserId, email: resultEmail } = jwt_decode(providerAuthToken) as any; + setEmail(resultEmail); + setUserId(resultUserId); + } + return () => { window.removeEventListener('storage', handleStorageChange); }; - }); + }, []); return { email, diff --git a/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts b/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts index 946505887..302dade1b 100644 --- a/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts +++ b/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts @@ -8,6 +8,7 @@ interface Props { protectedKey: string; protectedKeyIV: string; protectedKeyTag: string; + providerAuthToken?: string; publicKey: string; encryptedPrivateKey: string; encryptedPrivateKeyIV: string; @@ -49,9 +50,10 @@ const completeAccountInformationSignup = async ({ encryptedPrivateKeyTag, salt, verifier, - organizationName + organizationName, + providerAuthToken, }: Props) => { - const { data } = await apiRequest.post('/api/v2/signup/complete-account/signup', { + const { data } = await apiRequest.post('/api/v3/signup/complete-account/signup', { email, firstName, lastName, @@ -64,7 +66,8 @@ const completeAccountInformationSignup = async ({ encryptedPrivateKeyTag, salt, verifier, - organizationName + organizationName, + providerAuthToken, }); return data; diff --git a/frontend/src/pages/api/auth/Login1.ts b/frontend/src/pages/api/auth/Login1.ts index 2685a770c..dbf54e37e 100644 --- a/frontend/src/pages/api/auth/Login1.ts +++ b/frontend/src/pages/api/auth/Login1.ts @@ -12,7 +12,6 @@ interface Login1 { const login1 = async (loginDetails: { email: string; clientPublicKey: string; - userId?: string; }) => { const response = await fetch("/api/v2/auth/login1", { method: "POST", diff --git a/frontend/src/pages/api/auth/Login2.ts b/frontend/src/pages/api/auth/Login2.ts index 724a8b1d4..7699d6376 100644 --- a/frontend/src/pages/api/auth/Login2.ts +++ b/frontend/src/pages/api/auth/Login2.ts @@ -20,7 +20,6 @@ interface Login2Response { const login2 = async (loginDetails: { email: string; clientProof: string; - userId?: string; }) => { const response = await fetch('/api/v2/auth/login2', { method: 'POST', diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 958328b34..bc39198c6 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -25,7 +25,6 @@ export default function Login() { const [isLoginWithEmail, setIsLoginWithEmail] = useState(false); const { providerAuthToken, - userId, email: providerEmail, setProviderAuthToken } = useProviderAuth(); @@ -57,7 +56,6 @@ export default function Login() { if (providerAuthToken && step === 1) { return ( @@ -127,13 +127,14 @@ export default function SignUp() { return ( ) } @@ -142,7 +143,7 @@ export default function SignUp() { return ( From b734b519540199930362424efcb3afc8f82080fe Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 5 May 2023 00:39:28 +0800 Subject: [PATCH 07/60] developed new ui for new login and signup page --- frontend/public/locales/en/login.json | 4 +- frontend/public/locales/en/signup.json | 4 ++ .../src/components/login/InitialLoginStep.tsx | 36 ++++++++++++++++++ .../components/login/PasswordInputStep.tsx | 2 +- .../components/signup/InitialSignupStep.tsx | 36 ++++++++++++++++++ frontend/src/pages/login.tsx | 18 ++------- frontend/src/pages/signup.tsx | 37 ++++++------------- 7 files changed, 94 insertions(+), 43 deletions(-) create mode 100644 frontend/src/components/login/InitialLoginStep.tsx create mode 100644 frontend/src/components/signup/InitialSignupStep.tsx diff --git a/frontend/public/locales/en/login.json b/frontend/public/locales/en/login.json index 75b56a014..49ef6cf2e 100644 --- a/frontend/public/locales/en/login.json +++ b/frontend/public/locales/en/login.json @@ -6,5 +6,7 @@ "need-account": "Need an Infisical account?", "create-account": "Create an account", "forgot-password": "Forgot your password?", - "error-login": "Wrong credentials." + "error-login": "Wrong credentials.", + "continue-with-google": "Continue with Google", + "continue-with-email": "Continue with Email" } diff --git a/frontend/public/locales/en/signup.json b/frontend/public/locales/en/signup.json index 5fa5742e3..4a66f702d 100644 --- a/frontend/public/locales/en/signup.json +++ b/frontend/public/locales/en/signup.json @@ -1,4 +1,8 @@ { + "initial-title": "Create your Infisical account", + "continue-with-google": "Continue with Google", + "continue-with-email": "Continue with Email", + "create-policy": "By signing up, you agree to our Terms of Service and Privacy Policy.", "title": "Sign Up", "og-title": "Replace .env files with 1 line of code. Sign Up for Infisical in 3 minutes.", "og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage API-keys and environemntal variables. Works with Node.js, Next.js, Gatsby, Nest.js...", diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx new file mode 100644 index 000000000..3e5ffba33 --- /dev/null +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from 'react-i18next'; +import Link from 'next/link'; + +import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; + +export default function InitialLoginStep({ + setIsLoginWithEmail, +}: { + setIsLoginWithEmail: (value: boolean) => void; +}) { + + const { t } = useTranslation(); + + return
+

Login to Infisical

+
+ +
+
+ +
+ +

{t('login:create-account')}

+ +
+} + +export const getStaticProps = getTranslatedStaticProps(['login']); diff --git a/frontend/src/components/login/PasswordInputStep.tsx b/frontend/src/components/login/PasswordInputStep.tsx index d817fea24..c85fe6d1d 100644 --- a/frontend/src/components/login/PasswordInputStep.tsx +++ b/frontend/src/components/login/PasswordInputStep.tsx @@ -121,7 +121,7 @@ export default function PasswordInputStep({ type="button" className="ml-1.5 text-sm font-normal text-primary-700 underline-offset-4 duration-200 hover:text-primary" > - Log in with other options. + Login with other option
diff --git a/frontend/src/components/signup/InitialSignupStep.tsx b/frontend/src/components/signup/InitialSignupStep.tsx new file mode 100644 index 000000000..b859ddd46 --- /dev/null +++ b/frontend/src/components/signup/InitialSignupStep.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from 'react-i18next'; +import Link from 'next/link'; + +import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; + +export default function InitialSignupStep({ + setIsSignupWithEmail, +}: { + setIsSignupWithEmail: (value: boolean) => void +}) { + const { t } = useTranslation(); + + return
+

{t('signup:initial-title')}

+
+ +
+
+ +
+ {t('signup:create-policy')} + +

{t('signup:already-have-account')}

+ +
+} + +export const getStaticProps = getTranslatedStaticProps(['signup']); diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index bc39198c6..2be66ce61 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -6,6 +6,7 @@ import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; import ListBox from '@app/components/basic/Listbox'; +import InitialLoginStep from '@app/components/login/InitialLoginStep'; import LoginStep from '@app/components/login/LoginStep'; import MFAStep from '@app/components/login/MFAStep'; import PasswordInputStep from '@app/components/login/PasswordInputStep'; @@ -78,20 +79,7 @@ export default function Login() { } if (!isLoginWithEmail && loginStep === 1) { - return ( - <> - - - - ) + return } if (step === 2) { @@ -105,7 +93,7 @@ export default function Login() { } return ( -
+
{t('common:head-title', { title: t('login:title') })} diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index ea3715901..645ef96aa 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'next-i18next'; import CodeInputStep from '@app/components/signup/CodeInputStep'; import DownloadBackupPDF from '@app/components/signup/DonwloadBackupPDFStep'; import EnterEmailStep from '@app/components/signup/EnterEmailStep'; +import InitialSignupStep from '@app/components/signup/InitialSignupStep'; import TeamInviteStep from '@app/components/signup/TeamInviteStep'; import UserInfoStep from '@app/components/signup/UserInfoStep'; import SecurityClient from '@app/components/utilities/SecurityClient'; @@ -19,7 +20,6 @@ import { useProviderAuth } from '@app/hooks/useProviderAuth'; import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode'; import getWorkspaces from './api/workspace/getWorkspaces'; - /** * @returns the signup page */ @@ -96,20 +96,7 @@ export default function SignUp() { } if (!isSignupWithEmail && registerStep === 1) { - return ( - <> - - - - ) + return } if (registerStep === 2) { @@ -158,7 +145,7 @@ export default function SignUp() { } return ( -
+
{t('common:head-title', { title: t('signup:title') })} @@ -166,16 +153,14 @@ export default function SignUp() { -
- -
- Infisical Wide Logo -
- -
e.preventDefault()}> - {renderView(step)} -
-
+ +
+ long logo +
+ +
e.preventDefault()}> + {renderView(step)} +
); } From 3e19e6fd99b0a9fa53d4a05ef4975eb3e2972bf0 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 6 May 2023 18:52:01 +0800 Subject: [PATCH 08/60] finalized login and signup ui --- frontend/public/locales/en/login.json | 3 ++- frontend/src/components/login/InitialLoginStep.tsx | 8 +++++--- frontend/src/components/login/PasswordInputStep.tsx | 2 +- frontend/src/components/signup/InitialSignupStep.tsx | 10 ++++++---- frontend/src/components/utilities/SecurityClient.ts | 2 +- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/frontend/public/locales/en/login.json b/frontend/public/locales/en/login.json index 49ef6cf2e..3aafeec4a 100644 --- a/frontend/public/locales/en/login.json +++ b/frontend/public/locales/en/login.json @@ -8,5 +8,6 @@ "forgot-password": "Forgot your password?", "error-login": "Wrong credentials.", "continue-with-google": "Continue with Google", - "continue-with-email": "Continue with Email" + "continue-with-email": "Continue with Email", + "other-option": "Log in with other option" } diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index 3e5ffba33..5ec56ec11 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -13,14 +13,14 @@ export default function InitialLoginStep({ return

Login to Infisical

-
+
-
+
-

{t('login:create-account')}

+
} diff --git a/frontend/src/components/login/PasswordInputStep.tsx b/frontend/src/components/login/PasswordInputStep.tsx index c85fe6d1d..77d9b7ae0 100644 --- a/frontend/src/components/login/PasswordInputStep.tsx +++ b/frontend/src/components/login/PasswordInputStep.tsx @@ -121,7 +121,7 @@ export default function PasswordInputStep({ type="button" className="ml-1.5 text-sm font-normal text-primary-700 underline-offset-4 duration-200 hover:text-primary" > - Login with other option + {t('login:other-option')}
diff --git a/frontend/src/components/signup/InitialSignupStep.tsx b/frontend/src/components/signup/InitialSignupStep.tsx index b859ddd46..cb15a6514 100644 --- a/frontend/src/components/signup/InitialSignupStep.tsx +++ b/frontend/src/components/signup/InitialSignupStep.tsx @@ -12,23 +12,25 @@ export default function InitialSignupStep({ return

{t('signup:initial-title')}

-
+
-
+
- {t('signup:create-policy')} + {t('signup:create-policy')} -

{t('signup:already-have-account')}

+
} diff --git a/frontend/src/components/utilities/SecurityClient.ts b/frontend/src/components/utilities/SecurityClient.ts index 6e686be3b..86cd56e41 100644 --- a/frontend/src/components/utilities/SecurityClient.ts +++ b/frontend/src/components/utilities/SecurityClient.ts @@ -11,7 +11,7 @@ export const PROVIDER_AUTH_TOKEN_KEY = 'infisical__provider-auth-token'; export default class SecurityClient { static setProviderAuthToken(tokenStr: string) { - localStorage.setItem(PROVIDER_AUTH_TOKEN_KEY, tokenStr) + localStorage.setItem(PROVIDER_AUTH_TOKEN_KEY, tokenStr || '') } static getProviderAuthToken() { From ea86e59d4f01549600326b73abded0261e40943b Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 6 May 2023 19:26:42 +0800 Subject: [PATCH 09/60] resolved component alignment of signup --- frontend/src/components/signup/DonwloadBackupPDFStep.tsx | 6 +++--- frontend/src/components/signup/EnterEmailStep.tsx | 2 +- frontend/src/pages/signup.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/signup/DonwloadBackupPDFStep.tsx b/frontend/src/components/signup/DonwloadBackupPDFStep.tsx index 240d6468a..41cefd12c 100644 --- a/frontend/src/components/signup/DonwloadBackupPDFStep.tsx +++ b/frontend/src/components/signup/DonwloadBackupPDFStep.tsx @@ -30,7 +30,7 @@ export default function DonwloadBackupPDFStep({ const { t } = useTranslation(); return ( -
+

{t('signup:step4-message')}

@@ -50,8 +50,8 @@ export default function DonwloadBackupPDFStep({ email, password, personalName: name, - setBackupKeyError: () => {}, - setBackupKeyIssued: () => {} + setBackupKeyError: () => { }, + setBackupKeyIssued: () => { } }); incrementStep(); }} diff --git a/frontend/src/components/signup/EnterEmailStep.tsx b/frontend/src/components/signup/EnterEmailStep.tsx index d7114946e..e4c5e8f79 100644 --- a/frontend/src/components/signup/EnterEmailStep.tsx +++ b/frontend/src/components/signup/EnterEmailStep.tsx @@ -56,7 +56,7 @@ export default function EnterEmailStep({ return (
-
+

{t('signup:step1-start')}

diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index 645ef96aa..8388b763b 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -145,7 +145,7 @@ export default function SignUp() { } return ( -
+
{t('common:head-title', { title: t('signup:title') })} From 4c434555a4de57e5bd9d03392d426dacc565505a Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 7 May 2023 20:54:30 +0800 Subject: [PATCH 10/60] finalized signup/signin ux regarding redirects --- backend/src/controllers/v3/authController.ts | 1 + backend/src/utils/auth.ts | 3 ++- .../components/login/PasswordInputStep.tsx | 6 +++--- .../src/components/signup/UserInfoStep.tsx | 1 + .../src/components/utilities/attemptLogin.ts | 4 +++- .../components/utilities/attemptLoginMfa.ts | 6 +++++- frontend/src/hooks/useProviderAuth.ts | 20 +++++++++++++++---- frontend/src/pages/login.tsx | 7 ++++++- frontend/src/pages/signup.tsx | 10 +++++++++- 9 files changed, 46 insertions(+), 12 deletions(-) diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 7066dc866..e6492ef31 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -28,6 +28,7 @@ declare module 'jsonwebtoken' { userId: string; email: string; authProvider: AuthProvider; + isUserCompleted: boolean, } } diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index fac1af5b4..02627fed8 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -74,7 +74,7 @@ const initializePassport = async () => { let user = await User.findOne({ authProvider: AuthProvider.GOOGLE, authId: profile.id, - }) + }).select('+publicKey') if (!user) { user = await new User({ @@ -89,6 +89,7 @@ const initializePassport = async () => { userId: user._id.toString(), email: user.email, authProvider: user.authProvider, + isUserCompleted: !!user.publicKey }, expiresIn: await getJwtProviderAuthLifetime(), secret: await getJwtProviderAuthSecret(), diff --git a/frontend/src/components/login/PasswordInputStep.tsx b/frontend/src/components/login/PasswordInputStep.tsx index 77d9b7ae0..819f76528 100644 --- a/frontend/src/components/login/PasswordInputStep.tsx +++ b/frontend/src/components/login/PasswordInputStep.tsx @@ -33,15 +33,15 @@ export default function PasswordInputStep({ const handleLogin = async () => { try { setIsLoading(true); - const isLoginSuccessful = await attemptLogin({ + const loginAttempt = await attemptLogin({ email, password }); - if (isLoginSuccessful && isLoginSuccessful.success) { + if (loginAttempt && loginAttempt.success) { // case: login was successful - if (isLoginSuccessful.mfaEnabled) { + if (loginAttempt.mfaEnabled) { // case: login requires MFA step setStep(2); setIsLoading(false); diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index dc49ce02f..e62d1d3a4 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -167,6 +167,7 @@ export default function UserInfoStep({ // unset signup JWT token and set JWT token SecurityClient.setSignupToken(''); SecurityClient.setToken(response.token); + SecurityClient.setProviderAuthToken(''); saveTokenToLocalStorage({ publicKey, diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index 90a9876ea..c5bc39810 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -91,7 +91,9 @@ const attemptLogin = async ( token ) { // case: MFA is not enabled - + + // unset provider auth token in case it was used + SecurityClient.setProviderAuthToken(''); // set JWT token SecurityClient.setToken(token); diff --git a/frontend/src/components/utilities/attemptLoginMfa.ts b/frontend/src/components/utilities/attemptLoginMfa.ts index 58d3b2330..b7d499817 100644 --- a/frontend/src/components/utilities/attemptLoginMfa.ts +++ b/frontend/src/components/utilities/attemptLoginMfa.ts @@ -36,7 +36,10 @@ const attemptLoginMfa = async ({ }, async () => { try { const clientPublicKey = client.getPublicKey(); - const { salt } = await login1(email, clientPublicKey); + const { salt } = await login1({ + email, + clientPublicKey, + }); const { encryptionVersion, @@ -56,6 +59,7 @@ const attemptLoginMfa = async ({ // unset temporary (MFA) JWT token and set JWT token SecurityClient.setMfaToken(''); SecurityClient.setToken(token); + SecurityClient.setProviderAuthToken(''); const privateKey = await KeyService.decryptPrivateKey({ encryptionVersion, diff --git a/frontend/src/hooks/useProviderAuth.ts b/frontend/src/hooks/useProviderAuth.ts index dc380d7c1..bda90e6e6 100644 --- a/frontend/src/hooks/useProviderAuth.ts +++ b/frontend/src/hooks/useProviderAuth.ts @@ -9,14 +9,19 @@ export const useProviderAuth = () => { const [providerAuthToken, setProviderAuthToken] = useState( SecurityClient.getProviderAuthToken() || '' ); + const [isProviderUserCompleted, setIsProviderUserCompleted] = useState(); 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; - + const { + userId: resultUserId, + email: resultEmail, + isUserCompleted: resultIsUserCompleted, + } = jwt_decode(token) as any; + setIsProviderUserCompleted(resultIsUserCompleted); setProviderAuthToken(token); setEmail(resultEmail); setUserId(resultUserId); @@ -24,6 +29,7 @@ export const useProviderAuth = () => { setProviderAuthToken(''); setEmail(''); setUserId(''); + setIsProviderUserCompleted(false); } setProviderAuthToken(event.newValue || ''); } @@ -32,9 +38,14 @@ export const useProviderAuth = () => { window.addEventListener('storage', handleStorageChange); if (providerAuthToken) { - const { userId: resultUserId, email: resultEmail } = jwt_decode(providerAuthToken) as any; + const { + userId: resultUserId, + email: resultEmail, + isUserCompleted: resultIsUserCompleted, + } = jwt_decode(providerAuthToken) as any; setEmail(resultEmail); setUserId(resultUserId); + setIsProviderUserCompleted(resultIsUserCompleted); } return () => { @@ -44,10 +55,11 @@ export const useProviderAuth = () => { return { email, + isProviderUserCompleted, providerAuthToken, userId, - setProviderAuthToken, setEmail, + setProviderAuthToken, setUserId, }; }; diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 2be66ce61..01fe98675 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -27,9 +27,14 @@ export default function Login() { const { providerAuthToken, email: providerEmail, - setProviderAuthToken + setProviderAuthToken, + isProviderUserCompleted, } = useProviderAuth(); + if (providerAuthToken && isProviderUserCompleted === false) { + router.push('/signup'); + } + const setLanguage = async (to: string) => { router.push('/login', '/login', { locale: to }); localStorage.setItem('lang', to); diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index 8388b763b..d47044599 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -35,7 +35,15 @@ export default function SignUp() { const { data: serverDetails } = useFetchServerStatus(); const [isSignupWithEmail, setIsSignupWithEmail] = useState(false); const { t } = useTranslation(); - const { email: providerEmail, providerAuthToken } = useProviderAuth(); + const { + email: providerEmail, + providerAuthToken, + isProviderUserCompleted, + } = useProviderAuth(); + + if (providerAuthToken && isProviderUserCompleted) { + router.push('/login'); + } if (providerAuthToken && step < 3) { setStep(3); From 4e7145dfe5dcde727471470237634029a0a37cbe Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Tue, 9 May 2023 20:45:59 -0700 Subject: [PATCH 11/60] Style changes to login --- frontend/public/images/gradientLogo.svg | 9 ++ .../src/components/login/InitialLoginStep.tsx | 51 ++++++--- frontend/src/components/login/LoginStep.tsx | 108 ++++++++---------- .../src/components/signup/CodeInputStep.tsx | 41 ++++--- .../src/components/signup/EnterEmailStep.tsx | 55 ++++----- .../components/signup/InitialSignupStep.tsx | 52 ++++++--- frontend/src/components/v2/Button/Button.tsx | 7 +- frontend/src/components/v2/Input/Input.tsx | 6 +- frontend/src/pages/login.tsx | 11 +- frontend/src/pages/signup.tsx | 11 +- 10 files changed, 186 insertions(+), 165 deletions(-) create mode 100644 frontend/public/images/gradientLogo.svg diff --git a/frontend/public/images/gradientLogo.svg b/frontend/public/images/gradientLogo.svg new file mode 100644 index 000000000..7698d8e90 --- /dev/null +++ b/frontend/public/images/gradientLogo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index 5ec56ec11..ff343e574 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -1,8 +1,12 @@ import { useTranslation } from 'react-i18next'; import Link from 'next/link'; +import { faGoogle } from '@fortawesome/free-brands-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; +import { Button } from '../v2'; + export default function InitialLoginStep({ setIsLoginWithEmail, }: { @@ -12,26 +16,39 @@ export default function InitialLoginStep({ const { t } = useTranslation(); return
-

Login to Infisical

-
- +
-
- +
+ +
+
+ Don't have an acount yet? + + {t('login:create-account')} +
- - -
} diff --git a/frontend/src/components/login/LoginStep.tsx b/frontend/src/components/login/LoginStep.tsx index a45ebe3e5..dc8a6f155 100644 --- a/frontend/src/components/login/LoginStep.tsx +++ b/frontend/src/components/login/LoginStep.tsx @@ -5,12 +5,12 @@ import { useTranslation } from 'next-i18next'; import { faWarning } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import Button from '@app/components/basic/buttons/Button'; import Error from '@app/components/basic/Error'; -import InputField from '@app/components/basic/InputField'; import attemptLogin from '@app/components/utilities/attemptLogin'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; +import { Button, Input } from '../v2'; + /** * 1st step of login - user enters their username and password * @param {Object} obj @@ -74,55 +74,54 @@ export default function LoginStep ({ return (
e.preventDefault()}> -
-

- {t('login:login')} -

-
- +
+

+ Enter your email and password +

+
+ setEmail(e.target.value)} + type="email" + placeholder="Enter your email..." + isRequired + autoComplete="username" + className="h-12" + /> +
+
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your password..." + isRequired + autoComplete="current-password" + id="current-password" + className="h-12" + /> +
+
+ {!isLoading && loginError && } +
+
+ +
+
-
- -
+
- + {t('login:forgot-password')} -
-
- {!isLoading && loginError && } -
-
-
-
{false && (
@@ -130,19 +129,6 @@ export default function LoginStep ({ {t('common:maintenance-alert')}
)} -
-

- {t('login:need-account')} -

- - - -
); } diff --git a/frontend/src/components/signup/CodeInputStep.tsx b/frontend/src/components/signup/CodeInputStep.tsx index 67b4e530c..340389259 100644 --- a/frontend/src/components/signup/CodeInputStep.tsx +++ b/frontend/src/components/signup/CodeInputStep.tsx @@ -5,8 +5,8 @@ import { useTranslation } from 'next-i18next'; import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail'; -import Button from '../basic/buttons/Button'; import Error from '../basic/Error'; +import { Button } from '../v2'; // The style for the verification code input const props = { @@ -83,10 +83,10 @@ export default function CodeInputStep({ }; return ( -
-

{t('signup:step2-message')}

-

{email}

-
+
+

{t('signup:step2-message')}

+

{email}

+
-
+
{codeError && } -
- +
{t('signup:step2-resend-alert')} - +
- +

{t('signup:step2-spam-alert')}

diff --git a/frontend/src/components/signup/EnterEmailStep.tsx b/frontend/src/components/signup/EnterEmailStep.tsx index e4c5e8f79..2b412d301 100644 --- a/frontend/src/components/signup/EnterEmailStep.tsx +++ b/frontend/src/components/signup/EnterEmailStep.tsx @@ -1,11 +1,9 @@ import React, { useState } from 'react'; -import Link from 'next/link'; import { useTranslation } from 'next-i18next'; import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail'; -import Button from '../basic/buttons/Button'; -import InputField from '../basic/InputField'; +import { Button, Input } from '../v2'; interface DownloadBackupPDFStepProps { incrementStep: () => void; @@ -27,7 +25,6 @@ export default function EnterEmailStep({ incrementStep }: DownloadBackupPDFStepProps): JSX.Element { const [emailError, setEmailError] = useState(false); - const [emailErrorMessage, setEmailErrorMessage] = useState(''); const { t } = useTranslation(); /** @@ -37,11 +34,9 @@ export default function EnterEmailStep({ let emailCheckBool = false; if (!email) { setEmailError(true); - setEmailErrorMessage('Please enter your email.'); emailCheckBool = true; } else if (!email.includes('@') || !email.includes('.') || !/[a-z]/.test(email)) { setEmailError(true); - setEmailErrorMessage('Please enter a valid email.'); emailCheckBool = true; } else { setEmailError(false); @@ -56,44 +51,36 @@ export default function EnterEmailStep({ return (
-
-

- {t('signup:step1-start')} +

+

+ {t('signup:initial-title')}

-
- + setEmail(e.target.value)} value={email} - placeholder="" isRequired - error={emailError} - errorText={emailErrorMessage} + // error={emailError} + // errorText={emailErrorMessage} autoComplete="username" + className="h-12" /> + {emailError &&

Please enter a valid email.

}
-
-

{t('signup:step1-privacy')}

-
+
+
-
- - - -
); } diff --git a/frontend/src/components/signup/InitialSignupStep.tsx b/frontend/src/components/signup/InitialSignupStep.tsx index cb15a6514..f3e4fee86 100644 --- a/frontend/src/components/signup/InitialSignupStep.tsx +++ b/frontend/src/components/signup/InitialSignupStep.tsx @@ -1,8 +1,12 @@ import { useTranslation } from 'react-i18next'; import Link from 'next/link'; +import { faGoogle } from '@fortawesome/free-brands-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps'; +import { Button } from '../v2'; + export default function InitialSignupStep({ setIsSignupWithEmail, }: { @@ -11,27 +15,41 @@ export default function InitialSignupStep({ const { t } = useTranslation(); return
-

{t('signup:initial-title')}

-
- +
-
- + +
+
+ {t('signup:create-policy')} +
+
+ + {t('signup:already-have-account')} +
- {t('signup:create-policy')} - - -
} diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index 17ae7b867..8d4294e44 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -63,6 +63,11 @@ const buttonVariants = cva( variant: 'star', className: 'hover:bg-primary hover:text-black' }, + { + colorSchema: 'primary', + variant: 'solid', + className: 'bg-primary-500 bg-opacity-90 hover:bg-primary-500 hover:text-black' + }, { colorSchema: 'primary', variant: 'selected', @@ -71,7 +76,7 @@ const buttonVariants = cva( { colorSchema: 'primary', variant: 'outline_bg', - className: 'bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200' + className: 'bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200' }, { colorSchema: 'secondary', diff --git a/frontend/src/components/v2/Input/Input.tsx b/frontend/src/components/v2/Input/Input.tsx index 98784cdc0..cc912e7db 100644 --- a/frontend/src/components/v2/Input/Input.tsx +++ b/frontend/src/components/v2/Input/Input.tsx @@ -13,7 +13,7 @@ type Props = { }; const inputVariants = cva( - 'input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2', + 'input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-[0.05rem] hover:ring-bunker-400/60 duration-100', { variants: { size: { @@ -27,13 +27,13 @@ const inputVariants = cva( false: '' }, variant: { - filled: ['bg-bunker-800', 'text-gray-400'], + filled: ['bg-mineshaft-900', 'text-gray-400'], outline: ['bg-transparent'], plain: 'bg-transparent outline-none' }, isError: { true: 'focus:ring-red/50 placeholder-red-300', - false: 'focus:ring-primary/50' + false: 'focus:ring-primary-400/50 focus:ring-1' } }, compoundVariants: [] diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 01fe98675..6559a2fd3 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; -import Link from 'next/link'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; @@ -98,7 +97,7 @@ export default function Login() { } return ( -
+
{t('common:head-title', { title: t('login:title') })} @@ -106,11 +105,9 @@ export default function Login() { - -
- long logo -
- +
+ Infisical logo +
{renderView(step)}
diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index d47044599..de85be8bc 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; -import Link from 'next/link'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; @@ -153,7 +152,7 @@ export default function SignUp() { } return ( -
+
{t('common:head-title', { title: t('signup:title') })} @@ -161,11 +160,9 @@ export default function SignUp() { - -
- long logo -
- +
+ Infisical Logo +
e.preventDefault()}> {renderView(step)}
From 1fce8cc769a6d833c674e4ee7a6330bd7dea8e1e Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Tue, 9 May 2023 23:24:37 -0700 Subject: [PATCH 12/60] More style changes to login --- frontend/src/components/basic/InputField.tsx | 18 +-- .../signup/DonwloadBackupPDFStep.tsx | 56 ++++----- .../src/components/signup/EnterEmailStep.tsx | 2 - .../src/components/signup/TeamInviteStep.tsx | 76 ++++++------ .../src/components/signup/UserInfoStep.tsx | 110 +++++++++--------- frontend/src/pages/signup.tsx | 23 ++-- 6 files changed, 141 insertions(+), 144 deletions(-) diff --git a/frontend/src/components/basic/InputField.tsx b/frontend/src/components/basic/InputField.tsx index a8ad953a4..df6dd1424 100644 --- a/frontend/src/components/basic/InputField.tsx +++ b/frontend/src/components/basic/InputField.tsx @@ -61,22 +61,6 @@ const InputField = ({

{label}

- {/* {label === "Password" && router.asPath !== "/login" && ( -
- - - The password should contain at least 8 - characters including at least 1 lowercase - character, uppercase character, number, and a - special character. - -
- )} */}
void; @@ -30,33 +30,37 @@ export default function DonwloadBackupPDFStep({ const { t } = useTranslation(); return ( -
-

+

+

{t('signup:step4-message')}

-
-
{t('signup:step4-description1')}
-
{t('signup:step4-description2')}
-
-
- - {t('signup:step4-description3')} -
-
- +
+
); diff --git a/frontend/src/components/signup/EnterEmailStep.tsx b/frontend/src/components/signup/EnterEmailStep.tsx index 2b412d301..bf987eac1 100644 --- a/frontend/src/components/signup/EnterEmailStep.tsx +++ b/frontend/src/components/signup/EnterEmailStep.tsx @@ -61,8 +61,6 @@ export default function EnterEmailStep({ onChange={(e) => setEmail(e.target.value)} value={email} isRequired - // error={emailError} - // errorText={emailErrorMessage} autoComplete="username" className="h-12" /> diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/signup/TeamInviteStep.tsx index 12d558606..97dd7489a 100644 --- a/frontend/src/components/signup/TeamInviteStep.tsx +++ b/frontend/src/components/signup/TeamInviteStep.tsx @@ -39,50 +39,52 @@ export default function TeamInviteStep(): JSX.Element { }; return ( -
-

+

+

{t('signup:step5-invite-team')}

-

+

{t('signup:step5-subtitle')}

-
-
-
+
+
+
+ Emails +
+