Skip to content

Commit

Permalink
mfa password reset required journey (#1293)
Browse files Browse the repository at this point in the history
  • Loading branch information
mark-cab authored Feb 1, 2024
1 parent 383df0c commit c4419d9
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 92 deletions.
46 changes: 1 addition & 45 deletions src/components/common/state-machine/state-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,51 +588,7 @@ const authStateMachine = createMachine(
],
},
},
[PATH_NAMES.RESET_PASSWORD_REQUIRED]: {
on: {
[USER_JOURNEY_EVENTS.PASSWORD_CREATED]: [
{
target: [PATH_NAMES.GET_SECURITY_CODES],
cond: "isAccountPartCreated",
},
{
target: [PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE],
cond: "requiresMFAAuthAppCode",
},
{ target: [PATH_NAMES.ENTER_MFA], cond: "requiresTwoFactorAuth" },
{
target: [PATH_NAMES.UPDATED_TERMS_AND_CONDITIONS],
cond: "isLatestTermsAndConditionsAccepted",
},
{
target: [PATH_NAMES.SHARE_INFO],
cond: "isConsentRequired",
},
{ target: [PATH_NAMES.AUTH_CODE] },
],
},
[PATH_NAMES.CREATE_ACCOUNT_ENTER_PHONE_NUMBER]: {
on: {
[USER_JOURNEY_EVENTS.VERIFY_PHONE_NUMBER]: [
PATH_NAMES.CHECK_YOUR_PHONE,
],
},
meta: {
optionalPaths: [
PATH_NAMES.SECURITY_CODE_WAIT,
PATH_NAMES.SECURITY_CODE_INVALID,
PATH_NAMES.SECURITY_CODE_REQUEST_EXCEEDED,
],
},
},
meta: {
optionalPaths: [
PATH_NAMES.ENTER_EMAIL_SIGN_IN,
PATH_NAMES.ACCOUNT_LOCKED,
PATH_NAMES.SIGN_IN_OR_CREATE,
],
},
},
[PATH_NAMES.RESET_PASSWORD_REQUIRED]: {},
[PATH_NAMES.PROVE_IDENTITY]: {
on: {
[USER_JOURNEY_EVENTS.PROVE_IDENTITY_INIT]: [
Expand Down
102 changes: 55 additions & 47 deletions src/components/reset-password/reset-password-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,59 +79,67 @@ export function resetPasswordPost(
);
}
}

const loginResponse = await loginService.loginUser(
sessionId,
email,
newPassword,
clientSessionId,
req.ip,
persistentSessionId
);

if (!loginResponse.success) {
throw new BadRequestError(
loginResponse.data.message,
loginResponse.data.code
);
}

req.session.user.redactedPhoneNumber =
loginResponse.data.redactedPhoneNumber;
req.session.user.isConsentRequired = loginResponse.data.consentRequired;
req.session.user.isLatestTermsAndConditionsAccepted =
loginResponse.data.latestTermsAndConditionsAccepted;
req.session.user.isAccountPartCreated =
!loginResponse.data.mfaMethodVerified;
if (req.session.user.isPasswordChangeRequired) {
req.session.user.isPasswordChangeRequired = false;
}

if (
!support2FABeforePasswordReset() &&
loginResponse.data.mfaMethodVerified &&
loginResponse.data.mfaMethodType === MFA_METHOD_TYPE.SMS
) {
const mfaResponse = await mfaCodeService.sendMfaCode(
let mfaMethodType;
let isMfaMethodVerified;
if (support2FABeforePasswordReset && req.session.user.isAuthenticated) {
mfaMethodType = req.session.user.accountRecoveryVerifiedMfaType;
isMfaMethodVerified = !req.session.user.isAccountPartCreated;
} else {
const loginResponse = await loginService.loginUser(
sessionId,
clientSessionId,
email,
newPassword,
clientSessionId,
req.ip,
persistentSessionId,
false,
xss(req.cookies.lng as string)
persistentSessionId
);

if (!mfaResponse.success) {
const path = getErrorPathByCode(mfaResponse.data.code);
if (path) {
return res.redirect(path);
}
if (!loginResponse.success) {
throw new BadRequestError(
mfaResponse.data.message,
mfaResponse.data.code
loginResponse.data.message,
loginResponse.data.code
);
}

req.session.user.redactedPhoneNumber =
loginResponse.data.redactedPhoneNumber;
req.session.user.isConsentRequired = loginResponse.data.consentRequired;
req.session.user.isLatestTermsAndConditionsAccepted =
loginResponse.data.latestTermsAndConditionsAccepted;
req.session.user.isAccountPartCreated =
!loginResponse.data.mfaMethodVerified;
if (req.session.user.isPasswordChangeRequired) {
req.session.user.isPasswordChangeRequired = false;
}

if (
!support2FABeforePasswordReset() &&
loginResponse.data.mfaMethodVerified &&
loginResponse.data.mfaMethodType === MFA_METHOD_TYPE.SMS
) {
const mfaResponse = await mfaCodeService.sendMfaCode(
sessionId,
clientSessionId,
email,
req.ip,
persistentSessionId,
false,
xss(req.cookies.lng as string)
);

if (!mfaResponse.success) {
const path = getErrorPathByCode(mfaResponse.data.code);
if (path) {
return res.redirect(path);
}
throw new BadRequestError(
mfaResponse.data.message,
mfaResponse.data.code
);
}
}
mfaMethodType = loginResponse.data.mfaMethodType;
isMfaMethodVerified = loginResponse.data.mfaMethodVerified;
}

return res.redirect(
Expand All @@ -144,8 +152,8 @@ export function resetPasswordPost(
requiresTwoFactorAuth: !support2FABeforePasswordReset(),
isLatestTermsAndConditionsAccepted:
req.session.user.isLatestTermsAndConditionsAccepted,
mfaMethodType: loginResponse.data.mfaMethodType,
isMfaMethodVerified: loginResponse.data.mfaMethodVerified,
mfaMethodType: mfaMethodType,
isMfaMethodVerified: isMfaMethodVerified,
support2FABeforePasswordReset: support2FABeforePasswordReset(),
},
res.locals.sessionId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,50 @@ describe("reset password controller (in 6 digit code flow)", () => {

expect(res.redirect).to.have.calledWith(PATH_NAMES.ENTER_MFA);
});

it("should not request 2fa and not login user when user already logged in", async () => {
process.env.SUPPORT_2FA_B4_PASSWORD_RESET = "1";
const fakeResetService: ResetPasswordServiceInterface = {
updatePassword: sinon.fake.returns({ success: true }),
} as unknown as ResetPasswordServiceInterface;
const fakeLoginService: EnterPasswordServiceInterface = {
loginUser: sinon.fake.returns({
success: true,
data: {
redactedPhoneNumber: "1234",
consentRequired: false,
latestTermsAndConditionsAccepted: true,
mfaMethodVerified: true,
mfaRequired: false,
mfaMethodType: MFA_METHOD_TYPE.SMS,
passwordChangeRequired: params.passwordChangeRequired,
},
}),
} as unknown as EnterPasswordServiceInterface;
fakeLoginService.loginUser;
const fakeMfAService: MfaServiceInterface = {
sendMfaCode: sinon.fake.returns({ success: true }),
} as unknown as MfaServiceInterface;

req.session.user = {
email: "[email protected]",
isAuthenticated: true,
isAccountPartCreated: false,
accountRecoveryVerifiedMfaType: MFA_METHOD_TYPE.SMS,
};
req.body.password = "Password1";

await resetPasswordPost(
fakeResetService,
fakeLoginService,
fakeMfAService
)(req as Request, res as Response);

expect(fakeResetService.updatePassword).to.have.been.calledOnce;
expect(fakeLoginService.loginUser).to.not.have.been.called;
expect(fakeMfAService.sendMfaCode).to.not.have.been.called;

expect(res.redirect).to.have.calledWith(PATH_NAMES.AUTH_CODE);
});
});
});
Loading

0 comments on commit c4419d9

Please sign in to comment.