Refactoring functions into services, helper functions, hooks, patch bugs

This commit is contained in:
Tuan Dang
2023-02-14 17:38:58 +07:00
parent 13b1805d04
commit e1ad8fbee8
30 changed files with 383 additions and 360 deletions
+28 -11
View File
@@ -269,19 +269,36 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
secure: NODE_ENV === 'production' ? true : false 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 // case: user does not have MFA enabled
// return (access) token in response // return (access) token in response
return res.status(200).send({ return res.status(200).send(resObj);
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
});
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
+20 -7
View File
@@ -7,6 +7,7 @@ import {
} from '../../helpers/signup'; } from '../../helpers/signup';
import { issueAuthTokens } from '../../helpers/auth'; import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables'; import { INVITED, ACCEPTED } from '../../variables';
import { NODE_ENV } from '../../config';
import axios from 'axios'; import axios from 'axios';
/** /**
@@ -105,7 +106,6 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
}); });
token = tokens.token; token = tokens.token;
refreshToken = tokens.refreshToken;
// sending a welcome email to new users // sending a welcome email to new users
if (process.env.LOOPS_API_KEY) { 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) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -132,8 +140,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: 'Successfully set up account',
user, user,
token, token
refreshToken
}); });
}; };
@@ -219,7 +226,14 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
}); });
token = tokens.token; 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) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -227,11 +241,10 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
message: 'Failed to complete account setup' message: 'Failed to complete account setup'
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: 'Successfully set up account',
user, user,
token, token
refreshToken
}); });
}; };
+56 -39
View File
@@ -12,7 +12,7 @@ import {
import { import {
SALT_ROUNDS SALT_ROUNDS
} from '../config'; } from '../config';
import { UnauthorizedRequestError } from '../utils/errors'; import { ForbiddenRequestError } from '../utils/errors';
/** /**
* Create and store a token in the database for purpose [type] * Create and store a token in the database for purpose [type]
@@ -65,29 +65,45 @@ const createTokenHelper = async ({
break; break;
} }
interface Query { interface TokenDataQuery {
type: string; type: string;
email?: string; email?: string;
phoneNumber?: string; phoneNumber?: string;
organization?: Types.ObjectId; 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 (email) {
if (phoneNumber) { query.phoneNumber = phoneNumber; } query.email = email;
if (organizationId) { query.organization = organizationId } update.email = email;
}
if (phoneNumber) {
query.phoneNumber = phoneNumber;
update.phoneNumber = phoneNumber;
}
if (organizationId) {
query.organization = organizationId
update.organization = organizationId
}
await TokenData.findOneAndUpdate( await TokenData.findOneAndUpdate(
query, query,
{ update,
type,
email,
phoneNumber,
organization: organizationId,
tokenHash: await bcrypt.hash(token, SALT_ROUNDS),
expiresAt
},
{ {
new: true, new: true,
upsert: true upsert: true
@@ -123,37 +139,38 @@ const validateTokenHelper = async ({
organizationId?: Types.ObjectId; organizationId?: Types.ObjectId;
token: string; token: string;
}) => { }) => {
try { interface Query {
interface Query { type: string;
type: string; email?: string;
email?: string; phoneNumber?: string;
phoneNumber?: string; organization?: Types.ObjectId;
organization?: Types.ObjectId; }
}
const query: Query = { type }; const query: Query = { type };
if (email) { query.email = email; } if (email) { query.email = email; }
if (phoneNumber) { query.phoneNumber = phoneNumber; } if (phoneNumber) { query.phoneNumber = phoneNumber; }
if (organizationId) { query.organization = organizationId; } if (organizationId) { query.organization = organizationId; }
const tokenData = await TokenData.findOneAndDelete(query); const tokenData = await TokenData.findOne(query).select('+tokenHash');
if (!tokenData) throw new Error('Failed to find token to validate'); if (!tokenData) throw new Error('Failed to find token to validate');
if (tokenData.expiresAt < new Date()) throw new Error('Token has expired'); if (tokenData.expiresAt < new Date()) {
await TokenData.findByIdAndDelete(tokenData._id);
const isValid = await bcrypt.compare(token, tokenData.tokenHash); throw ForbiddenRequestError({
if (!isValid) throw UnauthorizedRequestError({ 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' 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 { export {
@@ -1,11 +1,12 @@
import { ErrorRequestHandler } from "express"; import { ErrorRequestHandler } from "express";
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { InternalServerError } from "../utils/errors"; import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { getLogger } from "../utils/logger"; import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError"; import RequestError, { LogLevel } from "../utils/requestError";
import { NODE_ENV } from "../config"; import { NODE_ENV } from "../config";
import { TokenExpiredError } from 'jsonwebtoken';
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
if (res.headersSent) return 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 //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 }) error = InternalServerError({ context: { exception: error.message }, stack: error.stack })
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message) getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
} }
+33 -1
View File
@@ -1,5 +1,37 @@
--- ---
title: "Circle CI" 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)
<Info>
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.
</Info>
## 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)
+2 -2
View File
@@ -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 Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | 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 | | [React](/integrations/frameworks/react) | Framework | Available |
| [Vue](/integrations/frameworks/vue) | Framework | Available | | [Vue](/integrations/frameworks/vue) | Framework | Available |
| [Express](/integrations/frameworks/express) | 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 | | GCP | Cloud | Coming soon |
| Azure | Cloud | Coming soon | | Azure | Cloud | Coming soon |
| DigitalOcean | 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 | | TravisCI | CI/CD | Coming soon |
| GitHub Actions | CI/CD | Coming soon | | GitHub Actions | CI/CD | Coming soon |
| Jenkins | CI/CD | Coming soon | | Jenkins | CI/CD | Coming soon |
+2 -2
View File
@@ -9,9 +9,9 @@
"step1-start": "Let's get started", "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-privacy": "By creating an account, you agree to our Terms and have read and acknowledged the Privacy Policy.",
"step1-submit": "Get Started", "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-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-submit": "Resend",
"step2-resend-progress": "Resending...", "step2-resend-progress": "Resending...",
"step2-spam-alert": "Make sure to check your spam inbox.", "step2-spam-alert": "Make sure to check your spam inbox.",
+1 -1
View File
@@ -11,7 +11,7 @@
"step1-submit": "C'est parti", "step1-submit": "C'est parti",
"step2-message": "Nous avons envoyé un email de vérification à", "step2-message": "Nous avons envoyé un email de vérification à",
"step2-code-error": "Oops. Votre code est faux. Veuillez réessayer.", "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-submit": "Renvoyer",
"step2-resend-progress": "Envoie en cours...", "step2-resend-progress": "Envoie en cours...",
"step2-spam-alert": "Assurez-vous de vérifier vos spams.", "step2-spam-alert": "Assurez-vous de vérifier vos spams.",
+1 -1
View File
@@ -11,7 +11,7 @@
"step1-submit": "Başla", "step1-submit": "Başla",
"step2-message": "Şu adrese bir doğrulama maili yolladık", "step2-message": "Şu adrese bir doğrulama maili yolladık",
"step2-code-error": "Tüh. Kodun hatalı. Lütfen tekrar dene.", "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-submit": "Tekrar Yolla",
"step2-resend-progress": "Tekrar yollanıyor...", "step2-resend-progress": "Tekrar yollanıyor...",
"step2-spam-alert": "Spam kutunuzu kontrol ettiğinizden emin olun.", "step2-spam-alert": "Spam kutunuzu kontrol ettiğinizden emin olun.",
+5 -4
View File
@@ -5,7 +5,7 @@ import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa'; 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 Button from '../basic/buttons/Button';
import Error from '../basic/Error'; import Error from '../basic/Error';
@@ -50,6 +50,8 @@ export default function MFAStep({
const [mfaCode, setMfaCode] = useState(''); const [mfaCode, setMfaCode] = useState('');
const [codeError, setCodeError] = useState(false); const [codeError, setCodeError] = useState(false);
const sendMfaToken = useSendMfaToken();
const { t } = useTranslation(); const { t } = useTranslation();
const handleLoginMfa = async () => { const handleLoginMfa = async () => {
@@ -72,15 +74,14 @@ export default function MFAStep({
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setIsLoading(false);
setCodeError(true); setCodeError(true);
} }
} }
const handleResendMfaCode = async () => { const handleResendMfaCode = async () => {
try { try {
await resendMfaToken({ await sendMfaToken.mutateAsync({ email });
email
});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@@ -87,7 +87,6 @@ export default function Navbar() {
}, []); }, []);
const closeApp = async () => { const closeApp = async () => {
console.log('Logging out...');
await logout(); await logout();
router.push('/login'); router.push('/login');
}; };
@@ -161,6 +161,8 @@ export default function UserInfoStep({
organizationName: `${firstName}'s organization` organizationName: `${firstName}'s organization`
}); });
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken('');
SecurityClient.setToken(response.token); SecurityClient.setToken(response.token);
saveTokenToLocalStorage({ saveTokenToLocalStorage({
@@ -174,14 +176,18 @@ export default function UserInfoStep({
privateKey privateKey
}); });
incrementStep();
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
await ProjectService.initProject({ const orgId = userOrgs[0]?._id;
organizationId: userOrgs[0]?._id, const project = await ProjectService.initProject({
organizationId: orgId,
projectName: 'Example Project' projectName: 'Example Project'
}); });
localStorage.setItem('orgData.id', orgId);
localStorage.setItem('projectData.id', project._id);
incrementStep();
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);
console.error(error); console.error(error);
@@ -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 // depreciated: go for apiRequest module in config/api
export default class SecurityClient { export default class SecurityClient {
static setSignupToken(tokenStr: string) {
setSignupTempToken(tokenStr);
}
static setMfaToken(tokenStr: string) { static setMfaToken(tokenStr: string) {
setMfaTempToken(tokenStr); setMfaTempToken(tokenStr);
} }
@@ -44,7 +44,7 @@ const attemptLogin = async (
client.setSalt(salt); client.setSalt(salt);
client.setServerPublicKey(serverPublicKey); client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1 const clientProof = client.getProof(); // called M1
const { const {
mfaEnabled, mfaEnabled,
encryptionVersion, encryptionVersion,
@@ -65,7 +65,7 @@ const attemptLogin = async (
// case: MFA is enabled // case: MFA is enabled
// set temporary (MFA) JWT token // set temporary (MFA) JWT token
SecurityClient.setToken(token); SecurityClient.setMfaToken(token);
resolve({ resolve({
mfaEnabled, mfaEnabled,
@@ -113,40 +113,14 @@ const attemptLogin = async (
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id; const orgId = userOrgs[0]._id;
localStorage.setItem('orgData.id', orgId); localStorage.setItem('orgData.id', orgId);
const orgUserProjects = await getOrganizationUserProjects({ const orgUserProjects = await getOrganizationUserProjects({
orgId orgId
}); });
localStorage.setItem('projectData.id', orgUserProjects[0]._id);
// // TODO: this part definitely needs to be refactored if (orgUserProjects.length > 0) {
// const userOrgs = await getOrganizations(); localStorage.setItem('projectData.id', orgUserProjects[0]._id);
// 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 (email) { if (email) {
telemetry.identify(email); telemetry.identify(email);
@@ -166,100 +140,4 @@ const attemptLogin = async (
}); });
}; };
export default attemptLogin; 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'))
// });
// }
@@ -3,6 +3,8 @@ import jsrp from 'jsrp';
import login1 from '@app/pages/api/auth/Login1'; import login1 from '@app/pages/api/auth/Login1';
import verifyMfaToken from '@app/pages/api/auth/verifyMfaToken'; 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 KeyService from '@app/services/KeyService';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage'; import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
@@ -51,7 +53,8 @@ const attemptLoginMfa = async ({
mfaToken mfaToken
}); });
// set JWT token // unset temporary (MFA) JWT token and set JWT token
SecurityClient.setMfaToken('');
SecurityClient.setToken(token); SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({ const privateKey = await KeyService.decryptPrivateKey({
@@ -77,6 +80,18 @@ const attemptLoginMfa = async ({
privateKey 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); resolve(true);
} catch (err) { } catch (err) {
reject(err); reject(err);
+14 -5
View File
@@ -1,6 +1,9 @@
import axios from 'axios'; import axios from 'axios';
import { getAuthToken } from '@app/reactQuery'; import {
getAuthToken,
getMfaTempToken,
getSignupTempToken} from '@app/reactQuery';
export const apiRequest = axios.create({ export const apiRequest = axios.create({
baseURL: '/', baseURL: '/',
@@ -10,11 +13,17 @@ export const apiRequest = axios.create({
}); });
apiRequest.interceptors.request.use((config) => { apiRequest.interceptors.request.use((config) => {
const signupTempToken = getSignupTempToken();
const mfaTempToken = getMfaTempToken();
const token = getAuthToken(); const token = getAuthToken();
console.log('interceptors');
console.log('token', token); if (signupTempToken && config.headers) {
console.log('config.headers', config.headers); // eslint-disable-next-line no-param-reassign
if (token && config.headers) { 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 // eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
+4 -1
View File
@@ -1 +1,4 @@
export { useGetAuthToken } from './queries'; export {
useGetAuthToken,
useSendMfaToken,
useVerifyMfaToken} from './queries'
+27 -2
View File
@@ -1,14 +1,39 @@
import { useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request'; import { apiRequest } from '@app/config/request';
import { setAuthToken } from '@app/reactQuery'; import { setAuthToken } from '@app/reactQuery';
import { GetAuthTokenAPI } from './types'; import {
GetAuthTokenAPI,
SendMfaTokenDTO,
VerifyMfaTokenDTO,
VerifyMfaTokenRes} from './types';
const authKeys = { const authKeys = {
getAuthToken: ['token'] as const 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<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
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 // Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls // Using that we fetch the auth bearer token needed for auth calls
const fetchAuthToken = async () => { const fetchAuthToken = async () => {
+21
View File
@@ -1,3 +1,24 @@
export type GetAuthTokenAPI = { export type GetAuthTokenAPI = {
token: string; 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;
}
@@ -93,6 +93,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// Placing the localstorage as much as possible // Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched // Wait till tony integrates the azure and its launched
useEffect(() => { useEffect(() => {
// Put a user in a workspace if they're not in one yet // Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => { const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') { if (tempLocalStorage('orgData.id') === '') {
@@ -75,8 +75,18 @@ export const Navbar = () => {
const closeApp = async () => { const closeApp = async () => {
try { try {
console.log('Logging out...'); console.log('Logging out...')
await logout.mutateAsync(); 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'); router.push('/login');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -1,4 +1,5 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
import { apiRequest } from "@app/config/request";
interface Props { interface Props {
email: string; email: string;
@@ -35,7 +36,7 @@ interface Props {
* @param {string} obj.verifier * @param {string} obj.verifier
* @returns * @returns
*/ */
const completeAccountInformationSignup = ({ const completeAccountInformationSignup = async ({
email, email,
firstName, firstName,
lastName, lastName,
@@ -49,32 +50,24 @@ const completeAccountInformationSignup = ({
salt, salt,
verifier, verifier,
organizationName organizationName
}: Props) => SecurityClient.fetchCall('/api/v2/signup/complete-account/signup', { }: Props) => {
method: 'POST', const { data } = await apiRequest.post('/api/v2/signup/complete-account/signup', {
headers: { email,
'Content-Type': 'application/json' firstName,
}, lastName,
body: JSON.stringify({ protectedKey,
email, protectedKeyIV,
firstName, protectedKeyTag,
lastName, publicKey,
protectedKey, encryptedPrivateKey,
protectedKeyIV, encryptedPrivateKeyIV,
protectedKeyTag, encryptedPrivateKeyTag,
publicKey, salt,
encryptedPrivateKey, verifier,
encryptedPrivateKeyIV, organizationName
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');
}); });
return data;
}
export default completeAccountInformationSignup; export default completeAccountInformationSignup;
@@ -1,3 +1,5 @@
import { apiRequest } from "@app/config/request";
interface Props { interface Props {
email: string; email: string;
firstName: string; firstName: string;
@@ -11,9 +13,12 @@ interface Props {
encryptedPrivateKeyTag: string; encryptedPrivateKeyTag: string;
salt: string; salt: string;
verifier: string; verifier: string;
token: string;
} }
// missing token?
// TODO: add to SecurityClient
/** /**
* This function is called in the end of the signup process. * This function is called in the end of the signup process.
* It sends all the necessary nformation to the server. * 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 * @param {string} obj.token - token that confirms a user's identity
* @returns * @returns
*/ */
const completeAccountInformationSignupInvite = ({ const completeAccountInformationSignupInvite = async ({
email, email,
firstName, firstName,
lastName, lastName,
@@ -42,28 +47,24 @@ const completeAccountInformationSignupInvite = ({
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier, verifier
token }: Props) => {
}: Props) => fetch('/api/v2/signup/complete-account/invite', { const { data } = await apiRequest.post('/api/v2/signup/complete-account/invite', {
method: 'POST', email,
headers: { firstName,
'Content-Type': 'application/json', lastName,
Authorization: `Bearer ${token}` protectedKey,
}, protectedKeyIV,
body: JSON.stringify({ protectedKeyTag,
email, publicKey,
firstName, encryptedPrivateKey,
lastName, encryptedPrivateKeyIV,
protectedKey, encryptedPrivateKeyTag,
protectedKeyIV, salt,
protectedKeyTag, verifier
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
}); });
return data;
}
export default completeAccountInformationSignupInvite; export default completeAccountInformationSignupInvite;
+19 -12
View File
@@ -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. * 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 * We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry
*/ */
const logout = async () => const logout = async () => {
SecurityClient.fetchCall('/api/v1/auth/logout', { try {
method: 'POST', const res = await SecurityClient.fetchCall('/api/v1/auth/logout', {
headers: { method: 'POST',
'Content-Type': 'application/json' headers: {
}, 'Content-Type': 'application/json'
credentials: 'include' },
}).then((res) => { credentials: 'include'
});
if (res?.status === 200) { if (res?.status === 200) {
SecurityClient.setToken(''); SecurityClient.setToken('');
// Delete the cookie by not setting a value; Alternatively clear the local storage // 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('iv');
localStorage.removeItem('tag'); localStorage.removeItem('tag');
localStorage.removeItem('PRIVATE_KEY'); localStorage.removeItem('PRIVATE_KEY');
localStorage.removeItem('orgData.id');
localStorage.removeItem('projectData.id');
console.log('User logged out', res);
return res; return res;
} }
console.log('Failed to log out');
return undefined; } catch (error) {
}); console.log('Error logging out', error);
}
return undefined;
};
export default logout; export default logout;
@@ -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;
+9 -19
View File
@@ -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] * Verify MFA token [mfaToken] for user with email [email]
@@ -13,23 +13,13 @@ const verifyMfaToken = async ({
}: { }: {
email: string; email: string;
mfaToken: string; mfaToken: string;
}) => SecurityClient.fetchCall('/api/v2/auth/mfa/verify', { }) => {
method: 'POST', const { data } = await apiRequest.post('/api/v2/auth/mfa/verify', {
headers: { email,
'Content-Type': 'application/json' mfaToken
}, });
body: JSON.stringify({
email, return data;
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');
});
export default verifyMfaToken; export default verifyMfaToken;
+1 -7
View File
@@ -17,7 +17,6 @@ export default function Login() {
const router = useRouter(); const router = useRouter();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const { t } = useTranslation(); const { t } = useTranslation();
const lang = router.locale ?? 'en'; const lang = router.locale ?? 'en';
@@ -41,7 +40,6 @@ export default function Login() {
} }
}; };
if (isLoggedIn()) { if (isLoggedIn()) {
setIsAlreadyLoggedIn(true);
redirectToDashboard(); redirectToDashboard();
} }
}, []); }, []);
@@ -72,10 +70,6 @@ export default function Login() {
} }
} }
if (isAlreadyLoggedIn) {
return null
}
return ( return (
<div className="bg-bunker-800 h-screen flex flex-col justify-start px-6"> <div className="bg-bunker-800 h-screen flex flex-col justify-start px-6">
<Head> <Head>
@@ -106,4 +100,4 @@ export default function Login() {
); );
} }
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']); export const getStaticProps = getTranslatedStaticProps(['auth', 'login', 'signup']);
+2 -1
View File
@@ -59,7 +59,8 @@ export default function SignUp() {
// Checking if the code matches the email. // Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code }); const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200) { if (response.status === 200) {
SecurityClient.setToken((await response.json()).token); const {token} = await response.json();
SecurityClient.setSignupToken(token);
setStep(3); setStep(3);
} else { } else {
setCodeError(true); setCodeError(true);
+28 -25
View File
@@ -21,6 +21,10 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto'; import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey'; 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 completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
import verifySignupInvite from './api/auth/VerifySignupInvite'; import verifySignupInvite from './api/auth/VerifySignupInvite';
@@ -29,7 +33,6 @@ import verifySignupInvite from './api/auth/VerifySignupInvite';
const client = new jsrp.client(); const client = new jsrp.client();
export default function SignupInvite() { export default function SignupInvite() {
console.log('SignupInvite');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(''); const [lastName, setLastName] = useState('');
@@ -82,13 +85,6 @@ export default function SignupInvite() {
const privateKey = encodeBase64(secretKeyUint8Array); const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array); 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); localStorage.setItem('PRIVATE_KEY', privateKey);
client.init( client.init(
@@ -134,8 +130,9 @@ export default function SignupInvite() {
secret: Buffer.from(derivedKey.hash) secret: Buffer.from(derivedKey.hash)
}); });
console.log('SignupInvite A'); const {
let response = await completeAccountInformationSignupInvite({ token: jwtToken
} = await completeAccountInformationSignupInvite({
email, email,
firstName, firstName,
lastName, lastName,
@@ -147,24 +144,30 @@ export default function SignupInvite() {
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt: result.salt, salt: result.salt,
verifier: result.verifier, verifier: result.verifier
token: verificationToken
}); });
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. saveTokenToLocalStorage({
if (!errorCheck && response.status === 200) { protectedKey,
response = await response.json(); protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
console.log('SignupInvite C'); const userOrgs = await getOrganizations();
localStorage.setItem('publicKey', publicKey);
localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey);
localStorage.setItem('iv', encryptedPrivateKeyIV);
localStorage.setItem('tag', encryptedPrivateKeyTag);
console.log('SignupInvite D');
setStep(3); const orgId = userOrgs[0]._id;
} localStorage.setItem('orgData.id', orgId);
setStep(3);
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);
console.error(error); console.error(error);
@@ -197,7 +200,7 @@ export default function SignupInvite() {
// user will have temp token if doesn't have an account // user will have temp token if doesn't have an account
// then continue with account setup workflow // then continue with account setup workflow
if (res?.token) { if (res?.token) {
setVerificationToken(res.token); SecurityClient.setSignupToken(res.token);
setStep(2); setStep(2);
} else { } else {
// user will be redirected to dashboard // user will be redirected to dashboard
+5
View File
@@ -1,6 +1,7 @@
import { QueryClient } from '@tanstack/react-query'; import { QueryClient } from '@tanstack/react-query';
// this is saved in react-query cache // 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 MFA_TEMP_TOKEN_CACHE_KEY = ['infisical__mfa-temp-token'];
export const AUTH_TOKEN_CACHE_KEY = ['infisical__auth-token']; export const AUTH_TOKEN_CACHE_KEY = ['infisical__auth-token'];
@@ -14,12 +15,16 @@ export const queryClient = new QueryClient({
}); });
// set token in memory cache // set token in memory cache
export const setSignupTempToken = (token: string) =>
queryClient.setQueryData(SIGNUP_TEMP_TOKEN_CACHE_KEY, token);
export const setMfaTempToken = (token: string) => export const setMfaTempToken = (token: string) =>
queryClient.setQueryData(MFA_TEMP_TOKEN_CACHE_KEY, token); queryClient.setQueryData(MFA_TEMP_TOKEN_CACHE_KEY, token);
export const setAuthToken = (token: string) => export const setAuthToken = (token: string) =>
queryClient.setQueryData(AUTH_TOKEN_CACHE_KEY, token); 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 getMfaTempToken = () => queryClient.getQueryData(MFA_TEMP_TOKEN_CACHE_KEY) as string;
export const getAuthToken = () => queryClient.getQueryData(AUTH_TOKEN_CACHE_KEY) as string; export const getAuthToken = () => queryClient.getQueryData(AUTH_TOKEN_CACHE_KEY) as string;