Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve 2fa: ask for code before account removal and 2fa disabling #3817

Merged
merged 75 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
3659f8c
fix conflicts
Aug 24, 2022
f02cb4d
fix remove separate function and call mutation directly
Aug 10, 2022
cc50a02
feat: add new react-otp-input to enable 2fa flow
Aug 11, 2022
e3e35c0
fix: comment out
Aug 11, 2022
3ebf238
fix: remove next-auth 4.9.0 from yarn.lock
Aug 11, 2022
dc06114
fix: delete account test fill password before submit
Aug 12, 2022
6fb2454
fix: test delete accc
Aug 12, 2022
013315f
fix typo in delete acc test
Aug 12, 2022
67a9d46
Update apps/web/components/security/EnableTwoFactorModal.tsx
agustif Aug 12, 2022
acdd58e
feat: remove react-otp-input reuse TwoFactor
Aug 24, 2022
faf7e39
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 24, 2022
bb88d5e
feat: add center props to TwoFactor
Aug 24, 2022
95f4273
fix: no v2
Aug 24, 2022
cfcbb33
feat: disable 2fa requires 2fa api
Aug 24, 2022
32f533c
feat: make 2fa required to disable 2fa
Aug 24, 2022
959371d
fix: FormEvent instead of SyntheticEvent
Aug 24, 2022
49f1e77
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 24, 2022
d87062e
fix: types
Aug 25, 2022
b8b797f
fix: move disable 2fa form to fully use RHF
Aug 25, 2022
8b56f84
fix if (e) e.preventDefault();
Aug 25, 2022
287f91a
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
55e80b3
feat: fix remove account
Aug 25, 2022
677f37e
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
72fa9e5
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
f0c528c
fix: remove react-otp-input types
Aug 25, 2022
353d1ed
fix: separate onConfirm to add to form handleSubmit
Aug 25, 2022
815ba00
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
f09dea9
fix: types e:SyntethicEvent
Aug 25, 2022
df42834
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
04d2c53
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
b78ba87
fix: types
Aug 25, 2022
2be9c5d
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
716672b
fix: import packages lib not web lib
Aug 25, 2022
ec85a64
Update apps/web/components/security/EnableTwoFactorModal.tsx
agustif Aug 25, 2022
2fcecb9
Update apps/web/components/security/EnableTwoFactorModal.tsx
agustif Aug 25, 2022
4d29522
fix: no import from web
Aug 25, 2022
33393b3
fix: import
Aug 25, 2022
5c24bc4
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
25225da
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 25, 2022
43f976c
fix: remove duplicate FormEvent
Aug 25, 2022
6efc7e6
fix: upgrade ErrorCode imports
Aug 26, 2022
af5bda8
fix profile types totpCode not optional
Aug 26, 2022
5b6057b
fix: build pass
Aug 26, 2022
e5c21bb
Merge branch 'main' into fix/auth-ask
agustif Aug 26, 2022
bd4ed06
fix: dont touch test delete-account
Aug 26, 2022
d41b6ed
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
e19b640
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
641a234
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
299cee5
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
bd4d9f1
fix: type
Aug 26, 2022
93c1bb7
fix: add data-testid to password field
Aug 26, 2022
80475ae
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
93f5ca5
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
321e164
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
5bad442
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
40a4556
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
f34e9cd
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
619c0e1
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 26, 2022
28dde26
fix: conflicts w syncServices
Aug 30, 2022
973ebad
fix: conflicts
Aug 30, 2022
97784f4
Build fixes
zomars Aug 30, 2022
483ee3a
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 30, 2022
3824865
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
2df21d5
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
d099798
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
ef6ad47
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
9d6a033
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
955f4c2
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
6b4c147
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
993ddd3
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
80e0169
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
13c7c20
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
79e3b55
Merge branch 'main' into fix/auth-ask
kodiakhq[bot] Aug 31, 2022
0eab546
Merge branch 'main' into fix/auth-ask
zomars Aug 31, 2022
ec4d46d
Fixes delete account e2e test
zomars Aug 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions apps/web/components/auth/TwoFactor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import React, { useEffect, useState } from "react";
import useDigitInput from "react-digit-input";
import { useFormContext } from "react-hook-form";

import { Input } from "@calcom/ui/form/fields";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Input } from "@calcom/ui/form/fields";

import { useLocale } from "@lib/hooks/useLocale";

export default function TwoFactor() {
export default function TwoFactor({ center = true }) {
const [value, onChange] = useState("");
const { t } = useLocale();
const methods = useFormContext();
Expand All @@ -26,7 +25,9 @@ export default function TwoFactor() {
const className = "h-12 w-12 !text-xl text-center";

return (
<div className="mx-auto !mt-0 max-w-sm">
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
<Label className="mt-4"> {t("2fa_code")}</Label>

<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
<div className="flex flex-row justify-between">
Expand Down
79 changes: 41 additions & 38 deletions apps/web/components/security/DisableTwoFactorModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { SyntheticEvent, useState } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";

import { ErrorCode } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { Form, Label } from "@calcom/ui/form/fields";
import { PasswordField } from "@calcom/ui/v2/core/form/fields";

import { ErrorCode } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import TwoFactor from "@components/auth/TwoFactor";

import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
import TwoFactorModalHeader from "./TwoFactorModalHeader";
Expand All @@ -16,23 +20,25 @@ interface DisableTwoFactorAuthModalProps {
onDisable: () => void;
}

interface DisableTwoFactorValues {
totpCode: string;
password: string;
}

const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuthModalProps) => {
const [password, setPassword] = useState("");
const [isDisabling, setIsDisabling] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { t } = useLocale();

async function handleDisable(e: SyntheticEvent) {
e.preventDefault();

const form = useForm<DisableTwoFactorValues>();
async function handleDisable({ totpCode, password }: DisableTwoFactorValues) {
if (isDisabling) {
return;
}
setIsDisabling(true);
setErrorMessage(null);

try {
const response = await TwoFactorAuthAPI.disable(password);
const response = await TwoFactorAuthAPI.disable(password, totpCode);
if (response.status === 200) {
onDisable();
return;
Expand All @@ -41,6 +47,12 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
const body = await response.json();
if (body.error === ErrorCode.IncorrectPassword) {
setErrorMessage(t("incorrect_password"));
}
if (body.error === ErrorCode.SecondFactorRequired) {
setErrorMessage(t("2fa_required"));
}
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
setErrorMessage(t("incorrect_2fa"));
} else {
setErrorMessage(t("something_went_wrong"));
}
Expand All @@ -55,41 +67,32 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
return (
<Dialog open={true}>
<DialogContent>
<TwoFactorModalHeader title={t("disable_2fa")} description={t("disable_2fa_recommendation")} />
<Form form={form} handleSubmit={handleDisable}>
<TwoFactorModalHeader title={t("disable_2fa")} description={t("disable_2fa_recommendation")} />

<form onSubmit={handleDisable}>
<div className="mb-4">
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
{t("password")}
</label>
<div className="mt-1">
<input
type="password"
name="password"
id="password"
required
value={password}
onInput={(e) => setPassword(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 text-sm"
/>
</div>
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
}}
{...form.register("password")}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
<Label className="mt-4"> {t("2fa_code")}</Label>

<TwoFactor center={false} />
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
</form>

<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
onClick={handleDisable}
disabled={password.length === 0 || isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
</div>
</Form>
</DialogContent>
</Dialog>
);
Expand Down
107 changes: 47 additions & 60 deletions apps/web/components/security/EnableTwoFactorModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { SyntheticEvent, useState } from "react";
import React, { BaseSyntheticEvent, useState } from "react";
import { useForm } from "react-hook-form";

import { ErrorCode } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { Form } from "@calcom/ui/v2/core/form/fields";

import { ErrorCode } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import TwoFactor from "@components/auth/TwoFactor";

import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
import TwoFactorModalHeader from "./TwoFactorModalHeader";
Expand Down Expand Up @@ -39,22 +42,27 @@ const WithStep = ({
return step === current ? children : null;
};

interface EnableTwoFactorValues {
totpCode: string;
}

const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps) => {
const { t } = useLocale();
const form = useForm<EnableTwoFactorValues>();

const setupDescriptions = {
[SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"),
[SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"),
[SetupStep.EnterTotpCode]: t("2fa_enter_six_digit_code"),
};
const [step, setStep] = useState(SetupStep.ConfirmPassword);
const [password, setPassword] = useState("");
const [totpCode, setTotpCode] = useState("");
const [dataUri, setDataUri] = useState("");
const [secret, setSecret] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

async function handleSetup(e: SyntheticEvent) {
async function handleSetup(e: React.FormEvent) {
e.preventDefault();

if (isSubmitting) {
Expand Down Expand Up @@ -88,10 +96,10 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
}
}

async function handleEnable(e: SyntheticEvent) {
e.preventDefault();
async function handleEnable({ totpCode }: EnableTwoFactorValues, e: BaseSyntheticEvent | undefined) {
e?.preventDefault();

if (isSubmitting || totpCode.length !== 6) {
if (isSubmitting) {
return;
}

Expand Down Expand Up @@ -158,64 +166,43 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<p className="text-center font-mono text-xs">{secret}</p>
</>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<form onSubmit={handleEnable}>
<Form handleSubmit={handleEnable} form={form}>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<div className="mb-4">
<label htmlFor="code" className="mt-4 block text-sm font-medium text-gray-700">
{t("code")}
</label>
<div className="mt-1">
<input
type="text"
name="code"
id="code"
required
value={totpCode}
maxLength={6}
minLength={6}
inputMode="numeric"
onInput={(e) => setTotpCode(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 text-sm"
autoComplete="one-time-code"
/>
</div>
<TwoFactor center />

{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
</form>
</WithStep>

<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
onClick={handleEnable}
disabled={totpCode.length !== 6 || isSubmitting}>
{t("enable")}

<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isSubmitting}>
{t("enable")}
</Button>
</WithStep>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
</WithStep>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
</div>
</div>
</Form>
</DialogContent>
</Dialog>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/security/TwoFactorAuthAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ const TwoFactorAuthAPI = {
});
},

async disable(password: string) {
async disable(password: string, code: string) {
return fetch("/api/auth/two-factor/totp/disable", {
method: "POST",
body: JSON.stringify({ password }),
body: JSON.stringify({ password, code }),
headers: {
"Content-Type": "application/json",
},
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/security/TwoFactorModalHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const TwoFactorModalHeader = ({ title, description }: { title: string; descripti
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<ShieldCheckIcon className="h-6 w-6 text-black" />
<ShieldCheckIcon className="h-6 w-6 text-white" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
Expand Down
9 changes: 5 additions & 4 deletions apps/web/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@ import { compare, hash } from "bcryptjs";
import { Session } from "next-auth";
import { getSession as getSessionInner, GetSessionParams } from "next-auth/react";

/** @deprecated use the one from `@calcom/lib/auth` */
export async function hashPassword(password: string) {
const hashedPassword = await hash(password, 12);
return hashedPassword;
}

/** @deprecated use the one from `@calcom/lib/auth` */
export async function verifyPassword(password: string, hashedPassword: string) {
const isValid = await compare(password, hashedPassword);
return isValid;
}

/** @deprecated use the one from `@calcom/lib/auth` */
export async function getSession(options: GetSessionParams): Promise<Session | null> {
const session = await getSessionInner(options);

// that these are equal are ensured in `[...nextauth]`'s callback
return session as Session | null;
}

/** @deprecated use the one from `@calcom/lib/auth` */
export enum ErrorCode {
UserNotFound = "user-not-found",
IncorrectPassword = "incorrect-password",
Expand All @@ -33,7 +34,7 @@ export enum ErrorCode {
NewPasswordMatchesOld = "new-password-matches-old",
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
}

/** @deprecated use the one from `@calcom/lib/auth` */
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
[IdentityProvider.CAL]: "Cal",
[IdentityProvider.GOOGLE]: "Google",
Expand Down
Loading