From f703ee29e58b3d56b861fd9dcedf9eecd6ef627e Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 3 May 2023 18:58:32 +0800 Subject: [PATCH] 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 69271e3d06..c23a3354ac 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 efbc90df82..a3f74caaae 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 15341341f9..e60002e9d4 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 1da61835fc..8a8b1d54d8 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 0000000000..5f1511e39e --- /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 ccbded2661..7066dc866f 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 fcc3ba8ff4..134349e0c2 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 e63ec0e965..004a4378f2 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 6cd6a01daa..843296b550 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 8cabb60a22..bb5b38d53f 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 2ff0e6d27a..2125aaf807 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 4b9dcafca2..a38a5bfb2e 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 0000000000..4bd764ca3b --- /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 bfb51e6d28..fac1af5b46 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 1794b834b8..958328b348 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 86affcc775..5febfa69bf 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)}