diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 9db83878b..1e9a73da0 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -269,19 +269,36 @@ export const verifyMfaToken = async (req: Request, res: Response) => { secure: NODE_ENV === 'production' ? true : false }); + interface VerifyMfaTokenRes { + encryptionVersion: number; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey: string; + encryptedPrivateKey: string; + iv: string; + tag: string; + } + + const resObj: VerifyMfaTokenRes = { + encryptionVersion: user.encryptionVersion, + token: tokens.token, + publicKey: user.publicKey as string, + encryptedPrivateKey: user.encryptedPrivateKey as string, + iv: user.iv as string, + tag: user.tag as string + } + + if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { + resObj.protectedKey = user.protectedKey; + resObj.protectedKeyIV = user.protectedKeyIV; + resObj.protectedKeyTag = user.protectedKeyTag; + } + // case: user does not have MFA enabled // return (access) token in response - return res.status(200).send({ - encryptionVersion: user.encryptionVersion, - protectedKey: user.protectedKey ?? null, - protectedKeyIV: user.protectedKeyIV ?? null, - protectedKeyTag: user.protectedKeyTag ?? null, - token: tokens.token, - publicKey: user.publicKey, - encryptedPrivateKey: user.encryptedPrivateKey, - iv: user.iv, - tag: user.tag - }); + return res.status(200).send(resObj); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index 8a26a9e9a..0fb349edc 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -7,6 +7,7 @@ import { } from '../../helpers/signup'; import { issueAuthTokens } from '../../helpers/auth'; import { INVITED, ACCEPTED } from '../../variables'; +import { NODE_ENV } from '../../config'; import axios from 'axios'; /** @@ -105,7 +106,6 @@ export const completeAccountSignup = async (req: Request, res: Response) => { }); token = tokens.token; - refreshToken = tokens.refreshToken; // sending a welcome email to new users if (process.env.LOOPS_API_KEY) { @@ -121,6 +121,14 @@ export const completeAccountSignup = async (req: Request, res: Response) => { }, }); } + + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: NODE_ENV === 'production' ? true : false + }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -132,8 +140,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { return res.status(200).send({ message: 'Successfully set up account', user, - token, - refreshToken + token }); }; @@ -219,7 +226,14 @@ export const completeAccountInvite = async (req: Request, res: Response) => { }); token = tokens.token; - refreshToken = tokens.refreshToken; + + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: NODE_ENV === 'production' ? true : false + }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -227,11 +241,10 @@ export const completeAccountInvite = async (req: Request, res: Response) => { message: 'Failed to complete account setup' }); } - + return res.status(200).send({ message: 'Successfully set up account', user, - token, - refreshToken + token }); }; \ No newline at end of file diff --git a/backend/src/helpers/token.ts b/backend/src/helpers/token.ts index 551e4eb44..026bb4ae7 100644 --- a/backend/src/helpers/token.ts +++ b/backend/src/helpers/token.ts @@ -12,7 +12,7 @@ import { import { SALT_ROUNDS } from '../config'; -import { UnauthorizedRequestError } from '../utils/errors'; +import { ForbiddenRequestError } from '../utils/errors'; /** * Create and store a token in the database for purpose [type] @@ -65,29 +65,45 @@ const createTokenHelper = async ({ break; } - interface Query { + interface TokenDataQuery { type: string; email?: string; phoneNumber?: string; organization?: Types.ObjectId; } + + interface TokenDataUpdate { + type: string; + email?: string; + phoneNumber?: string; + organization?: Types.ObjectId; + tokenHash: string; + expiresAt: Date; + } - const query: Query = { type }; + const query: TokenDataQuery = { type }; + const update: TokenDataUpdate = { + type, + tokenHash: await bcrypt.hash(token, SALT_ROUNDS), + expiresAt + } - if (email) { query.email = email; } - if (phoneNumber) { query.phoneNumber = phoneNumber; } - if (organizationId) { query.organization = organizationId } + if (email) { + query.email = email; + update.email = email; + } + if (phoneNumber) { + query.phoneNumber = phoneNumber; + update.phoneNumber = phoneNumber; + } + if (organizationId) { + query.organization = organizationId + update.organization = organizationId + } await TokenData.findOneAndUpdate( query, - { - type, - email, - phoneNumber, - organization: organizationId, - tokenHash: await bcrypt.hash(token, SALT_ROUNDS), - expiresAt - }, + update, { new: true, upsert: true @@ -123,37 +139,38 @@ const validateTokenHelper = async ({ organizationId?: Types.ObjectId; token: string; }) => { - try { - interface Query { - type: string; - email?: string; - phoneNumber?: string; - organization?: Types.ObjectId; - } + interface Query { + type: string; + email?: string; + phoneNumber?: string; + organization?: Types.ObjectId; + } - const query: Query = { type }; + const query: Query = { type }; - if (email) { query.email = email; } - if (phoneNumber) { query.phoneNumber = phoneNumber; } - if (organizationId) { query.organization = organizationId; } + if (email) { query.email = email; } + if (phoneNumber) { query.phoneNumber = phoneNumber; } + if (organizationId) { query.organization = organizationId; } - const tokenData = await TokenData.findOneAndDelete(query); - - if (!tokenData) throw new Error('Failed to find token to validate'); - - if (tokenData.expiresAt < new Date()) throw new Error('Token has expired'); - - const isValid = await bcrypt.compare(token, tokenData.tokenHash); - if (!isValid) throw UnauthorizedRequestError({ + const tokenData = await TokenData.findOne(query).select('+tokenHash'); + + if (!tokenData) throw new Error('Failed to find token to validate'); + + if (tokenData.expiresAt < new Date()) { + await TokenData.findByIdAndDelete(tokenData._id); + throw ForbiddenRequestError({ + message: 'Failed token data validation due to token is no longer valid' + }); + } + + const isValid = await bcrypt.compare(token, tokenData.tokenHash); + if (!isValid) { + throw ForbiddenRequestError({ message: 'Failed token data validation due to incorrect token' }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error( - "Failed to validate token data" - ); } + + await TokenData.findByIdAndDelete(tokenData._id); } export { diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 50044387e..f70b989d2 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -1,11 +1,12 @@ import { ErrorRequestHandler } from "express"; import * as Sentry from '@sentry/node'; -import { InternalServerError } from "../utils/errors"; +import { InternalServerError, UnauthorizedRequestError } from "../utils/errors"; import { getLogger } from "../utils/logger"; import RequestError, { LogLevel } from "../utils/requestError"; import { NODE_ENV } from "../config"; +import { TokenExpiredError } from 'jsonwebtoken'; export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { if (res.headersSent) return next(); @@ -16,7 +17,9 @@ export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | E } //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError - if (!(error instanceof RequestError)) { + if (error instanceof TokenExpiredError) { + error = UnauthorizedRequestError({ stack: error.stack, message: 'Token expired' }); + } else if (!(error instanceof RequestError)) { error = InternalServerError({ context: { exception: error.message }, stack: error.stack }) getLogger('backend-main').log((error).levelName.toLowerCase(), (error).message) } diff --git a/docs/integrations/cicd/circleci.mdx b/docs/integrations/cicd/circleci.mdx index 7ded52d8a..1cfcbc00d 100644 --- a/docs/integrations/cicd/circleci.mdx +++ b/docs/integrations/cicd/circleci.mdx @@ -1,5 +1,37 @@ --- title: "Circle CI" +description: "How to automatically sync secrets from Infisical into your CircleCI project." --- -Coming soon. +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for CircleCI + +Obtain a Fly.io access token in Access Tokens + +![integrations fly dashboard](../../images/integrations-flyio-dashboard.png) +![integrations fly token](../../images/integrations-flyio-token.png) + +Press on the Fly.io tile and input your Fly.io access token to grant Infisical access to your Fly.io account. + +![integrations fly authorization](../../images/integrations-flyio-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which Fly.io app and press create integration to start syncing secrets to Fly.io. + +![integrations fly](../../images/integrations-flyio-create.png) +![integrations fly](../../images/integrations-flyio.png) diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 56943db3f..439564258 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -21,6 +21,8 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | | [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | +| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available | +| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available | | [React](/integrations/frameworks/react) | Framework | Available | | [Vue](/integrations/frameworks/vue) | Framework | Available | | [Express](/integrations/frameworks/express) | Framework | Available | @@ -39,8 +41,6 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | GCP | Cloud | Coming soon | | Azure | Cloud | Coming soon | | DigitalOcean | Cloud | Coming soon | -| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available | -| [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon | | TravisCI | CI/CD | Coming soon | | GitHub Actions | CI/CD | Coming soon | | Jenkins | CI/CD | Coming soon | diff --git a/frontend/public/locales/en/signup.json b/frontend/public/locales/en/signup.json index bee993727..5fa5742e3 100644 --- a/frontend/public/locales/en/signup.json +++ b/frontend/public/locales/en/signup.json @@ -9,9 +9,9 @@ "step1-start": "Let's get started", "step1-privacy": "By creating an account, you agree to our Terms and have read and acknowledged the Privacy Policy.", "step1-submit": "Get Started", - "step2-message": "We've sent a verification email to", + "step2-message": "We've sent a verification code to", "step2-code-error": "Oops. Your code is wrong. Please try again.", - "step2-resend-alert": "Don't see the email?", + "step2-resend-alert": "Don't see the code?", "step2-resend-submit": "Resend", "step2-resend-progress": "Resending...", "step2-spam-alert": "Make sure to check your spam inbox.", diff --git a/frontend/public/locales/fr/signup.json b/frontend/public/locales/fr/signup.json index 48b36e203..63d869193 100644 --- a/frontend/public/locales/fr/signup.json +++ b/frontend/public/locales/fr/signup.json @@ -11,7 +11,7 @@ "step1-submit": "C'est parti", "step2-message": "Nous avons envoyé un email de vérification à", "step2-code-error": "Oops. Votre code est faux. Veuillez réessayer.", - "step2-resend-alert": "Vous ne voyez pas l'email?", + "step2-resend-alert": "Vous ne voyez pas le code?", "step2-resend-submit": "Renvoyer", "step2-resend-progress": "Envoie en cours...", "step2-spam-alert": "Assurez-vous de vérifier vos spams.", diff --git a/frontend/public/locales/tr/signup.json b/frontend/public/locales/tr/signup.json index 4a0778335..663b6b72b 100644 --- a/frontend/public/locales/tr/signup.json +++ b/frontend/public/locales/tr/signup.json @@ -11,7 +11,7 @@ "step1-submit": "Başla", "step2-message": "Şu adrese bir doğrulama maili yolladık", "step2-code-error": "Tüh. Kodun hatalı. Lütfen tekrar dene.", - "step2-resend-alert": "Mail ulaşamadı mı?", + "step2-resend-alert": "Kodu ulaşamadı mı?", "step2-resend-submit": "Tekrar Yolla", "step2-resend-progress": "Tekrar yollanıyor...", "step2-spam-alert": "Spam kutunuzu kontrol ettiğinizden emin olun.", diff --git a/frontend/src/components/login/MFAStep.tsx b/frontend/src/components/login/MFAStep.tsx index fa876ac5a..8793064b1 100644 --- a/frontend/src/components/login/MFAStep.tsx +++ b/frontend/src/components/login/MFAStep.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa'; -import resendMfaToken from '@app/pages/api/auth/resendMfaToken'; +import { useSendMfaToken } from '@app/hooks/api/auth'; import Button from '../basic/buttons/Button'; import Error from '../basic/Error'; @@ -50,6 +50,8 @@ export default function MFAStep({ const [mfaCode, setMfaCode] = useState(''); const [codeError, setCodeError] = useState(false); + const sendMfaToken = useSendMfaToken(); + const { t } = useTranslation(); const handleLoginMfa = async () => { @@ -72,15 +74,14 @@ export default function MFAStep({ } catch (err) { console.error(err); + setIsLoading(false); setCodeError(true); } } const handleResendMfaCode = async () => { try { - await resendMfaToken({ - email - }); + await sendMfaToken.mutateAsync({ email }); } catch (err) { console.error(err); } diff --git a/frontend/src/components/navigation/NavBarDashboard.tsx b/frontend/src/components/navigation/NavBarDashboard.tsx index 92cb72973..e31b04ed0 100644 --- a/frontend/src/components/navigation/NavBarDashboard.tsx +++ b/frontend/src/components/navigation/NavBarDashboard.tsx @@ -87,7 +87,6 @@ export default function Navbar() { }, []); const closeApp = async () => { - console.log('Logging out...'); await logout(); router.push('/login'); }; diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index e13ba01d7..a7aad081c 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -161,6 +161,8 @@ export default function UserInfoStep({ organizationName: `${firstName}'s organization` }); + // unset signup JWT token and set JWT token + SecurityClient.setSignupToken(''); SecurityClient.setToken(response.token); saveTokenToLocalStorage({ @@ -174,14 +176,18 @@ export default function UserInfoStep({ privateKey }); - incrementStep(); - const userOrgs = await getOrganizations(); - await ProjectService.initProject({ - organizationId: userOrgs[0]?._id, + const orgId = userOrgs[0]?._id; + const project = await ProjectService.initProject({ + organizationId: orgId, projectName: 'Example Project' }); + localStorage.setItem('orgData.id', orgId); + localStorage.setItem('projectData.id', project._id); + + incrementStep(); + } catch (error) { setIsLoading(false); console.error(error); diff --git a/frontend/src/components/utilities/SecurityClient.ts b/frontend/src/components/utilities/SecurityClient.ts index f1ba05ed9..1b513a3ff 100644 --- a/frontend/src/components/utilities/SecurityClient.ts +++ b/frontend/src/components/utilities/SecurityClient.ts @@ -1,7 +1,15 @@ -import { getAuthToken, setAuthToken , setMfaTempToken } from '@app/reactQuery'; +import { + getAuthToken, + setAuthToken, + setMfaTempToken, + setSignupTempToken} from '@app/reactQuery'; // depreciated: go for apiRequest module in config/api export default class SecurityClient { + static setSignupToken(tokenStr: string) { + setSignupTempToken(tokenStr); + } + static setMfaToken(tokenStr: string) { setMfaTempToken(tokenStr); } diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index f6ab84715..464a4a4fe 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -44,7 +44,7 @@ const attemptLogin = async ( client.setSalt(salt); client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 - + const { mfaEnabled, encryptionVersion, @@ -65,7 +65,7 @@ const attemptLogin = async ( // case: MFA is enabled // set temporary (MFA) JWT token - SecurityClient.setToken(token); + SecurityClient.setMfaToken(token); resolve({ mfaEnabled, @@ -113,40 +113,14 @@ const attemptLogin = async ( const userOrgs = await getOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem('orgData.id', orgId); - + const orgUserProjects = await getOrganizationUserProjects({ orgId }); - localStorage.setItem('projectData.id', orgUserProjects[0]._id); - // // TODO: this part definitely needs to be refactored - // const userOrgs = await getOrganizations(); - // const userOrgsData = userOrgs.map((org: { _id: string }) => org._id); - - // let orgToLogin; - // if (userOrgsData.includes(localStorage.getItem('orgData.id'))) { - // orgToLogin = localStorage.getItem('orgData.id'); - // } else { - // orgToLogin = userOrgsData[0]; - // localStorage.setItem('orgData.id', orgToLogin); - // } - - // let orgUserProjects = await getOrganizationUserProjects({ - // orgId: orgToLogin - // }); - - // orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id); - // let projectToLogin; - // if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) { - // projectToLogin = localStorage.getItem('projectData.id'); - // } else { - // try { - // projectToLogin = orgUserProjects[0]; - // localStorage.setItem('projectData.id', projectToLogin); - // } catch (error) { - // console.log('ERROR: User likely has no projects. ', error); - // } - // } + if (orgUserProjects.length > 0) { + localStorage.setItem('projectData.id', orgUserProjects[0]._id); + } if (email) { telemetry.identify(email); @@ -166,100 +140,4 @@ const attemptLogin = async ( }); }; -export default attemptLogin; - -// should be function: init first project - -// if (isSignUp) { -// const randomBytes = crypto.randomBytes(16).toString('hex'); -// const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY')); - -// const myUser = await getUser(); - -// const { ciphertext, nonce } = encryptAssymmetric({ -// plaintext: randomBytes, -// publicKey: myUser.publicKey, -// privateKey: PRIVATE_KEY -// }) as { ciphertext: string; nonce: string }; - -// await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce); - -// const secretsToBeAdded: SecretDataProps[] = [ -// { -// pos: 0, -// key: 'DATABASE_URL', -// // eslint-disable-next-line no-template-curly-in-string -// value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net', -// valueOverride: undefined, -// comment: 'Secret referencing example', -// id: '', -// tags: [] -// }, -// { -// pos: 1, -// key: 'DB_USERNAME', -// value: 'OVERRIDE_THIS', -// valueOverride: undefined, -// comment: -// 'Override secrets with personal value', -// id: '', -// tags: [] -// }, -// { -// pos: 2, -// key: 'DB_PASSWORD', -// value: 'OVERRIDE_THIS', -// valueOverride: undefined, -// comment: -// 'Another secret override', -// id: '', -// tags: [] -// }, -// { -// pos: 3, -// key: 'DB_USERNAME', -// value: 'user1234', -// valueOverride: 'user1234', -// comment: '', -// id: '', -// tags: [] -// }, -// { -// pos: 4, -// key: 'DB_PASSWORD', -// value: 'example_password', -// valueOverride: 'example_password', -// comment: '', -// id: '', -// tags: [] -// }, -// { -// pos: 5, -// key: 'TWILIO_AUTH_TOKEN', -// value: 'example_twillio_token', -// valueOverride: undefined, -// comment: '', -// id: '', -// tags: [] -// }, -// { -// pos: 6, -// key: 'WEBSITE_URL', -// value: 'http://localhost:3000', -// valueOverride: undefined, -// comment: '', -// id: '', -// tags: [] -// } -// ]; -// const secrets = await encryptSecrets({ -// secretsToEncrypt: secretsToBeAdded, -// workspaceId: String(localStorage.getItem('projectData.id')), -// env: 'dev' -// }); -// await addSecrets({ -// secrets: secrets ?? [], -// env: 'dev', -// workspaceId: String(localStorage.getItem('projectData.id')) -// }); - // } \ No newline at end of file +export default attemptLogin; \ No newline at end of file diff --git a/frontend/src/components/utilities/attemptLoginMfa.ts b/frontend/src/components/utilities/attemptLoginMfa.ts index 318c33168..8bec76b86 100644 --- a/frontend/src/components/utilities/attemptLoginMfa.ts +++ b/frontend/src/components/utilities/attemptLoginMfa.ts @@ -3,6 +3,8 @@ import jsrp from 'jsrp'; import login1 from '@app/pages/api/auth/Login1'; import verifyMfaToken from '@app/pages/api/auth/verifyMfaToken'; +import getOrganizations from '@app/pages/api/organization/getOrgs'; +import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects'; import KeyService from '@app/services/KeyService'; import { saveTokenToLocalStorage } from './saveTokenToLocalStorage'; @@ -51,7 +53,8 @@ const attemptLoginMfa = async ({ mfaToken }); - // set JWT token + // unset temporary (MFA) JWT token and set JWT token + SecurityClient.setMfaToken(''); SecurityClient.setToken(token); const privateKey = await KeyService.decryptPrivateKey({ @@ -77,6 +80,18 @@ const attemptLoginMfa = async ({ privateKey }); + // TODO: in the future - move this logic elsewhere + // because this function is about logging the user in + // and not initializing the login details + const userOrgs = await getOrganizations(); + const orgId = userOrgs[0]._id; + localStorage.setItem('orgData.id', orgId); + + const orgUserProjects = await getOrganizationUserProjects({ + orgId + }); + localStorage.setItem('projectData.id', orgUserProjects[0]._id); + resolve(true); } catch (err) { reject(err); diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index 7234da684..ee44dc147 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -1,6 +1,9 @@ import axios from 'axios'; -import { getAuthToken } from '@app/reactQuery'; +import { + getAuthToken, + getMfaTempToken, + getSignupTempToken} from '@app/reactQuery'; export const apiRequest = axios.create({ baseURL: '/', @@ -10,11 +13,17 @@ export const apiRequest = axios.create({ }); apiRequest.interceptors.request.use((config) => { + const signupTempToken = getSignupTempToken(); + const mfaTempToken = getMfaTempToken(); const token = getAuthToken(); - console.log('interceptors'); - console.log('token', token); - console.log('config.headers', config.headers); - if (token && config.headers) { + + if (signupTempToken && config.headers) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${signupTempToken}`; + } else if (mfaTempToken && config.headers) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${mfaTempToken}`; + } else if (token && config.headers) { // eslint-disable-next-line no-param-reassign config.headers.Authorization = `Bearer ${token}`; } diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index 5b7837303..de14c3bf4 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -1 +1,4 @@ -export { useGetAuthToken } from './queries'; +export { + useGetAuthToken, + useSendMfaToken, + useVerifyMfaToken} from './queries' diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index ceae44ed8..23e0c50fc 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -1,14 +1,39 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { apiRequest } from '@app/config/request'; import { setAuthToken } from '@app/reactQuery'; -import { GetAuthTokenAPI } from './types'; +import { + GetAuthTokenAPI, + SendMfaTokenDTO, + VerifyMfaTokenDTO, + VerifyMfaTokenRes} from './types'; const authKeys = { getAuthToken: ['token'] as const }; +export const useSendMfaToken = () => { + return useMutation<{}, {}, SendMfaTokenDTO>({ + mutationFn: async ({ email }) => { + const { data } = await apiRequest.post('/api/v2/auth/mfa/send', { email }); + return data; + } + }); +} + +export const useVerifyMfaToken = () => { + return useMutation({ + mutationFn: async ({ email, mfaCode }) => { + const { data } = await apiRequest.post('/api/v2/auth/mfa/verify', { + email, + mfaToken: mfaCode + }); + return data; + } + }); +} + // Refresh token is set as cookie when logged in // Using that we fetch the auth bearer token needed for auth calls const fetchAuthToken = async () => { diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index 998df8a5d..3d14c19ff 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -1,3 +1,24 @@ export type GetAuthTokenAPI = { token: string; }; + +export type SendMfaTokenDTO = { + email: string; +} + +export type VerifyMfaTokenDTO = { + email: string; + mfaCode: string; +} + +export type VerifyMfaTokenRes = { + encryptionVersion: number; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey: string; + encryptedPrivateKey: string; + iv: string; + tag: string; +} \ No newline at end of file diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 1a2dc2cfa..032d1f3a5 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -93,6 +93,7 @@ export const AppLayout = ({ children }: LayoutProps) => { // Placing the localstorage as much as possible // Wait till tony integrates the azure and its launched useEffect(() => { + // Put a user in a workspace if they're not in one yet const putUserInWorkSpace = async () => { if (tempLocalStorage('orgData.id') === '') { diff --git a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx index c6762f5bd..8ceb24098 100644 --- a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx +++ b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx @@ -75,8 +75,18 @@ export const Navbar = () => { const closeApp = async () => { try { - console.log('Logging out...'); + console.log('Logging out...') await logout.mutateAsync(); + localStorage.removeItem('protectedKey'); + localStorage.removeItem('protectedKeyIV'); + localStorage.removeItem('protectedKeyTag'); + localStorage.removeItem('publicKey'); + localStorage.removeItem('encryptedPrivateKey'); + localStorage.removeItem('iv'); + localStorage.removeItem('tag'); + localStorage.removeItem('PRIVATE_KEY'); + localStorage.removeItem('orgData.id'); + localStorage.removeItem('projectData.id'); router.push('/login'); } catch (error) { console.error(error); diff --git a/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts b/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts index a76307569..946505887 100644 --- a/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts +++ b/frontend/src/pages/api/auth/CompleteAccountInformationSignup.ts @@ -1,4 +1,5 @@ -import SecurityClient from '@app/components/utilities/SecurityClient'; + +import { apiRequest } from "@app/config/request"; interface Props { email: string; @@ -35,7 +36,7 @@ interface Props { * @param {string} obj.verifier * @returns */ -const completeAccountInformationSignup = ({ +const completeAccountInformationSignup = async ({ email, firstName, lastName, @@ -49,32 +50,24 @@ const completeAccountInformationSignup = ({ salt, verifier, organizationName -}: Props) => SecurityClient.fetchCall('/api/v2/signup/complete-account/signup', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - organizationName - }) - }).then(async (res) => { - if (res && res?.status === 200) { - return res.json(); - } - console.log('Failed to verify MFA code'); - throw new Error('Something went wrong during MFA code verification'); +}: Props) => { + const { data } = await apiRequest.post('/api/v2/signup/complete-account/signup', { + email, + firstName, + lastName, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier, + organizationName }); + return data; +} + export default completeAccountInformationSignup; diff --git a/frontend/src/pages/api/auth/CompleteAccountInformationSignupInvite.ts b/frontend/src/pages/api/auth/CompleteAccountInformationSignupInvite.ts index 62a5b503f..2307bbb74 100644 --- a/frontend/src/pages/api/auth/CompleteAccountInformationSignupInvite.ts +++ b/frontend/src/pages/api/auth/CompleteAccountInformationSignupInvite.ts @@ -1,3 +1,5 @@ +import { apiRequest } from "@app/config/request"; + interface Props { email: string; firstName: string; @@ -11,9 +13,12 @@ interface Props { encryptedPrivateKeyTag: string; salt: string; verifier: string; - token: string; } +// missing token? +// TODO: add to SecurityClient + + /** * This function is called in the end of the signup process. * It sends all the necessary nformation to the server. @@ -30,7 +35,7 @@ interface Props { * @param {string} obj.token - token that confirms a user's identity * @returns */ -const completeAccountInformationSignupInvite = ({ +const completeAccountInformationSignupInvite = async ({ email, firstName, lastName, @@ -42,28 +47,24 @@ const completeAccountInformationSignupInvite = ({ encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt, - verifier, - token -}: Props) => fetch('/api/v2/signup/complete-account/invite', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - body: JSON.stringify({ - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - }) + verifier +}: Props) => { + const { data } = await apiRequest.post('/api/v2/signup/complete-account/invite', { + email, + firstName, + lastName, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier }); + return data; +} + export default completeAccountInformationSignupInvite; diff --git a/frontend/src/pages/api/auth/Logout.ts b/frontend/src/pages/api/auth/Logout.ts index 71f81b1bd..d9f50fee2 100644 --- a/frontend/src/pages/api/auth/Logout.ts +++ b/frontend/src/pages/api/auth/Logout.ts @@ -4,14 +4,16 @@ import SecurityClient from '@app/components/utilities/SecurityClient'; * This route logs the user out. Note: the user should authorized to do this. * We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry */ -const logout = async () => - SecurityClient.fetchCall('/api/v1/auth/logout', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'include' - }).then((res) => { +const logout = async () => { + try { + const res = await SecurityClient.fetchCall('/api/v1/auth/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include' + }); + if (res?.status === 200) { SecurityClient.setToken(''); // Delete the cookie by not setting a value; Alternatively clear the local storage @@ -23,12 +25,17 @@ const logout = async () => localStorage.removeItem('iv'); localStorage.removeItem('tag'); localStorage.removeItem('PRIVATE_KEY'); + localStorage.removeItem('orgData.id'); + localStorage.removeItem('projectData.id'); - console.log('User logged out', res); return res; } - console.log('Failed to log out'); - return undefined; - }); + + } catch (error) { + console.log('Error logging out', error); + } + + return undefined; +}; export default logout; diff --git a/frontend/src/pages/api/auth/resendMfaToken.ts b/frontend/src/pages/api/auth/resendMfaToken.ts deleted file mode 100644 index 40f567f90..000000000 --- a/frontend/src/pages/api/auth/resendMfaToken.ts +++ /dev/null @@ -1,29 +0,0 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; - -/** - * Send new MFA token to user with email [email] - * @param {object} obj - * @param {string} obj.email - email of user - * @returns - */ -const resendMfaToken = async ({ - email, -}: { - email: string; -}) => SecurityClient.fetchCall('/api/v2/auth/mfa/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email - }) - }).then(async (res) => { - if (res && res?.status === 200) { - return res.json(); - } - console.log('Failed to send new MFA code'); - throw new Error('Something went wrong while sending new MFA code'); - }); - -export default resendMfaToken; diff --git a/frontend/src/pages/api/auth/verifyMfaToken.ts b/frontend/src/pages/api/auth/verifyMfaToken.ts index 8df09a826..7fdd92183 100644 --- a/frontend/src/pages/api/auth/verifyMfaToken.ts +++ b/frontend/src/pages/api/auth/verifyMfaToken.ts @@ -1,4 +1,4 @@ -import SecurityClient from "@app/components/utilities/SecurityClient"; +import { apiRequest } from "@app/config/request"; /** * Verify MFA token [mfaToken] for user with email [email] @@ -13,23 +13,13 @@ const verifyMfaToken = async ({ }: { email: string; mfaToken: string; -}) => SecurityClient.fetchCall('/api/v2/auth/mfa/verify', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email, - mfaToken - }) - }).then(async (res) => { - if (res && res?.status === 200) { - return res.json(); - } - console.log('Failed to verify MFA code'); - throw new Error('Something went wrong during MFA code verification'); - }); - - +}) => { + const { data } = await apiRequest.post('/api/v2/auth/mfa/verify', { + email, + mfaToken + }); + + return data; +} export default verifyMfaToken; diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index f08bd325b..1b380bfda 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -17,7 +17,6 @@ export default function Login() { const router = useRouter(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false); const [step, setStep] = useState(1); const { t } = useTranslation(); const lang = router.locale ?? 'en'; @@ -41,7 +40,6 @@ export default function Login() { } }; if (isLoggedIn()) { - setIsAlreadyLoggedIn(true); redirectToDashboard(); } }, []); @@ -72,10 +70,6 @@ export default function Login() { } } - if (isAlreadyLoggedIn) { - return null - } - return (
@@ -106,4 +100,4 @@ export default function Login() { ); } -export const getStaticProps = getTranslatedStaticProps(['auth', 'login']); +export const getStaticProps = getTranslatedStaticProps(['auth', 'login', 'signup']); diff --git a/frontend/src/pages/signup.tsx b/frontend/src/pages/signup.tsx index dbd610979..62d0f4a47 100644 --- a/frontend/src/pages/signup.tsx +++ b/frontend/src/pages/signup.tsx @@ -59,7 +59,8 @@ export default function SignUp() { // Checking if the code matches the email. const response = await checkEmailVerificationCode({ email, code }); if (response.status === 200) { - SecurityClient.setToken((await response.json()).token); + const {token} = await response.json(); + SecurityClient.setSignupToken(token); setStep(3); } else { setCodeError(true); diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 425b9cf6c..0e5db027a 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -21,6 +21,10 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck'; import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm'; import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto'; import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey'; +import { saveTokenToLocalStorage } from '@app/components/utilities/saveTokenToLocalStorage'; +import SecurityClient from '@app/components/utilities/SecurityClient'; +import getOrganizations from '@app/pages/api/organization/getOrgs'; +import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects'; import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite'; import verifySignupInvite from './api/auth/VerifySignupInvite'; @@ -29,7 +33,6 @@ import verifySignupInvite from './api/auth/VerifySignupInvite'; const client = new jsrp.client(); export default function SignupInvite() { - console.log('SignupInvite'); const [password, setPassword] = useState(''); const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); @@ -82,13 +85,6 @@ export default function SignupInvite() { const privateKey = encodeBase64(secretKeyUint8Array); const publicKey = encodeBase64(publicKeyUint8Array); - // const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ - // text: PRIVATE_KEY, - // secret: password - // .slice(0, 32) - // .padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0') - // }); - localStorage.setItem('PRIVATE_KEY', privateKey); client.init( @@ -134,8 +130,9 @@ export default function SignupInvite() { secret: Buffer.from(derivedKey.hash) }); - console.log('SignupInvite A'); - let response = await completeAccountInformationSignupInvite({ + const { + token: jwtToken + } = await completeAccountInformationSignupInvite({ email, firstName, lastName, @@ -147,24 +144,30 @@ export default function SignupInvite() { encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt: result.salt, - verifier: result.verifier, - token: verificationToken + verifier: result.verifier }); - console.log('SignupInvite B'); + + // unset temporary signup JWT token and set JWT token + SecurityClient.setSignupToken(''); + SecurityClient.setToken(jwtToken); - // if everything works, go the main dashboard page. - if (!errorCheck && response.status === 200) { - response = await response.json(); + saveTokenToLocalStorage({ + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + privateKey + }); - console.log('SignupInvite C'); - localStorage.setItem('publicKey', publicKey); - localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey); - localStorage.setItem('iv', encryptedPrivateKeyIV); - localStorage.setItem('tag', encryptedPrivateKeyTag); - console.log('SignupInvite D'); + const userOrgs = await getOrganizations(); - setStep(3); - } + const orgId = userOrgs[0]._id; + localStorage.setItem('orgData.id', orgId); + + setStep(3); } catch (error) { setIsLoading(false); console.error(error); @@ -197,7 +200,7 @@ export default function SignupInvite() { // user will have temp token if doesn't have an account // then continue with account setup workflow if (res?.token) { - setVerificationToken(res.token); + SecurityClient.setSignupToken(res.token); setStep(2); } else { // user will be redirected to dashboard diff --git a/frontend/src/reactQuery.ts b/frontend/src/reactQuery.ts index 7bb83b111..d65baa27f 100644 --- a/frontend/src/reactQuery.ts +++ b/frontend/src/reactQuery.ts @@ -1,6 +1,7 @@ import { QueryClient } from '@tanstack/react-query'; // this is saved in react-query cache +export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ['infisical__signup-temp-token']; export const MFA_TEMP_TOKEN_CACHE_KEY = ['infisical__mfa-temp-token']; export const AUTH_TOKEN_CACHE_KEY = ['infisical__auth-token']; @@ -14,12 +15,16 @@ export const queryClient = new QueryClient({ }); // set token in memory cache +export const setSignupTempToken = (token: string) => + queryClient.setQueryData(SIGNUP_TEMP_TOKEN_CACHE_KEY, token); + export const setMfaTempToken = (token: string) => queryClient.setQueryData(MFA_TEMP_TOKEN_CACHE_KEY, token); export const setAuthToken = (token: string) => queryClient.setQueryData(AUTH_TOKEN_CACHE_KEY, token); +export const getSignupTempToken = () => queryClient.getQueryData(SIGNUP_TEMP_TOKEN_CACHE_KEY) as string; export const getMfaTempToken = () => queryClient.getQueryData(MFA_TEMP_TOKEN_CACHE_KEY) as string; export const getAuthToken = () => queryClient.getQueryData(AUTH_TOKEN_CACHE_KEY) as string;