Remove remaining SecurityClient auth calls in favor of hooks, keep RouteGuard

This commit is contained in:
Tuan Dang
2023-08-11 11:27:33 +07:00
parent 2dba7847b6
commit a4edf6bd0c
40 changed files with 443 additions and 910 deletions

View File

@@ -26,7 +26,7 @@ router.post(
keyController.uploadKey
);
router.get(
router.get( // TODO endpoint: deprecate (note: move frontend to v2/workspace/key or something)
"/:workspaceId/latest",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],

View File

@@ -9,7 +9,7 @@ import { AuthMode } from "../../variables";
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
// TODO endpoint: consider moving these endpoints to be under /workspace to be more RESTful
router.get( // used for old CLI (deprecate)
router.get( // TODO endpoint: deprecate - used for old CLI (deprecate)
"/:workspaceId/connect",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],

View File

@@ -3,7 +3,9 @@ import React, { useState } from "react";
import ReactCodeInput from "react-code-input";
import { useTranslation } from "react-i18next";
import sendVerificationEmail from "@app/pages/api/auth/SendVerificationEmail";
import {
useSendVerificationEmail
} from "@app/hooks/api";
import Error from "../basic/Error";
import { Button } from "../v2";
@@ -70,6 +72,7 @@ export default function CodeInputStep({
codeError,
isCodeInputCheckLoading
}: CodeInputStepProps): JSX.Element {
const { mutateAsync } = useSendVerificationEmail();
const [isLoading, setIsLoading] = useState(false);
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
const { t } = useTranslation();
@@ -77,7 +80,7 @@ export default function CodeInputStep({
const resendVerificationEmail = async () => {
setIsResendingVerificationEmail(true);
setIsLoading(true);
sendVerificationEmail(email);
await mutateAsync({ email });
setTimeout(() => {
setIsLoading(false);
setIsResendingVerificationEmail(false);

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import sendVerificationEmail from "@app/pages/api/auth/SendVerificationEmail";
import { useSendVerificationEmail } from "@app/hooks/api";
import { Button, Input } from "../v2";
@@ -25,13 +25,14 @@ export default function EnterEmailStep({
setEmail,
incrementStep
}: DownloadBackupPDFStepProps): JSX.Element {
const { mutateAsync } = useSendVerificationEmail();
const [emailError, setEmailError] = useState(false);
const { t } = useTranslation();
/**
* Verifies if the entered email "looks" correct
*/
const emailCheck = () => {
const emailCheck = async () => {
let emailCheckBool = false;
if (!email) {
setEmailError(true);
@@ -45,7 +46,7 @@ export default function EnterEmailStep({
// If everything is correct, go to the next step
if (!emailCheckBool) {
sendVerificationEmail(email);
await mutateAsync({ email });
incrementStep();
}
};

View File

@@ -9,8 +9,8 @@ import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import { useGetCommonPasswords } from "@app/hooks/api";
import { completeAccountSignup } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup";
import ProjectService from "@app/services/ProjectService";
import InputField from "../basic/InputField";
@@ -159,7 +159,7 @@ export default function UserInfoStep({
secret: Buffer.from(derivedKey.hash)
});
const response = await completeAccountInformationSignup({
const response = await completeAccountSignup({
email,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),

View File

@@ -3,8 +3,9 @@ import crypto from "crypto";
import jsrp from "jsrp";
import changePassword2 from "@app/pages/api/auth/ChangePassword2";
import SRP1 from "@app/pages/api/auth/SRP1";
import {
changePassword,
srp1} from "@app/hooks/api/auth/queries";
import Aes256Gcm from "./cryptography/aes-256-gcm";
import { deriveArgonKey } from "./cryptography/crypto";
@@ -27,7 +28,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
try {
const clientPublicKey = clientOldPassword.getPublicKey();
const res = await SRP1({ clientPublicKey });
const res = await srp1({ clientPublicKey });
serverPublicKey = res.serverPublicKey;
salt = res.salt;
@@ -71,7 +72,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
secret: Buffer.from(derivedKey.hash)
});
await changePassword2({
await changePassword({
clientProof,
protectedKey,
protectedKeyIV,

View File

@@ -1,10 +1,9 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import login1 from "@app/pages/api/auth/Login1";
import login2 from "@app/pages/api/auth/Login2";
import KeyService from "@app/services/KeyService";
import Telemetry from "./telemetry/Telemetry";

View File

@@ -1,10 +1,10 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
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 KeyService from "@app/services/KeyService";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
@@ -65,7 +65,7 @@ const attemptLoginMfa = async ({
tag
} = await verifyMfaToken({
email,
mfaToken
mfaCode: mfaToken
});
// unset temporary (MFA) JWT token and set JWT token

View File

@@ -1,10 +1,9 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import login1 from "@app/pages/api/auth/Login1";
import login2 from "@app/pages/api/auth/Login2";
import KeyService from "@app/services/KeyService";
import Telemetry from "./telemetry/Telemetry";
@@ -46,12 +45,13 @@ const attemptLogin = async (
async () => {
try {
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1({
email,
clientPublicKey,
providerAuthToken,
});
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1

View File

@@ -1,10 +1,9 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import login1 from "@app/pages/api/auth/Login1";
import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken";
import KeyService from "@app/services/KeyService";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
@@ -56,7 +55,7 @@ const attemptLoginMfa = async ({
tag
} = await verifyMfaToken({
email,
mfaToken
mfaCode: mfaToken
});
// unset temporary (MFA) JWT token and set JWT token

View File

@@ -1,147 +0,0 @@
/* eslint-disable new-cap */
import crypto from "crypto";
import jsrp from "jsrp";
import changePassword2 from "@app/pages/api/auth/ChangePassword2";
import SRP1 from "@app/pages/api/auth/SRP1";
import { saveTokenToLocalStorage } from "../saveTokenToLocalStorage";
import Aes256Gcm from "./aes-256-gcm";
import { deriveArgonKey } from "./crypto";
const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client();
/**
* This function loggs in the user (whether it's right after signup, or a normal login)
* @param {*} email
* @param {*} password
* @param {*} setErrorLogin
* @param {*} router
* @param {*} isSignUp
* @returns
*/
const changePassword = async (
email: string,
currentPassword: string,
newPassword: string,
setCurrentPasswordError: (arg: boolean) => void,
setPasswordChanged: (arg: boolean) => void,
setCurrentPassword: (arg: string) => void,
setNewPassword: (arg: string) => void
) => {
try {
setPasswordChanged(false);
setCurrentPasswordError(false);
clientOldPassword.init(
{
username: email,
password: currentPassword
},
async () => {
const clientPublicKey = clientOldPassword.getPublicKey();
let serverPublicKey;
let salt;
try {
const res = await SRP1({
clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
} catch (err) {
setCurrentPasswordError(true);
console.log("Wrong current password", err, 1);
}
clientOldPassword.setSalt(salt);
clientOldPassword.setServerPublicKey(serverPublicKey);
const clientProof = clientOldPassword.getProof(); // called M1
clientNewPassword.init(
{
username: email,
password: newPassword
},
async () => {
clientNewPassword.createVerifier(async (err, result) => {
const derivedKey = await deriveArgonKey({
password: newPassword,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: localStorage.getItem("PRIVATE_KEY") as string,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
try {
await changePassword2({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier
});
saveTokenToLocalStorage({
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
});
setPasswordChanged(true);
setCurrentPassword("");
setNewPassword("");
window.location.href = "/login";
// move to login page
} catch (error) {
setCurrentPasswordError(true);
console.log(error);
}
});
}
);
}
);
} catch (error) {
console.log("Something went wrong during changing the password");
}
return true;
};
export default changePassword;

View File

@@ -3,8 +3,9 @@ import crypto from "crypto";
import jsrp from "jsrp";
import issueBackupPrivateKey from "@app/pages/api/auth/IssueBackupPrivateKey";
import SRP1 from "@app/pages/api/auth/SRP1";
import { issueBackupPrivateKey ,
srp1
} from "@app/hooks/api/auth/queries";
import generateBackupPDF from "../generateBackupPDF";
import Aes256Gcm from "./aes-256-gcm";
@@ -51,7 +52,7 @@ const issueBackupKey = async ({
let serverPublicKey;
let salt;
try {
const res = await SRP1({
const res = await srp1({
clientPublicKey
});
serverPublicKey = res.serverPublicKey;
@@ -61,8 +62,8 @@ const issueBackupKey = async ({
console.log("Wrong current password", err, 1);
}
clientPassword.setSalt(salt);
clientPassword.setServerPublicKey(serverPublicKey);
clientPassword.setSalt(salt as string);
clientPassword.setServerPublicKey(serverPublicKey as string);
const clientProof = clientPassword.getProof(); // called M1
const generatedKey = crypto.randomBytes(16).toString("hex");
@@ -80,24 +81,25 @@ const issueBackupKey = async ({
secret: generatedKey
});
const res = await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof
});
try {
await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof
});
if (res?.status === 400) {
setBackupKeyError(true);
} else if (res?.status === 200) {
generateBackupPDF({
personalName,
personalEmail: email,
generatedKey
});
setBackupKeyIssued(true);
} catch {
setBackupKeyError(true);
}
}
);

View File

@@ -1,6 +1,10 @@
export {
useGetAuthToken,
useGetCommonPasswords,
useResetPassword,
useSendMfaToken,
useVerifyMfaToken
} from "./queries"
useSendPasswordResetEmail,
useSendVerificationEmail,
useVerifyEmailVerificationCode,
useVerifyMfaToken,
useVerifyPasswordResetCode} from "./queries"

View File

@@ -4,16 +4,86 @@ import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery";
import {
ChangePasswordDTO,
CompleteAccountDTO,
CompleteAccountSignupDTO,
GetAuthTokenAPI,
GetBackupEncryptedPrivateKeyDTO,
IssueBackupPrivateKeyDTO,
Login1DTO,
Login1Res,
Login2DTO,
Login2Res,
ResetPasswordDTO,
SendMfaTokenDTO,
SRP1DTO,
SRPR1Res,
VerifyMfaTokenDTO,
VerifyMfaTokenRes} from "./types";
VerifyMfaTokenRes,
VerifySignupInviteDTO} from "./types";
const authKeys = {
getAuthToken: ["token"] as const,
commonPasswords: ["common-passwords"] as const
};
export const login1 = async (loginDetails: Login1DTO) => {
const { data } = await apiRequest.post<Login1Res>("/api/v3/auth/login1", loginDetails);
return data;
}
export const login2 = async (loginDetails: Login2DTO) => {
const { data } = await apiRequest.post<Login2Res>("/api/v3/auth/login2", loginDetails);
return data;
}
export const useLogin1 = () => {
return useMutation({
mutationFn: async (details: {
email: string;
clientPublicKey: string;
providerAuthToken?: string;
}) => {
return login1(details);
}
});
}
export const useLogin2 = () => {
return useMutation({
mutationFn: async (details: {
email: string;
clientProof: string;
providerAuthToken?: string;
}) => {
return login2(details);
}
});
}
export const srp1 = async (details: SRP1DTO) => {
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
return data;
}
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
return data;
}
export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => {
const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details);
return data;
}
export const useCompleteAccountSignup = () => {
return useMutation({
mutationFn: async (details: CompleteAccountSignupDTO) => {
return completeAccountSignup(details);
}
});
}
export const useSendMfaToken = () => {
return useMutation<{}, {}, SendMfaTokenDTO>({
mutationFn: async ({ email }) => {
@@ -23,18 +93,161 @@ export const useSendMfaToken = () => {
});
}
export const verifyMfaToken = async ({
email,
mfaCode
}: {
email: string;
mfaCode: string;
}) => {
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
email,
mfaToken: mfaCode
});
return data;
}
export const useVerifyMfaToken = () => {
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
mutationFn: async ({ email, mfaCode }) => {
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
return verifyMfaToken({
email,
mfaToken: mfaCode
mfaCode
});
}
});
}
export const verifySignupInvite = async (details: VerifySignupInviteDTO) => {
const { data } = await apiRequest.post("/api/v1/invite-org/verify", details);
return data;
}
export const useSendVerificationEmail = () => {
return useMutation({
mutationFn: async ({
email
}: {
email: string;
}) => {
const { data } = await apiRequest.post("/api/v1/signup/email/signup", {
email
});
return data;
}
});
}
export const useVerifyEmailVerificationCode = () => {
return useMutation({
mutationFn: async ({
email,
code
}: {
email: string;
code: string;
}) => {
const { data } = await apiRequest.post("/api/v1/signup/email/verify", {
email,
code
});
return data;
}
});
}
export const useSendPasswordResetEmail = () => {
return useMutation({
mutationFn: async ({
email
}: {
email: string;
}) => {
const { data } = await apiRequest.post("/api/v1/password/email/password-reset", {
email
});
return data;
}
});
}
export const useVerifyPasswordResetCode = () => {
return useMutation({
mutationFn: async ({
email,
code
}: {
email: string;
code: string;
}) => {
const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", {
email,
code
});
return data;
}
});
}
export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => {
const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details);
return data;
}
export const getBackupEncryptedPrivateKey = async ({
verificationToken
}: GetBackupEncryptedPrivateKeyDTO) => {
const { data } = await apiRequest.get("/api/v1/password/backup-private-key", {
headers: {
Authorization: `Bearer ${verificationToken}`
}
});
return data.backupPrivateKey;
}
export const useResetPassword = () => {
return useMutation({
mutationFn: async (details: ResetPasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/password-reset", {
protectedKey: details.protectedKey,
protectedKeyIV: details.protectedKeyIV,
protectedKeyTag: details.protectedKeyTag,
encryptedPrivateKey: details.encryptedPrivateKey,
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
salt: details.salt,
verifier: details.verifier
}, {
headers: {
Authorization: `Bearer ${details.verificationToken}`
}
});
return data;
}
});
}
export const changePassword = async (details: ChangePasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
return data;
}
export const useChangePassword = () => {
// note: use after srp1
return useMutation({
mutationFn: async (details: ChangePasswordDTO) => {
return changePassword(details);
}
});
}
// Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls
const fetchAuthToken = async () => {

View File

@@ -21,4 +21,107 @@ export type VerifyMfaTokenRes = {
encryptedPrivateKey: string;
iv: string;
tag: string;
}
export type Login1DTO = {
email: string;
clientPublicKey: string;
providerAuthToken?: string;
}
export type Login2DTO = {
email: string;
clientProof: string;
providerAuthToken?: string;
}
export type Login1Res = {
serverPublicKey: string;
salt: string;
}
export type Login2Res = {
mfaEnabled: boolean;
token: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
export type SRP1DTO = {
clientPublicKey: string;
}
export type SRPR1Res = {
serverPublicKey: string;
salt: string;
}
export type CompleteAccountDTO = {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
}
export type CompleteAccountSignupDTO = CompleteAccountDTO & {
providerAuthToken?: string;
attributionSource?: string;
organizationName: string;
}
export type VerifySignupInviteDTO = {
email: string;
code: string;
organizationId: string;
}
export type ChangePasswordDTO = {
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
}
export type ResetPasswordDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
verificationToken: string;
}
export type IssueBackupPrivateKeyDTO = {
encryptedPrivateKey: string;
iv: string;
tag: string;
salt: string;
verifier: string;
clientProof: string;
}
export type GetBackupEncryptedPrivateKeyDTO = {
verificationToken: string;
}

View File

@@ -201,7 +201,9 @@ export const useRegisterUserAction = () => {
export const useLogoutUser = () =>
useMutation({
mutationFn: () => apiRequest.post("/api/v1/auth/logout"),
mutationFn: async () => {
await apiRequest.post("/api/v1/auth/logout");
},
onSuccess: () => {
setAuthToken("");
// Delete the cookie by not setting a value; Alternatively clear the local storage

View File

@@ -1,46 +0,0 @@
import { apiRequest } from "@app/config/request";
interface Props {
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
}
/**
* This is the second step of the change password process (pake)
* @param {*} clientPublicKey
* @returns
*/
const changePassword2 = async ({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}: Props) => {
const { data } = await apiRequest.post("/api/v1/password/change-password", {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
return data;
}
export default changePassword2;

View File

@@ -4,12 +4,13 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
* This function is used to check if the user is authenticated.
* To do that, we get their tokens from cookies, and verify if they are good.
*/
const checkAuth = async () =>
SecurityClient.fetchCall("/api/v1/auth/checkAuth", {
const checkAuth = async () => {
return SecurityClient.fetchCall("/api/v1/auth/checkAuth", {
method: "POST",
headers: {
"Content-Type": "application/json"
}
}).then((res) => res);
}
export default checkAuth;

View File

@@ -1,24 +0,0 @@
interface Props {
email: string;
code: string;
}
/**
* This route check the verification code from the email that user just recieved
* @param {object} obj
* @param {string} obj.email
* @param {string} obj.code
* @returns
*/
const checkEmailVerificationCode = ({ email, code }: Props) => fetch("/api/v1/signup/email/verify", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email,
code
})
});
export default checkEmailVerificationCode;

View File

@@ -1,79 +0,0 @@
import { apiRequest } from "@app/config/request";
interface Props {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
providerAuthToken?: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
organizationName: string;
salt: string;
verifier: string;
attributionSource?: string;
}
/**
* This function is called in the end of the signup process.
* It sends all the necessary nformation to the server.
* @param {object} obj
* @param {string} obj.email - email of the user completing signup
* @param {string} obj.firstName - first name of the user completing signup
* @param {string} obj.lastName - last name of the user completing sign up
* @param {string} obj.protectedKey - protected key in encryption version 2
* @param {string} obj.protectedKeyIV - IV of protected key in encryption version 2
* @param {string} obj.protectedKeyTag - tag of protected key in encryption version 2
* @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization)
* @param {string} obj.publicKey - public key of the user completing signup
* @param {string} obj.ciphertext
* @param {string} obj.iv
* @param {string} obj.tag
* @param {string} obj.salt
* @param {string} obj.verifier
* @returns
*/
const completeAccountInformationSignup = async ({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName,
providerAuthToken,
attributionSource
}: Props) => {
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName,
providerAuthToken,
...(attributionSource ? { attributionSource } : {})
});
return data;
}
export default completeAccountInformationSignup;

View File

@@ -1,70 +0,0 @@
import { apiRequest } from "@app/config/request";
interface Props {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: 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.
* @param {object} obj
* @param {string} obj.email - email of the user completing signupinvite flow
* @param {string} obj.firstName - first name of the user completing signupinvite flow
* @param {string} obj.lastName - last name of the user completing signupinvite flow
* @param {string} obj.publicKey - public key of the user completing signupinvite flow
* @param {string} obj.ciphertext
* @param {string} obj.iv
* @param {string} obj.tag
* @param {string} obj.salt
* @param {string} obj.verifier
* @param {string} obj.token - token that confirms a user's identity
* @returns
*/
const completeAccountInformationSignupInvite = async ({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
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;

View File

@@ -1,34 +0,0 @@
interface Props {
email: string;
code: string;
}
/**
* This is the second part of the account recovery step (a user needs to verify their email).
* A user need to click on a button in a magic link page
* @param {object} obj
* @param {object} obj.email - email of a user that is trying to recover access to their account
* @param {object} obj.code - token that a use received via the magic link
* @returns
*/
const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => {
const response = await fetch("/api/v1/password/email/password-reset-verify", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email,
code
})
});
if (response?.status === 200) {
return response;
}
throw new Error(
"Something went wrong during email verification on password reset."
);
};
export default EmailVerifyOnPasswordReset;

View File

@@ -1,51 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
encryptedPrivateKey: string;
iv: string;
tag: string;
salt: string;
verifier: string;
clientProof: string;
}
/**
* This is the route that issues a backup private key that will afterwards be added into a pdf
* @param {object} obj
* @param {string} obj.encryptedPrivateKey
* @param {string} obj.iv
* @param {string} obj.tag
* @param {string} obj.salt
* @param {string} obj.verifier
* @param {string} obj.clientProof
* @returns
*/
const issueBackupPrivateKey = ({
encryptedPrivateKey,
iv,
tag,
salt,
verifier,
clientProof
}: Props) =>
SecurityClient.fetchCall("/api/v1/password/backup-private-key", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
clientProof,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
})
}).then((res) => {
if (res?.status !== 200) {
console.log("Failed to issue the backup key");
}
return res;
});
export default issueBackupPrivateKey;

View File

@@ -1,33 +0,0 @@
interface Login1 {
serverPublicKey: string;
salt: string;
}
/**
* This is the first step of the login process (pake)
* @param {*} email
* @param {*} clientPublicKey
* @returns
*/
const login1 = async (loginDetails: {
email: string;
clientPublicKey: string;
providerAuthToken?: string;
}) => {
const response = await fetch("/api/v3/auth/login1", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(loginDetails),
});
// need precise error handling about the status code
if (response?.status === 200) {
const data = (await response.json()) as unknown as Login1;
return data;
}
throw new Error("Wrong password");
};
export default login1;

View File

@@ -1,42 +0,0 @@
interface Login2Response {
mfaEnabled: boolean;
token: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
/**
* This is the second step of the login process
* @param {*} email
* @param {*} clientPublicKey
* @returns
*/
const login2 = async (loginDetails: {
email: string;
clientProof: string;
providerAuthToken?: string;
}) => {
const response = await fetch("/api/v3/auth/login2", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(loginDetails),
credentials: "include"
});
// need precise error handling about the status code
if (response.status === 200) {
const data = (await response.json()) as unknown as Login2Response;
return data;
}
throw new Error("Password verification failed");
};
export default login2;

View File

@@ -1,41 +0,0 @@
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 () => {
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
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");
return res;
}
} catch (error) {
console.log("Error logging out", error);
}
return undefined;
};
export default logout;

View File

@@ -1,29 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
clientPublicKey: string;
}
/**
* This is the first step of the change password process (pake)
* @param {string} clientPublicKey
* @returns
*/
const SRP1 = ({ clientPublicKey }: Props) =>
SecurityClient.fetchCall("/api/v1/password/srp1", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
clientPublicKey
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to do the first step of SRP");
return undefined;
});
export default SRP1;

View File

@@ -1,33 +0,0 @@
interface Props {
email: string;
}
/**
* This is the first of the account recovery step (a user needs to verify their email).
* It will send an email containing a magic link to start the account recovery flow.
* @param {object} obj
* @param {object} obj.email - email of a user that is trying to recover access to their account
* @returns
*/
const SendEmailOnPasswordReset = async ({ email }: Props) => {
const response = await fetch("/api/v1/password/email/password-reset", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email
})
});
// need precise error handling about the status code
if (response?.status === 200) {
const data = await response.json();
return data;
}
throw new Error(
"Something went wrong while sending the email verification for password reset."
);
};
export default SendEmailOnPasswordReset;

View File

@@ -1,17 +0,0 @@
/**
* This route send the verification email to the user's email (contains a 6-digit verification code)
* @param {*} email
*/
const sendVerificationEmail = (email: string) => {
fetch("/api/v1/signup/email/signup", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email
})
});
};
export default sendVerificationEmail;

View File

@@ -1,16 +0,0 @@
const token = async () =>
fetch("/api/v1/auth/token", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
credentials: "include"
}).then(async (res) => {
if (res.status === 200) {
return (await res.json()).token;
}
console.log("Getting a new token failed");
return undefined;
});
export default token;

View File

@@ -1,27 +0,0 @@
interface Props {
email: string;
code: string;
organizationId: string;
}
/**
* This route verifies the signup invite link
* @param {object} obj
* @param {string} obj.email - email that a user is trying to verify
* @param {string} obj.organizationId - id of organization that a user is trying to verify for
* @param {string} obj.code - code that a user received to the abovementioned email
* @returns
*/
const verifySignupInvite = ({ email, organizationId, code }: Props) => fetch("/api/v1/invite-org/verify", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email,
organizationId,
code
})
});
export default verifySignupInvite;

View File

@@ -1,24 +0,0 @@
/**
* This is the route that get an encrypted private key (will be decrypted with a backup key)
* @param {object} obj
* @param {object} obj.verificationToken - this is the token that confirms that a user is the right one
* @returns
*/
const getBackupEncryptedPrivateKey = ({
verificationToken
}: {
verificationToken: string;
}) => fetch("/api/v1/password/backup-private-key", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${ verificationToken}`
}
}).then(async (res) => {
if (res?.status !== 200) {
console.log("Failed to get the backup key");
}
return (await res?.json())?.backupPrivateKey;
});
export default getBackupEncryptedPrivateKey;

View File

@@ -1,8 +0,0 @@
const publicKeyInfisical = () => fetch("/api/v1/key/publicKey/infisical", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
export default publicKeyInfisical;

View File

@@ -1,57 +0,0 @@
interface Props {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
verificationToken: string;
}
/**
* This is the route that resets the account password if all the previus steps were passed
* @param {object} obj
* @param {object} obj.verificationToken - this is the token that confirms that a user is the right one
* @param {object} obj.encryptedPrivateKey - the new encrypted private key (encrypted using the new password)
* @param {object} obj.iv
* @param {object} obj.tag
* @param {object} obj.salt
* @param {object} obj.verifier
* @returns
*/
const resetPasswordOnAccountRecovery = ({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
verificationToken,
}: Props) => fetch("/api/v1/password/password-reset", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${verificationToken}`
},
body: JSON.stringify({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
}).then(async (res) => {
if (res?.status !== 200) {
console.log("Failed to get the backup key");
}
return res;
});
export default resetPasswordOnAccountRecovery;

View File

@@ -1,25 +0,0 @@
import { apiRequest } from "@app/config/request";
/**
* Verify MFA token [mfaToken] for user with email [email]
* @param {object} obj
* @param {string} obj.email - email of user
* @param {string} obj.mfaToken - MFA cod/token to verify
* @returns
*/
const verifyMfaToken = async ({
email,
mfaToken
}: {
email: string;
mfaToken: string;
}) => {
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
email,
mfaToken
});
return data;
}
export default verifyMfaToken;

View File

@@ -12,11 +12,10 @@ import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField";
import passwordCheck from "@app/components/utilities/checks/PasswordCheck";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { useResetPassword,useVerifyPasswordResetCode } from "@app/hooks/api";
import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
import { deriveArgonKey } from "../components/utilities/cryptography/crypto";
import EmailVerifyOnPasswordReset from "./api/auth/EmailVerifyOnPasswordReset";
import getBackupEncryptedPrivateKey from "./api/auth/getBackupEncryptedPrivateKey";
import resetPasswordOnAccountRecovery from "./api/auth/resetPasswordOnAccountRecovery";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
@@ -34,6 +33,10 @@ export default function PasswordReset() {
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const router = useRouter();
const { mutateAsync: verifyPasswordResetCodeMutateAsync } = useVerifyPasswordResetCode();
const { mutateAsync: resetPasswordMutateAsync } = useResetPassword();
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
const token = parsedUrl.token as string;
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
@@ -43,7 +46,7 @@ export default function PasswordReset() {
e.preventDefault();
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
setPrivateKey(
Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey,
@@ -53,7 +56,8 @@ export default function PasswordReset() {
})
);
setStep(3);
} catch {
} catch(err) {
console.error(err);
setBackupKeyError(true);
}
};
@@ -112,7 +116,7 @@ export default function PasswordReset() {
secret: Buffer.from(derivedKey.hash)
});
const response = await resetPasswordOnAccountRecovery({
await resetPasswordMutateAsync({
protectedKey,
protectedKeyIV,
protectedKeyTag,
@@ -123,11 +127,9 @@ export default function PasswordReset() {
verifier: result.verifier,
verificationToken
});
router.push("/login");
// if everything works, go the main dashboard page.
if (response?.status === 200) {
router.push("/login");
}
setLoading(false)
});
}
@@ -146,15 +148,16 @@ export default function PasswordReset() {
<Button
text="Confirm Email"
onButtonPressed={async () => {
const response = await EmailVerifyOnPasswordReset({
email,
code: token
});
if (response.status === 200) {
setVerificationToken((await response.json()).token);
try {
const response = await verifyPasswordResetCodeMutateAsync({
email,
code: token
});
setVerificationToken(response.token);
setStep(2);
} else {
console.log("ERROR", response);
} catch (err) {
console.log("ERROR", err);
router.push("/email-not-verified");
}
}}

View File

@@ -12,9 +12,9 @@ import InitialSignupStep from "@app/components/signup/InitialSignupStep";
import TeamInviteStep from "@app/components/signup/TeamInviteStep";
import UserInfoStep from "@app/components/signup/UserInfoStep";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { useVerifyEmailVerificationCode } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import checkEmailVerificationCode from "@app/pages/api/auth/CheckEmailVerificationCode";
/**
* @returns the signup page
@@ -33,6 +33,7 @@ export default function SignUp() {
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
const { t } = useTranslation();
const { mutateAsync } = useVerifyEmailVerificationCode();
useEffect(() => {
const tryAuth = async () => {
@@ -60,12 +61,12 @@ export default function SignUp() {
} else if (step === 2) {
setIsCodeInputCheckLoading(true);
// Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200) {
const { token } = await response.json();
try {
const { token } = await mutateAsync({ email, code });
SecurityClient.setSignupToken(token);
setStep(3);
} else {
} catch(err) {
console.error(err);
setCodeError(true);
}
setIsCodeInputCheckLoading(false);
@@ -74,15 +75,16 @@ export default function SignUp() {
// when email service is not configured, skip step 2 and 5
useEffect(() => {
if (!serverDetails?.emailConfigured && step === 2) {
incrementStep();
}
(async () => {
if (!serverDetails?.emailConfigured && step === 2) {
incrementStep();
}
if (!serverDetails?.emailConfigured && step === 5) {
getOrganizations().then((userOrgs) => {
if (!serverDetails?.emailConfigured && step === 5) {
const userOrgs = await fetchOrganizations();
router.push(`/org/${userOrgs[0]._id}/overview`);
});
}
}
})();
}, [step]);
const renderView = (registerStep: number) => {

View File

@@ -7,7 +7,7 @@ import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faCheck, faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import jsrp from "jsrp";
import queryString from "query-string";
@@ -16,7 +16,6 @@ import { encodeBase64 } from "tweetnacl-util";
import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField";
import attemptLogin from "@app/components/utilities/attemptLogin";
import checkPassword from "@app/components/utilities/checks/checkPassword";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
@@ -26,11 +25,12 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
import {
useGetCommonPasswords
} from "@app/hooks/api";
import {
completeAccountSignupInvite,
verifySignupInvite
} from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import completeAccountInformationSignupInvite from "./api/auth/CompleteAccountInformationSignupInvite";
import verifySignupInvite from "./api/auth/VerifySignupInvite";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
@@ -141,7 +141,7 @@ export default function SignupInvite() {
const {
token: jwtToken
} = await completeAccountInformationSignupInvite({
} = await completeAccountSignupInvite({
email,
firstName,
lastName,
@@ -197,25 +197,27 @@ export default function SignupInvite() {
<Button
text="Confirm Email"
onButtonPressed={async () => {
const response = await verifySignupInvite({
email,
code: token,
organizationId
});
if (response.status === 200) {
const res = await response.json();
// user will have temp token if doesn't have an account
// then continue with account setup workflow
if (res?.token) {
SecurityClient.setSignupToken(res.token);
setStep(2);
} else {
// user will be redirected to dashboard
// if not logged in gets kicked out to login
router.push(`/org/${organizationId}/overview`);
try {
const response = await verifySignupInvite({
email,
code: token,
organizationId
});
if (response) {
// user will have temp token if doesn't have an account
// then continue with account setup workflow
if (response?.token) {
SecurityClient.setSignupToken(response.token);
setStep(2);
} else {
// user will be redirected to dashboard
// if not logged in gets kicked out to login
router.push(`/org/${organizationId}/overview`);
}
}
} else {
console.log("ERROR", response);
} catch (err) {
console.error(err);
router.push("/requestnewinvite");
}
}}

View File

@@ -7,10 +7,9 @@ import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField";
import { EmailServiceSetupModal } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { useSendPasswordResetEmail } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import SendEmailOnPasswordReset from "./api/auth/SendEmailOnPasswordReset";
export default function VerifyEmail() {
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState("");
@@ -18,12 +17,14 @@ export default function VerifyEmail() {
const { data: serverDetails } = useFetchServerStatus();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
const { mutateAsync } = useSendPasswordResetEmail();
/**
* This function sends the verification email and forwards a user to the next step.
*/
const sendVerificationEmail = async () => {
if (email) {
await SendEmailOnPasswordReset({ email });
await mutateAsync({ email });
setStep(2);
}
};

View File

@@ -17,8 +17,8 @@ import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLo
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { useGetCommonPasswords } from "@app/hooks/api";
import { completeAccountSignup } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup";
import ProjectService from "@app/services/ProjectService";
// eslint-disable-next-line new-cap
@@ -157,7 +157,7 @@ export const UserInfoSSOStep = ({
secret: Buffer.from(derivedKey.hash)
});
const response = await completeAccountInformationSignup({
const response = await completeAccountSignup({
email,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),