Skip to content

Commit

Permalink
Merge pull request #2460 from govuk-one-login/AUT-3920/return-to-otp
Browse files Browse the repository at this point in the history
AUT-3920: Redirect back to enter mfa pages
  • Loading branch information
alhcomer authored Jan 14, 2025
2 parents 00d7401 + de58f9c commit fdc5f94
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 74 deletions.
5 changes: 5 additions & 0 deletions src/app.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,8 @@ export const WEB_TO_MOBILE_ERROR_MESSAGE_MAPPINGS: Record<string, string> = {
"pages.reEnterEmailAccount.enterYourEmailAddressError":
"mobileAppPages.reEnterEmailAccount.enterYourEmailAddressError",
};

export const CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION = {
HELP_DELETE_ACCOUNT: "help-to-delete-account",
RETRY_SECURITY_CODE: "retry-security-code",
};
14 changes: 12 additions & 2 deletions src/components/common/state-machine/state-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,10 +761,20 @@ const authStateMachine = createMachine(
},
},
[PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES]: {
type: "final",
on: {
[USER_JOURNEY_EVENTS.VERIFY_MFA]: [PATH_NAMES.ENTER_MFA],
[USER_JOURNEY_EVENTS.VERIFY_AUTH_APP_CODE]: [
PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE,
],
},
},
[PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES_IDENTITY_FAIL]: {
type: "final",
on: {
[USER_JOURNEY_EVENTS.VERIFY_MFA]: [PATH_NAMES.ENTER_MFA],
[USER_JOURNEY_EVENTS.VERIFY_AUTH_APP_CODE]: [
PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE,
],
},
},
},
},
Expand Down
1 change: 1 addition & 0 deletions src/components/enter-password/enter-password-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export function enterPasswordPost(
req.session.user.isLatestTermsAndConditionsAccepted =
userLogin.data.latestTermsAndConditionsAccepted;
req.session.user.isPasswordChangeRequired = isPasswordChangeRequired;
req.session.user.mfaMethodType = userLogin.data.mfaMethodType;

if (isPasswordChangeRequired && supportAccountInterventions()) {
const accountInterventionsResponse =
Expand Down
32 changes: 28 additions & 4 deletions src/components/ipv-callback/ipv-callback-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { reverificationResultService } from "./reverification-result-service";
import { BadRequestError } from "../../utils/error";
import { getNextPathAndUpdateJourney } from "../common/constants";
import { USER_JOURNEY_EVENTS } from "../common/state-machine/state-machine";
import { PATH_NAMES } from "../../app.constants";
import {
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION,
MFA_METHOD_TYPE,
PATH_NAMES,
} from "../../app.constants";

const ERROR_TO_EVENT_MAP = new Map<string, string>();
ERROR_TO_EVENT_MAP.set(
Expand Down Expand Up @@ -98,9 +102,29 @@ export function cannotChangeSecurityCodesGet(
});
}

export function cannotChangeSecurityCodesPost(
export async function cannotChangeSecurityCodesPost(
req: Request,
res: Response
): void {
res.send("In development");
): Promise<void> {
const { sessionId } = res.locals;
const cannotChangeHowGetSecurityCodeAction =
req.body.cannotChangeHowGetSecurityCodeAction;
if (
cannotChangeHowGetSecurityCodeAction ===
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE
) {
return res.redirect(
await getNextPathAndUpdateJourney(
req,
req.path,
req.session.user.mfaMethodType === MFA_METHOD_TYPE.SMS
? USER_JOURNEY_EVENTS.VERIFY_MFA
: USER_JOURNEY_EVENTS.VERIFY_AUTH_APP_CODE,
{},
sessionId
)
);
} else {
res.send("In development");
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { mockResponse, RequestOutput, ResponseOutput } from "mock-req-res";
import sinon from "sinon";
import { createMockRequest } from "../../../../test/helpers/mock-request-helper";
import { PATH_NAMES } from "../../../app.constants";
import {
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION,
MFA_METHOD_TYPE,
PATH_NAMES,
} from "../../../app.constants";
import { expect } from "chai";
import { Request, Response } from "express";
import {
cannotChangeSecurityCodesGet,
cannotChangeSecurityCodesPost,
ipvCallbackGet,
} from "../ipv-callback-controller";
import {
Expand Down Expand Up @@ -203,5 +208,31 @@ describe("ipv callback controller", () => {
);
});
});

describe("cannotChangeSecurityCodePost", () => {
it("should redirect to enter sms mfa page when sms mfa user selects 'ry entering a security code again with the method you already have set up' radio button", async () => {
req.path = PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES;
req.body.cannotChangeHowGetSecurityCodeAction =
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE;
req.session.user.mfaMethodType = MFA_METHOD_TYPE.SMS;

await cannotChangeSecurityCodesPost(req as Request, res as Response);

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

it("should redirect to enter auth app mfa page when auth app mfa user selects 'ry entering a security code again with the method you already have set up' radio button", async () => {
req.path = PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES;
req.body.cannotChangeHowGetSecurityCodeAction =
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE;
req.session.user.mfaMethodType = MFA_METHOD_TYPE.AUTH_APP;

await cannotChangeSecurityCodesPost(req as Request, res as Response);

expect(res.redirect).to.have.calledWith(
PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE
);
});
});
});
});
191 changes: 124 additions & 67 deletions src/components/ipv-callback/tests/ipv-callback-integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,32 @@
import { describe } from "mocha";
import decache from "decache";
import { expect, request, sinon } from "../../../../test/utils/test-utils";
import { API_ENDPOINTS, PATH_NAMES } from "../../../app.constants";
import {
API_ENDPOINTS,
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION,
MFA_METHOD_TYPE,
PATH_NAMES,
} from "../../../app.constants";
import express from "express";
import nock from "nock";
import * as cheerio from "cheerio";

describe("Integration:: ipv callback", () => {
let app: express.Application;
let baseApi: string;
let sessionMiddleware: any;

before(async () => {
process.env.SUPPORT_MFA_RESET_WITH_IPV = "1";
});

after(() => {
delete process.env.SUPPORT_MFA_RESET_WITH_IPV;
});

describe("ipv callback", () => {
before(async () => {
decache("../../../app");
decache("../../../middleware/session-middleware");
process.env.SUPPORT_MFA_RESET_WITH_IPV = "1";
baseApi = process.env.FRONTEND_API_BASE_URL;
sessionMiddleware = require("../../../middleware/session-middleware");

sinon
.stub(sessionMiddleware, "validateSessionMiddleware")
.callsFake(function (req: any, res: any, next: any): void {
res.locals.sessionId = "tDy103saszhcxbQq0-mjdzU854";

req.session.user = {
email: "[email protected]",
phoneNumber: "7867",
journey: {
nextPath: PATH_NAMES.IPV_CALLBACK,
},
};

next();
});

app = await require("../../../app").createApp();
app = await stubSessionMiddlewareAndCreateApp(PATH_NAMES.IPV_CALLBACK);
});

after(() => {
Expand Down Expand Up @@ -92,53 +79,18 @@ describe("Integration:: ipv callback", () => {
});

describe("cannot change how get security codes", () => {
let token: string | string[];
let cookies: string;

before(async () => {
decache("../../../app");
decache("../../../middleware/session-middleware");
process.env.SUPPORT_MFA_RESET_WITH_IPV = "1";
sessionMiddleware = require("../../../middleware/session-middleware");

sinon
.stub(sessionMiddleware, "validateSessionMiddleware")
.callsFake(function (req: any, res: any, next: any): void {
res.locals.sessionId = "tDy103saszhcxbQq0-mjdzU854";

req.session.user = {
email: "[email protected]",
phoneNumber: "7867",
journey: {
nextPath: PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES,
},
};

next();
});

app = await require("../../../app").createApp();

await request(
app,
(test) => test.get(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES),
{
expectAnalyticsPropertiesMatchSnapshot: false,
}
).then((res) => {
const $ = cheerio.load(res.text);
token = $("[name=_csrf]").val();
cookies = res.headers["set-cookie"];
});
});

after(() => {
afterEach(() => {
app = undefined;
nock.cleanAll();
sinon.restore();
});

it("returns a dummy page when an option is selected", async () => {
const app = await stubSessionMiddlewareAndCreateApp(
PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES
);
const { token, cookies } =
await getCannotChangeSecurityCodesAndReturnTokenAndCookies(app);

await request(
app,
(test) => test.post(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES),
Expand All @@ -150,7 +102,8 @@ describe("Integration:: ipv callback", () => {
.set("Cookie", cookies)
.send({
_csrf: token,
cannotChangeHowGetSecurityCodeAction: "help-to-delete-account",
cannotChangeHowGetSecurityCodeAction:
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.HELP_DELETE_ACCOUNT,
})
.expect(function (res) {
expect(res.text).to.equals("In development");
Expand All @@ -159,6 +112,12 @@ describe("Integration:: ipv callback", () => {
});

it("returns a validation error when no option is selected", async () => {
const app = await stubSessionMiddlewareAndCreateApp(
PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES
);
const { token, cookies } =
await getCannotChangeSecurityCodesAndReturnTokenAndCookies(app);

await request(
app,
(test) => test.post(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES),
Expand All @@ -180,5 +139,103 @@ describe("Integration:: ipv callback", () => {
})
.expect(400);
});

it("goes to /enter-code when user selects retry security code radio button and their mfaMethodType is SMS", async () => {
const app = await stubSessionMiddlewareAndCreateApp(
PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES,
MFA_METHOD_TYPE.SMS
);
const { token, cookies } =
await getCannotChangeSecurityCodesAndReturnTokenAndCookies(app);

await request(
app,
(test) => test.post(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES),
{
expectAnalyticsPropertiesMatchSnapshot: false,
}
)
.type("form")
.set("Cookie", cookies)
.send({
_csrf: token,
cannotChangeHowGetSecurityCodeAction:
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE,
})
.expect("Location", PATH_NAMES.ENTER_MFA)
.expect(302);
});

it("goes to /enter-authenticator-app-code when user selects retry security code radio button and their mfaMethodType is AUTH_APP", async () => {
const app = await stubSessionMiddlewareAndCreateApp(
PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES,
MFA_METHOD_TYPE.AUTH_APP
);
const { token, cookies } =
await getCannotChangeSecurityCodesAndReturnTokenAndCookies(app);

await request(
app,
(test) => test.post(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES),
{
expectAnalyticsPropertiesMatchSnapshot: false,
}
)
.type("form")
.set("Cookie", cookies)
.send({
_csrf: token,
cannotChangeHowGetSecurityCodeAction:
CANNOT_CHANGE_HOW_GET_SECURITY_CODES_ACTION.RETRY_SECURITY_CODE,
})
.expect("Location", PATH_NAMES.ENTER_AUTHENTICATOR_APP_CODE)
.expect(302);
});
});
});

const stubSessionMiddlewareAndCreateApp = async (
nextPath: string,
mfaMethodType?: MFA_METHOD_TYPE
): Promise<express.Application> => {
decache("../../../app");
decache("../../../middleware/session-middleware");
const sessionMiddleware = require("../../../middleware/session-middleware");

sinon
.stub(sessionMiddleware, "validateSessionMiddleware")
.callsFake(function (req: any, res: any, next: any): void {
res.locals.sessionId = "tDy103saszhcxbQq0-mjdzU854";

req.session.user = {
email: "[email protected]",
phoneNumber: "7867",
journey: {
nextPath: nextPath,
},
mfaMethodType: mfaMethodType,
};

next();
});

return await require("../../../app").createApp();
};

const getCannotChangeSecurityCodesAndReturnTokenAndCookies = async (
app: express.Application
) => {
let cookies, token;
await request(
app,
(test) => test.get(PATH_NAMES.CANNOT_CHANGE_SECURITY_CODES),
{
expectAnalyticsPropertiesMatchSnapshot: false,
}
).then((res) => {
const $ = cheerio.load(res.text);
token = $("[name=_csrf]").val();
cookies = res.headers["set-cookie"];
});
return { token, cookies };
};
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface UserSession {
isSignInJourney?: boolean;
isVerifyEmailCodeResendRequired?: boolean;
channel?: string;
mfaMethodType?: string;
}

export interface UserSessionClient {
Expand Down

0 comments on commit fdc5f94

Please sign in to comment.