Checkpoint weaving frontend and backend MFA

This commit is contained in:
Tuan Dang
2023-02-13 12:29:36 +07:00
parent 17406e413d
commit e53fd110f6
30 changed files with 1177 additions and 482 deletions
@@ -1,3 +1,5 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
email: string;
firstName: string;
@@ -12,7 +14,6 @@ interface Props {
organizationName: string;
salt: string;
verifier: string;
token: string;
}
/**
@@ -32,7 +33,6 @@ interface Props {
* @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 completeAccountInformationSignup = ({
@@ -48,13 +48,11 @@ const completeAccountInformationSignup = ({
encryptedPrivateKeyTag,
salt,
verifier,
token,
organizationName
}: Props) => fetch('/api/v2/signup/complete-account/signup', {
}: Props) => SecurityClient.fetchCall('/api/v2/signup/complete-account/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ token}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
@@ -71,6 +69,12 @@ const completeAccountInformationSignup = ({
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');
});
export default completeAccountInformationSignup;
+1 -1
View File
@@ -25,7 +25,7 @@ const login1 = async (email: string, clientPublicKey: string) => {
const data = (await response.json()) as unknown as Login1;
return data;
}
throw new Error("Wrong password");
};
+6 -6
View File
@@ -1,14 +1,14 @@
interface Login2Response {
mfaEnabled: boolean;
encryptionVersion: number;
token: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
/**
@@ -0,0 +1,29 @@
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;
@@ -0,0 +1,35 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* 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;
}) => 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');
});
export default verifyMfaToken;
@@ -0,0 +1,32 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
isMfaEnabled: boolean;
}
/**
* Update the user's MFA-enabled status to [isMfaEnabled]
* @param {Object} obj
* @param {Boolean} obj.isMfaEnabled - whether or not MFA status should be set to enabled or not
* @returns {User} user - user with updated MFA-enabled status
*/
const updateMyMfaEnabled = async ({
isMfaEnabled
}: Props) =>
SecurityClient.fetchCall(`/api/v2/users/me/mfa`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
isMfaEnabled,
})
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).user;
}
console.log('Failed to update MFA status');
return undefined;
});
export default updateMyMfaEnabled;
+31 -101
View File
@@ -4,28 +4,24 @@ import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import Error from '@app/components/basic/Error';
import InputField from '@app/components/basic/InputField';
import ListBox from '@app/components/basic/Listbox';
import attemptLogin from '@app/components/utilities/attemptLogin';
import LoginStep from '@app/components/login/LoginStep';
import MFAStep from '@app/components/login/MFAStep';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { isLoggedIn } from '@app/reactQuery';
import getWorkspaces from './api/workspace/getWorkspaces';
export default function Login() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
const router = useRouter();
const [step, setStep] = useState(1);
const { t } = useTranslation();
const lang = router.locale ?? 'en';
const setLanguage = async (to: string) => {
router.push('/login', '/login', { locale: to });
@@ -49,25 +45,32 @@ export default function Login() {
redirectToDashboard();
}
}, []);
/**
* This function check if the user entered the correct credentials and should be allowed to log in.
*/
const loginCheck = async () => {
// #TODO: IF 2FA IS ENABLED REDIRECT TO <2FASTEP /> AND 'return;'
if (!email || !password) {
return;
const renderStep = (loginStep: number) => {
// TODO: add MFA step
switch (loginStep) {
case 1:
return (
<LoginStep
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
setStep={setStep}
/>
);
case 2:
// TODO: add MFA step
return (
<MFAStep
email={email}
password={password}
/>
);
default:
return <div />
}
setIsLoading(true);
await attemptLogin(email, password, setErrorLogin, router, false, true).then(() => {
setTimeout(() => {
setIsLoading(false);
}, 2000);
});
};
}
if (isAlreadyLoggedIn) {
return null
@@ -87,80 +90,7 @@ export default function Login() {
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
</div>
</Link>
<form onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()}>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
{t('login:login')}
</p>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<InputField
label={t('common:email')}
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
autoComplete="username"
/>
</div>
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
<InputField
label={t('common:password')}
onChangeHandler={setPassword}
type="password"
value={password}
placeholder=""
isRequired
autoComplete="current-password"
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && errorLogin && <Error text={t('login:error-login') ?? ''} />}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button
type="submit"
text={t('login:login') ?? ''}
onButtonPressed={loginCheck}
loading={isLoading}
size="lg"
/>
</div>
</div>
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
</div> */}
</div>
{false && (
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
{t('common:maintenance-alert')}
</div>
)}
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
<p className="text-sm flex justify-center text-gray-400 w-max">
{t('login:need-account')}
</p>
<Link href="/signup">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:create-account')}
</button>
</Link>
</div>
</form>
{renderStep(step)}
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
<div className="w-48 mx-auto">
<ListBox
@@ -101,10 +101,7 @@ export default function PersonalSettings() {
/>
</div>
</div>
<SecuritySection
isTwoFAEnabled
onIsTwoFAEnabledChange={(value: boolean) => console.log(value)}
/>
<SecuritySection />
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-2 mb-8 pt-2">
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full">
+2 -3
View File
@@ -11,6 +11,7 @@ import DownloadBackupPDF from '@app/components/signup/DonwloadBackupPDFStep';
import EnterEmailStep from '@app/components/signup/EnterEmailStep';
import TeamInviteStep from '@app/components/signup/TeamInviteStep';
import UserInfoStep from '@app/components/signup/UserInfoStep';
import SecurityClient from '@app/components/utilities/SecurityClient';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
@@ -28,7 +29,6 @@ export default function SignUp() {
const [codeError, setCodeError] = useState(false);
const [step, setStep] = useState(1);
const router = useRouter();
const [verificationToken, setVerificationToken] = useState('');
const { t } = useTranslation();
@@ -59,7 +59,7 @@ export default function SignUp() {
// Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200) {
setVerificationToken((await response.json()).token);
SecurityClient.setToken((await response.json()).token);
setStep(3);
} else {
setCodeError(true);
@@ -94,7 +94,6 @@ export default function SignUp() {
/>
) : step === 3 ? (
<UserInfoStep
verificationToken={verificationToken}
incrementStep={incrementStep}
email={email}
password={password}
+5 -1
View File
@@ -29,6 +29,7 @@ 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('');
@@ -133,6 +134,7 @@ export default function SignupInvite() {
secret: Buffer.from(derivedKey.hash)
});
console.log('SignupInvite A');
let response = await completeAccountInformationSignupInvite({
email,
firstName,
@@ -148,17 +150,19 @@ export default function SignupInvite() {
verifier: result.verifier,
token: verificationToken
});
console.log('SignupInvite B');
// if everything works, go the main dashboard page.
if (!errorCheck && response.status === 200) {
response = await response.json();
console.log('SignupInvite C');
localStorage.setItem('publicKey', publicKey);
localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey);
localStorage.setItem('iv', encryptedPrivateKeyIV);
localStorage.setItem('tag', encryptedPrivateKeyTag);
console.log('SignupInvite D');
await attemptLogin(email, password, setErrorLogin, router, false, false);
setStep(3);
}
} catch (error) {