Skip to content

Commit

Permalink
OLH-1849 Call the new API when a user adds a phone number as a backup…
Browse files Browse the repository at this point in the history
… MFA method
  • Loading branch information
aenciso committed Jun 21, 2024
1 parent cb2812a commit b9a6e90
Show file tree
Hide file tree
Showing 17 changed files with 205 additions and 90 deletions.
2 changes: 1 addition & 1 deletion deploy/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Mappings:
GOOGLEANALYTICS4GTMCONTAINERID: "GTM-KD86CMZ"
GA4DISABLED: "false"
UADISABLED: "false"
SUPPORTADDBACKUPMFA: "0"
SUPPORTADDBACKUPMFA: "1"
SUPPORTCHANGEMFA: "1"
ACCESSIBILITYSTATEMENTURL: "https://signin.account.gov.uk/accessibility-statement"
LANGUAGETOGGLE: "1"
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: "3.8"
services:
localstack:
container_name: localstack
Expand Down Expand Up @@ -64,6 +63,7 @@ services:
- ENVIRONMENT=${ENVIRONMENT}
- VERIFY_ACCESS_VALUE=${VERIFY_ACCESS_VALUE}
- METHOD_MANAGEMENT_BASE_URL=${METHOD_MANAGEMENT_BASE_URL}
- SUPPORT_ADD_BACKUP_MFA=${SUPPORT_ADD_BACKUP_MFA}
- SUPPORT_METHOD_MANAGEMENT=${SUPPORT_METHOD_MANAGEMENT}
- SUPPORT_CHANGE_MFA=${SUPPORT_CHANGE_MFA}
- ACCESSIBILITY_STATEMENT_URL=${ACCESSIBILITY_STATEMENT_URL}
Expand Down
2 changes: 1 addition & 1 deletion local.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ WORKDIR /app

EXPOSE $PORT

CMD npm install && npm run copy-assets && npm run dev
CMD npm install && npm run copy-assets && npm run build-js:analytics && npm run dev
32 changes: 20 additions & 12 deletions src/components/add-mfa-method-app/add-mfa-method-app-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import assert from "node:assert";
import { formatValidationError } from "../../utils/validation";
import { EventType, getNextState } from "../../utils/state-machine";
import { renderMfaMethodPage } from "../common/mfa";
import { getTxmaHeader } from "../../utils/txma-header";

const ADD_MFA_METHOD_AUTH_APP_TEMPLATE = "add-mfa-method-app/index.njk";

Expand Down Expand Up @@ -67,19 +68,26 @@ export async function addMfaAppMethodPost(
);
}

const { status } = await addMfaMethod({
email: req.session.user.email,
otp: code,
credential: authAppSecret,
mfaMethod: {
priorityIdentifier: "SECONDARY",
mfaMethodType: "AUTH_APP",
const { status } = await addMfaMethod(
{
email: req.session.user.email,
otp: code,
credential: authAppSecret,
mfaMethod: {
priorityIdentifier: "SECONDARY",
mfaMethodType: "AUTH_APP",
},
},
accessToken: req.session.user.tokens.accessToken,
sourceIp: req.ip,
sessionId: req.session.id,
persistentSessionId: res.locals.persistentSessionId,
});
{
accessToken: req.session.user.tokens.accessToken,
sourceIp: req.ip,
sessionId: req.session.id,
persistentSessionId: res.locals.persistentSessionId,
userLanguage: req.cookies.lng as string,
clientSessionId: res.locals.clientSessionId,
txmaAuditEncoded: getTxmaHeader(req, res.locals.trace),
}
);

if (status !== HTTP_STATUS_CODES.OK) {
throw Error(`Failed to add MFA method, response status: ${status}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ describe("addMfaAppMethodPost", () => {

it("should redirect to add mfa app confirmation page", async () => {
const req = {
headers: {
"txma-audit-encoded": "txma-audit-encoded",
},
body: {
code: "123456",
authAppSecret: "A".repeat(20),
Expand All @@ -98,10 +101,15 @@ describe("addMfaAppMethodPost", () => {
log: { error: sinon.fake() },
ip: "127.0.0.1",
t: (t: string) => t,
cookies: {
lng: "en",
},
};
const res = {
locals: {
persistentSessionId: "persistentSessionId",
clientSessionId: "clientSessionId",
trace: "trace",
},
redirect: sandbox.fake(() => {}),
};
Expand Down Expand Up @@ -132,16 +140,26 @@ describe("addMfaAppMethodPost", () => {
next
);

expect(addMfaMethod).to.have.been.calledWith({
email: "test@test.com",
otp: "123456",
credential: "AAAAAAAAAAAAAAAAAAAA",
mfaMethod: { priorityIdentifier: "SECONDARY", mfaMethodType: "AUTH_APP" },
accessToken: "token",
sourceIp: "127.0.0.1",
sessionId: "session_id",
persistentSessionId: "persistentSessionId",
});
expect(addMfaMethod).to.have.been.calledWith(
{
email: "test@test.com",
otp: "123456",
credential: "AAAAAAAAAAAAAAAAAAAA",
mfaMethod: {
priorityIdentifier: "SECONDARY",
mfaMethodType: "AUTH_APP",
},
},
{
accessToken: "token",
sourceIp: "127.0.0.1",
sessionId: "session_id",
persistentSessionId: "persistentSessionId",
userLanguage: "en",
clientSessionId: "clientSessionId",
txmaAuditEncoded: "txma-audit-encoded",
}
);

expect(res.redirect).to.have.been.calledWith(
PATH_DATA.ADD_MFA_METHOD_APP_CONFIRMATION.url
Expand Down
84 changes: 68 additions & 16 deletions src/components/add-mfa-method-sms/add-mfa-method-sms-controller.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,87 @@
import { Request, Response } from "express";
import { PATH_DATA } from "../../app.constants";
import { ERROR_CODES, PATH_DATA } from "../../app.constants";
import {
convertInternationalPhoneNumberToE164Format,
getLastNDigits,
} from "../../utils/phone-number";
import { EventType, getNextState } from "../../utils/state-machine";
import xss from "xss";
import { getTxmaHeader } from "../../utils/txma-header";
import { ChangePhoneNumberServiceInterface } from "../change-phone-number/types";
import { changePhoneNumberService } from "../change-phone-number/change-phone-number-service";
import {
formatValidationError,
renderBadRequest,
} from "../../utils/validation";
import { BadRequestError } from "../../utils/errors";

const CHANGE_PHONE_NUMBER_TEMPLATE = "add-mfa-method-sms/index.njk";

export async function addMfaSmsMethodGet(
req: Request,
res: Response
): Promise<void> {
res.render("add-mfa-method-sms/index.njk");
}
export function addMfaSmsMethodPost(
service: ChangePhoneNumberServiceInterface = changePhoneNumberService()
) {
return async function (req: Request, res: Response): Promise<void> {
const { email } = req.session.user;
const { accessToken } = req.session.user.tokens;
const hasInternationalPhoneNumber = req.body.hasInternationalPhoneNumber;
let newPhoneNumber;

export async function addMfaSmsMethodPost(
req: Request,
res: Response
): Promise<void> {
//TODO do something with this
req.session.user.state.changePhoneNumber = getNextState(
req.session.user.state.addMfaMethod.value,
EventType.VerifyCodeSent
);

req.session.user.newPhoneNumber = req.body.hasInternationalPhoneNumber
? convertInternationalPhoneNumberToE164Format(
if (hasInternationalPhoneNumber === "true") {
newPhoneNumber = convertInternationalPhoneNumberToE164Format(
req.body.internationalPhoneNumber
)
: req.body.ukPhoneNumber;
);
} else {
newPhoneNumber = req.body.ukPhoneNumber;
}

const response = await service.sendPhoneVerificationNotification(
accessToken,
email,
newPhoneNumber,
req.ip,
res.locals.sessionId,
res.locals.persistentSessionId,
xss(req.cookies.lng as string),
res.locals.clientSessionId,
getTxmaHeader(req, res.locals.trace)
);

if (response.success) {
req.session.user.newPhoneNumber = newPhoneNumber;

req.session.user.state.changePhoneNumber = getNextState(
req.session.user.state.addMfaMethod.value,
EventType.VerifyCodeSent
);

return res.redirect(
`${PATH_DATA.CHECK_YOUR_PHONE.url}?intent=addMfaMethod`
);
}

if (response.code === ERROR_CODES.NEW_PHONE_NUMBER_SAME_AS_EXISTING) {
const href: string =
hasInternationalPhoneNumber && hasInternationalPhoneNumber === "true"
? "internationalPhoneNumber"
: "phoneNumber";

res.redirect(`${PATH_DATA.CHECK_YOUR_PHONE.url}?intent=addMfaMethod`);
const error = formatValidationError(
href,
req.t(
"pages.changePhoneNumber.ukPhoneNumber.validationError.samePhoneNumber"
)
);
return renderBadRequest(res, req, CHANGE_PHONE_NUMBER_TEMPLATE, error);
} else {
throw new BadRequestError(response.message, response.code);
}
};
}

export async function addMfaAppMethodConfirmationGet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
addMfaSmsMethodGet,
addMfaSmsMethodPost,
} from "./add-mfa-method-sms-controller";
import { asyncHandler } from "../../utils/async";

const router = express.Router();

Expand All @@ -21,7 +22,7 @@ router.post(
PATH_DATA.ADD_MFA_METHOD_SMS.url,
requiresAuthMiddleware,
validateStateMiddleware,
addMfaSmsMethodPost
asyncHandler(addMfaSmsMethodPost())
);

router.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "../../../../test/utils/builders";
import { addMfaSmsMethodPost } from "../add-mfa-method-sms-controller";
import { PATH_DATA } from "../../../app.constants";
import { ChangePhoneNumberServiceInterface } from "../../change-phone-number/types";

describe("add sms mfa method controller", () => {
let sandbox: sinon.SinonSandbox;
Expand Down Expand Up @@ -37,9 +38,14 @@ describe("add sms mfa method controller", () => {
sandbox.restore();
});

it("should redirect the user to the check phone page", () => {
it("should redirect the user to the check phone page", async () => {
const fakeService: ChangePhoneNumberServiceInterface = {
sendPhoneVerificationNotification: sandbox.fake.resolves({
success: true,
}),
};
req.body.ukPhoneNumber = "1234";
addMfaSmsMethodPost(req as Request, res as Response);
await addMfaSmsMethodPost(fakeService)(req as Request, res as Response);
expect(redirect).to.be.calledWith(
`${PATH_DATA.CHECK_YOUR_PHONE.url}?intent=addMfaMethod`
);
Expand Down
22 changes: 20 additions & 2 deletions src/components/check-your-phone/check-your-phone-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,26 @@ export function checkYourPhonePost(
throw Error(`No existing MFA method for: ${email}`);
}
} else if (intent === "addMfaMethod") {
// TODO add MFA method here
isPhoneNumberUpdated = true;
const smsMFAMethod: MfaMethod = req.session.mfaMethods.find(
(mfa) => mfa.priorityIdentifier === "PRIMARY"
);
if (smsMFAMethod) {
smsMFAMethod.endPoint = newPhoneNumber;
updateInput.credential = "";
updateInput.mfaMethod = {
...smsMFAMethod,
mfaIdentifier: smsMFAMethod.mfaIdentifier + 1,
priorityIdentifier: "SECONDARY",
mfaMethodType:
smsMFAMethod.mfaMethodType === "SMS" ? "AUTH_APP" : "SMS",
endPoint: newPhoneNumber,
methodVerified: true,
};
isPhoneNumberUpdated = await service.addMfaMethodService(
updateInput,
sessionDetails
);
}
} else {
throw Error(`Unknown phone verification intent ${intent}`);
}
Expand Down
10 changes: 9 additions & 1 deletion src/components/check-your-phone/check-your-phone-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
UpdateInformationInput,
UpdateInformationSessionValues,
} from "../../utils/types";
import { updateMfaMethod } from "../../utils/mfa";
import { createOrUpdateMfaMethod, updateMfaMethod } from "../../utils/mfa";

export function checkYourPhoneService(
axios: Http = http
Expand Down Expand Up @@ -41,8 +41,16 @@ export function checkYourPhoneService(
return updateMfaMethod(updateInput, sessionDetails);
};

const addMfaMethodService = async function (
updateInput: UpdateInformationInput,
sessionDetails: UpdateInformationSessionValues
): Promise<boolean> {
return createOrUpdateMfaMethod(updateInput, sessionDetails);
};

return {
updatePhoneNumber,
updatePhoneNumberWithMfaApi,
addMfaMethodService,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe("check your phone controller", () => {
const fakeService: CheckYourPhoneServiceInterface = {
updatePhoneNumber: sandbox.fake.resolves(true),
updatePhoneNumberWithMfaApi: sandbox.fake.resolves(true),
addMfaMethodService: sandbox.fake.resolves(true),
};

req.session.user.tokens = { accessToken: "token" } as any;
Expand All @@ -82,6 +83,7 @@ describe("check your phone controller", () => {
const fakeService: CheckYourPhoneServiceInterface = {
updatePhoneNumber: sandbox.fake.resolves(false),
updatePhoneNumberWithMfaApi: sandbox.fake.resolves(false),
addMfaMethodService: sandbox.fake.resolves(false),
};

req.session.user.tokens = { accessToken: "token" } as any;
Expand All @@ -102,6 +104,7 @@ describe("check your phone controller", () => {
const fakeService: CheckYourPhoneServiceInterface = {
updatePhoneNumber: sandbox.fake.resolves(true),
updatePhoneNumberWithMfaApi: sandbox.fake.resolves(true),
addMfaMethodService: sandbox.fake.resolves(true),
};

req.session.user.tokens = { accessToken: "token" } as any;
Expand Down
5 changes: 5 additions & 0 deletions src/components/check-your-phone/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ export interface CheckYourPhoneServiceInterface {
updateInput: UpdateInformationInput,
sessionDetails: UpdateInformationSessionValues
) => Promise<boolean>;

addMfaMethodService: (
updateInput: UpdateInformationInput,
sessionDetails: UpdateInformationSessionValues
) => Promise<boolean>;
}
4 changes: 2 additions & 2 deletions src/components/common/layout/base.njk
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
url: currentUrl,
activeLanguage: htmlLang,
languages: [
{
{
code: 'en',
text: 'English',
visuallyHidden: 'Change to English'
Expand All @@ -67,7 +67,7 @@
}]
})
}}
{% endif %}
{% endif %}
{% if backLink %}
<a href="{{backLink}}" class="govuk-back-link js-back-link">
{{ backLinkText }}
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@
"step1": {
"title": "Enter your new mobile phone number",
"message": "We will send a 6 digit security code to the number you give us",
"uk_mobile": "UK moble phone number",
"uk_mobile": "UK mobile phone number",
"no_uk_mobile": "I do not have a UK mobile number",
"intl_mobile_phone_number": "Mobile phone number",
"intl_mobile_phone_number_hint": "Include the country code, for example +33 for France"
Expand Down
Loading

0 comments on commit b9a6e90

Please sign in to comment.