Skip to content

Commit

Permalink
chore: TwoFa restriction after multiple failed attempts before login (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Riddhiagrawal001 authored Oct 24, 2024
1 parent 2c31177 commit 9ff488b
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let make = () => {
| MERCHANT_SELECT
| ACCEPT_INVITE =>
<ListInvitationScreen />
| TOTP => <TotpSetup />
| TOTP => <TwoFaLanding />
| FORCE_SET_PASSWORD
| RESET_PASSWORD =>
<ResetPassword flowType />
Expand Down
34 changes: 22 additions & 12 deletions src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ let p3Regular = HSwitchUtils.getTextClass((P3, Regular))

module EnterAccessCode = {
@react.component
let make = (~setTwoFaPageState, ~onClickVerifyAccessCode) => {
let make = (~setTwoFaPageState, ~onClickVerifyAccessCode, ~errorHandling) => {
let showToast = ToastState.useShowToast()
let verifyRecoveryCodeLogic = TotpHooks.useVerifyRecoveryCode()
let (recoveryCode, setRecoveryCode) = React.useState(_ => "")
let (buttonState, setButtonState) = React.useState(_ => Button.Normal)

let verifyAccessCode = async () => {
let verifyAccessCode = async _ => {
open LogicUtils
try {
open LogicUtils

setButtonState(_ => Button.Loading)

if recoveryCode->String.length > 0 {
Expand All @@ -25,7 +24,12 @@ module EnterAccessCode = {
}
setButtonState(_ => Button.Normal)
} catch {
| _ => {
| Exn.Error(e) => {
let err = Exn.message(e)->Option.getOr("Something went wrong")
let errorCode = err->safeParse->getDictFromJsonObject->getString("code", "")
if errorCode == "UR_49" {
errorHandling(errorCode)
}
setRecoveryCode(_ => "")
setButtonState(_ => Button.Normal)
}
Expand Down Expand Up @@ -105,6 +109,7 @@ module ConfigureTotpScreen = {
~twoFaStatus,
~setTwoFaPageState,
~terminateTwoFactorAuth,
~errorHandling,
) => {
open TwoFaTypes

Expand All @@ -115,9 +120,8 @@ module ConfigureTotpScreen = {
let (buttonState, setButtonState) = React.useState(_ => Button.Normal)

let verifyTOTP = async () => {
open LogicUtils
try {
open LogicUtils

setButtonState(_ => Button.Loading)

if otp->String.length > 0 {
Expand All @@ -135,7 +139,12 @@ module ConfigureTotpScreen = {
}
setButtonState(_ => Button.Normal)
} catch {
| _ => {
| Exn.Error(e) => {
let err = Exn.message(e)->Option.getOr("Something went wrong")
let errorCode = err->safeParse->getDictFromJsonObject->getString("code", "")
if errorCode == "UR_48" {
errorHandling(errorCode)
}
setOtp(_ => "")
setButtonState(_ => Button.Normal)
}
Expand Down Expand Up @@ -225,7 +234,7 @@ module ConfigureTotpScreen = {
}

@react.component
let make = () => {
let make = (~setTwoFaPageState, ~twoFaPageState, ~errorHandling) => {
open HSwitchUtils
open TwoFaTypes

Expand All @@ -238,7 +247,6 @@ let make = () => {
let (isQrVisible, setIsQrVisible) = React.useState(_ => false)
let (totpUrl, setTotpUrl) = React.useState(_ => "")
let (twoFaStatus, setTwoFaStatus) = React.useState(_ => TWO_FA_NOT_SET)
let (twoFaPageState, setTwoFaPageState) = React.useState(_ => TOTP_SHOW_QR)
let (showNewQR, setShowNewQR) = React.useState(_ => false)

let delayTimer = () => {
Expand Down Expand Up @@ -325,14 +333,16 @@ let make = () => {
{switch twoFaPageState {
| TOTP_SHOW_QR =>
<ConfigureTotpScreen
isQrVisible totpUrl twoFaStatus setTwoFaPageState terminateTwoFactorAuth
isQrVisible totpUrl twoFaStatus setTwoFaPageState terminateTwoFactorAuth errorHandling
/>
| TOTP_SHOW_RC =>
<TotpRecoveryCodes
setTwoFaPageState onClickDownload={terminateTwoFactorAuth} setShowNewQR
/>
| TOTP_INPUT_RECOVERY_CODE =>
<EnterAccessCode setTwoFaPageState onClickVerifyAccessCode={terminateTwoFactorAuth} />
<EnterAccessCode
setTwoFaPageState onClickVerifyAccessCode={terminateTwoFactorAuth} errorHandling
/>
}}
<div className="text-grey-200 flex gap-2">
{"Log in with a different account?"->React.string}
Expand Down
126 changes: 126 additions & 0 deletions src/entryPoints/AuthModule/TwoFaAuth/TwoFaLanding.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
let p2Regular = HSwitchUtils.getTextClass((P2, Regular))

module AttemptsExpiredComponent = {
@react.component
let make = (~expiredType, ~setTwoFaPageState, ~setTwoFaStatus) => {
open TwoFaTypes
open HSwitchUtils
let {setAuthStatus} = React.useContext(AuthInfoProvider.authStatusContext)

let belowComponent = switch expiredType {
| TOTP_ATTEMPTS_EXPIRED =>
<p className={`${p2Regular} text-jp-gray-700`}>
{"or "->React.string}
<span
className="cursor-pointer underline underline-offset-2 text-blue-600"
onClick={_ => {
setTwoFaStatus(_ => TwoFaNotExpired)
setTwoFaPageState(_ => TOTP_INPUT_RECOVERY_CODE)
}}>
{"Use recovery-code"->React.string}
</span>
</p>
| RC_ATTEMPTS_EXPIRED =>
<p className={`${p2Regular} text-jp-gray-700`}>
{"or "->React.string}
<span
className="cursor-pointer underline underline-offset-2 text-blue-600"
onClick={_ => {
setTwoFaStatus(_ => TwoFaNotExpired)
setTwoFaPageState(_ => TOTP_SHOW_QR)
}}>
{"Use totp"->React.string}
</span>
</p>
| TWO_FA_EXPIRED => React.null
}

<BackgroundImageWrapper>
<div className="h-full w-full flex flex-col gap-4 items-center justify-center p-6 ">
<div
className="bg-white px-6 py-12 rounded-md border w-1/3 text-center font-semibold flex flex-col gap-4">
<p>
{"There have been multiple unsuccessful sign-in attempts for this account. Please wait a moment before trying again."->React.string}
</p>
{belowComponent}
</div>
<div className="text-grey-200 flex gap-2">
{"Log in with a different account?"->React.string}
<p
className="underline cursor-pointer underline-offset-2 hover:text-blue-700"
onClick={_ => setAuthStatus(LoggedOut)}>
{"Click here to log out."->React.string}
</p>
</div>
</div>
</BackgroundImageWrapper>
}
}

@react.component
let make = () => {
open TwoFaTypes
let getURL = APIUtils.useGetURL()
let fetchDetails = APIUtils.useGetMethod()
let {setAuthStatus} = React.useContext(AuthInfoProvider.authStatusContext)
let (twoFaStatus, setTwoFaStatus) = React.useState(_ => TwoFaNotExpired)
let (twoFaPageState, setTwoFaPageState) = React.useState(_ => TOTP_SHOW_QR)
let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Loading)

let handlePageBasedOnAttempts = responseDict => {
switch responseDict {
| Some(value) =>
if value.totp.attemptsRemaining > 0 && value.recoveryCode.attemptsRemaining > 0 {
setTwoFaStatus(_ => TwoFaNotExpired)
setTwoFaPageState(_ => TOTP_SHOW_QR)
} else if value.totp.attemptsRemaining == 0 && value.recoveryCode.attemptsRemaining == 0 {
setTwoFaStatus(_ => TwoFaExpired(TWO_FA_EXPIRED))
} else if value.totp.attemptsRemaining == 0 {
setTwoFaStatus(_ => TwoFaExpired(TOTP_ATTEMPTS_EXPIRED))
} else if value.recoveryCode.attemptsRemaining == 0 {
setTwoFaStatus(_ => TwoFaExpired(RC_ATTEMPTS_EXPIRED))
}
| None => setTwoFaStatus(_ => TwoFaNotExpired)
}
}

let checkTwofaStatus = async () => {
try {
let url = getURL(
~entityName=USERS,
~userType=#CHECK_TWO_FACTOR_AUTH_STATUS_V2,
~methodType=Get,
)
let response = await fetchDetails(url)
let responseDict = response->TwoFaUtils.jsonTocheckTwofaResponseType
handlePageBasedOnAttempts(responseDict.status)
setScreenState(_ => PageLoaderWrapper.Success)
} catch {
| _ => {
setScreenState(_ => PageLoaderWrapper.Error("Failed to fetch!"))
setAuthStatus(LoggedOut)
}
}
}

let errorHandling = errorCode => {
if errorCode == "UR_48" {
setTwoFaStatus(_ => TwoFaExpired(TOTP_ATTEMPTS_EXPIRED))
} else if errorCode == "UR_49" {
setTwoFaStatus(_ => TwoFaExpired(RC_ATTEMPTS_EXPIRED))
}
}

React.useEffect(() => {
checkTwofaStatus()->ignore
None
}, [])

<PageLoaderWrapper screenState sectionHeight="h-screen">
{switch twoFaStatus {
| TwoFaExpired(expiredType) =>
<AttemptsExpiredComponent expiredType setTwoFaPageState setTwoFaStatus />
| TwoFaNotExpired => <TotpSetup twoFaPageState setTwoFaPageState errorHandling />
}}
</PageLoaderWrapper>
}
24 changes: 23 additions & 1 deletion src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
type twoFaPageState = TOTP_SHOW_QR | TOTP_SHOW_RC | TOTP_INPUT_RECOVERY_CODE
type twoFaPageState =
| TOTP_SHOW_QR
| TOTP_SHOW_RC
| TOTP_INPUT_RECOVERY_CODE

type twoFaStatus = TWO_FA_NOT_SET | TWO_FA_SET

type twoFaValueType = {
isCompleted: bool,
attemptsRemaining: int,
}

type twoFatype = {
totp: twoFaValueType,
recoveryCode: twoFaValueType,
}

type checkTwofaResponseType = {status: option<twoFatype>}

type expiredTypes =
| TOTP_ATTEMPTS_EXPIRED
| RC_ATTEMPTS_EXPIRED
| TWO_FA_EXPIRED

type twoFaStatusType = TwoFaExpired(expiredTypes) | TwoFaNotExpired
30 changes: 30 additions & 0 deletions src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,33 @@ let downloadRecoveryCodes = (~recoveryCodes) => {
~content=JSON.stringifyWithIndent(recoveryCodes->getJsonFromArrayOfString, 3),
)
}

let jsonToTwoFaValueType: Dict.t<'a> => TwoFaTypes.twoFaValueType = dict => {
open LogicUtils

{
isCompleted: dict->getBool("is_completed", false),
attemptsRemaining: dict->getInt("remaining_attempts", 4),
}
}

let jsonTocheckTwofaResponseType: JSON.t => TwoFaTypes.checkTwofaResponseType = json => {
open LogicUtils
let jsonToDict = json->getDictFromJsonObject->Dict.get("status")

let statusValue = switch jsonToDict {
| Some(json) => {
let dict = json->getDictFromJsonObject
let twoFaValue: TwoFaTypes.twoFatype = {
totp: dict->getDictfromDict("totp")->jsonToTwoFaValueType,
recoveryCode: dict->getDictfromDict("recovery_code")->jsonToTwoFaValueType,
}
Some(twoFaValue)
}
| None => None
}

{
status: statusValue,
}
}
1 change: 1 addition & 0 deletions src/screens/APIUtils/APIUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@ let useGetURL = () => {

// SPT FLOWS (Totp)
| #BEGIN_TOTP => `${userUrl}/2fa/totp/begin`
| #CHECK_TWO_FACTOR_AUTH_STATUS_V2 => `${userUrl}/2fa/v2`
| #VERIFY_TOTP => `${userUrl}/2fa/totp/verify`
| #VERIFY_RECOVERY_CODE => `${userUrl}/2fa/recovery_code/verify`
| #GENERATE_RECOVERY_CODES => `${userUrl}/2fa/recovery_code/generate`
Expand Down
1 change: 1 addition & 0 deletions src/screens/APIUtils/APIUtilsTypes.res
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ type userType = [
| #USER_DETAILS
| #LIST_ROLES_FOR_ROLE_UPDATE
| #ACCEPT_INVITATION_HOME
| #CHECK_TWO_FACTOR_AUTH_STATUS_V2
| #NONE
]

Expand Down

0 comments on commit 9ff488b

Please sign in to comment.