diff --git a/deploy/template.yaml b/deploy/template.yaml index 6077aa2c4..ccdb195a5 100644 --- a/deploy/template.yaml +++ b/deploy/template.yaml @@ -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" diff --git a/docker-compose.yml b/docker-compose.yml index 23f005f1e..bc7c2bbc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.8" services: localstack: container_name: localstack diff --git a/local.Dockerfile b/local.Dockerfile index 91fb55864..ba10d29c3 100644 --- a/local.Dockerfile +++ b/local.Dockerfile @@ -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 diff --git a/src/components/add-mfa-method-app/add-mfa-method-app-controller.ts b/src/components/add-mfa-method-app/add-mfa-method-app-controller.ts index ba19d27b4..928431f27 100755 --- a/src/components/add-mfa-method-app/add-mfa-method-app-controller.ts +++ b/src/components/add-mfa-method-app/add-mfa-method-app-controller.ts @@ -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"; @@ -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}`); diff --git a/src/components/add-mfa-method-app/tests/add-mfa-methods-app-controller.test.ts b/src/components/add-mfa-method-app/tests/add-mfa-methods-app-controller.test.ts index a53787b1c..a4aafb8f8 100755 --- a/src/components/add-mfa-method-app/tests/add-mfa-methods-app-controller.test.ts +++ b/src/components/add-mfa-method-app/tests/add-mfa-methods-app-controller.test.ts @@ -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), @@ -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(() => {}), }; @@ -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 diff --git a/src/components/add-mfa-method-sms/add-mfa-method-sms-controller.ts b/src/components/add-mfa-method-sms/add-mfa-method-sms-controller.ts index 9ed0a6562..810601a82 100755 --- a/src/components/add-mfa-method-sms/add-mfa-method-sms-controller.ts +++ b/src/components/add-mfa-method-sms/add-mfa-method-sms-controller.ts @@ -1,10 +1,21 @@ 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, @@ -12,24 +23,65 @@ export async function addMfaSmsMethodGet( ): Promise { res.render("add-mfa-method-sms/index.njk"); } +export function addMfaSmsMethodPost( + service: ChangePhoneNumberServiceInterface = changePhoneNumberService() +) { + return async function (req: Request, res: Response): Promise { + 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 { - //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( diff --git a/src/components/add-mfa-method-sms/add-mfa-method-sms-routes.ts b/src/components/add-mfa-method-sms/add-mfa-method-sms-routes.ts index 6d3ad9c2b..35576bb32 100755 --- a/src/components/add-mfa-method-sms/add-mfa-method-sms-routes.ts +++ b/src/components/add-mfa-method-sms/add-mfa-method-sms-routes.ts @@ -7,6 +7,7 @@ import { addMfaSmsMethodGet, addMfaSmsMethodPost, } from "./add-mfa-method-sms-controller"; +import { asyncHandler } from "../../utils/async"; const router = express.Router(); @@ -21,7 +22,7 @@ router.post( PATH_DATA.ADD_MFA_METHOD_SMS.url, requiresAuthMiddleware, validateStateMiddleware, - addMfaSmsMethodPost + asyncHandler(addMfaSmsMethodPost()) ); router.get( diff --git a/src/components/add-mfa-method-sms/tests/add-mfa-method-sms-controller.test.ts b/src/components/add-mfa-method-sms/tests/add-mfa-method-sms-controller.test.ts index 4feabee5a..cd8a47543 100644 --- a/src/components/add-mfa-method-sms/tests/add-mfa-method-sms-controller.test.ts +++ b/src/components/add-mfa-method-sms/tests/add-mfa-method-sms-controller.test.ts @@ -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; @@ -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` ); diff --git a/src/components/check-your-phone/check-your-phone-controller.ts b/src/components/check-your-phone/check-your-phone-controller.ts index 50d3c3516..e2aa60978 100644 --- a/src/components/check-your-phone/check-your-phone-controller.ts +++ b/src/components/check-your-phone/check-your-phone-controller.ts @@ -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}`); } diff --git a/src/components/check-your-phone/check-your-phone-service.ts b/src/components/check-your-phone/check-your-phone-service.ts index fe019f742..e5d4a7d43 100644 --- a/src/components/check-your-phone/check-your-phone-service.ts +++ b/src/components/check-your-phone/check-your-phone-service.ts @@ -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 @@ -41,8 +41,16 @@ export function checkYourPhoneService( return updateMfaMethod(updateInput, sessionDetails); }; + const addMfaMethodService = async function ( + updateInput: UpdateInformationInput, + sessionDetails: UpdateInformationSessionValues + ): Promise { + return createOrUpdateMfaMethod(updateInput, sessionDetails); + }; + return { updatePhoneNumber, updatePhoneNumberWithMfaApi, + addMfaMethodService, }; } diff --git a/src/components/check-your-phone/tests/check-your-phone-controller.test.ts b/src/components/check-your-phone/tests/check-your-phone-controller.test.ts index 1051fbbac..13e0864fd 100644 --- a/src/components/check-your-phone/tests/check-your-phone-controller.test.ts +++ b/src/components/check-your-phone/tests/check-your-phone-controller.test.ts @@ -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; @@ -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; @@ -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; diff --git a/src/components/check-your-phone/types.ts b/src/components/check-your-phone/types.ts index b716d40d2..5bd0c09c5 100644 --- a/src/components/check-your-phone/types.ts +++ b/src/components/check-your-phone/types.ts @@ -13,4 +13,9 @@ export interface CheckYourPhoneServiceInterface { updateInput: UpdateInformationInput, sessionDetails: UpdateInformationSessionValues ) => Promise; + + addMfaMethodService: ( + updateInput: UpdateInformationInput, + sessionDetails: UpdateInformationSessionValues + ) => Promise; } diff --git a/src/components/common/layout/base.njk b/src/components/common/layout/base.njk index 76433f50d..26518dde7 100644 --- a/src/components/common/layout/base.njk +++ b/src/components/common/layout/base.njk @@ -55,7 +55,7 @@ url: currentUrl, activeLanguage: htmlLang, languages: [ - { + { code: 'en', text: 'English', visuallyHidden: 'Change to English' @@ -67,7 +67,7 @@ }] }) }} - {% endif %} + {% endif %} {% if backLink %} {{ backLinkText }} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a1687a093..f9a425961 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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" diff --git a/src/utils/mfa/index.ts b/src/utils/mfa/index.ts index 5f3972161..570bbdad5 100644 --- a/src/utils/mfa/index.ts +++ b/src/utils/mfa/index.ts @@ -5,13 +5,7 @@ import { } from "../../app.constants"; import { logger } from "../logger"; import { getRequestConfig, Http } from "../http"; -import { - MfaMethod, - MfaMethodType, - PriorityIdentifier, - ProblemDetail, - ValidationProblem, -} from "./types"; +import { MfaMethod, ProblemDetail, ValidationProblem } from "./types"; import { getAppEnv, getMfaServiceUrl } from "../../config"; import { authenticator } from "otplib"; import { @@ -40,40 +34,19 @@ export function verifyMfaCode(secret: string, code: string): boolean { return authenticator.check(code, secret); } -export function addMfaMethod({ - email, - otp, - credential, - mfaMethod, - accessToken, - sourceIp, - sessionId, - persistentSessionId, -}: { - email: string; - otp: string; - credential: string; - mfaMethod: { - priorityIdentifier: PriorityIdentifier; - mfaMethodType: MfaMethodType; - }; - accessToken: string; - sourceIp: string; - sessionId: string; - persistentSessionId: string; -}): Promise<{ +export function addMfaMethod( + updateInput: UpdateInformationInput, + sessionDetails: UpdateInformationSessionValues +): Promise<{ status: number; data: MfaMethod; }> { const http = new Http(getMfaServiceUrl()); + const { accessToken, sourceIp, persistentSessionId, sessionId } = + sessionDetails; return http.client.post( METHOD_MANAGEMENT_API.MFA_METHODS_ADD, - { - email, - otp, - credential, - mfaMethod, - }, + updateInput, getRequestConfig({ token: accessToken, sourceIp, @@ -142,7 +115,7 @@ async function putRequest( status: number; data: MfaMethod; }> { - const response = await http.client.put( + return await http.client.put( format( METHOD_MANAGEMENT_API.MFA_METHODS_PUT, updateInput.mfaMethod.mfaIdentifier @@ -163,8 +136,6 @@ async function putRequest( ...sessionDetails, }) ); - - return response; } function errorHandler(error: any, trace: string, action: string): void { @@ -242,4 +213,27 @@ export async function updateMfaMethod( return isUpdated; } +export async function createOrUpdateMfaMethod( + updateInput: UpdateInformationInput, + sessionDetails: UpdateInformationSessionValues +): Promise { + let isUpdated = false; + try { + const response = await addMfaMethod(updateInput, sessionDetails); + + if (response.status === HTTP_STATUS_CODES.OK) { + isUpdated = true; + } else { + errorHandler( + new Error("MFA Method Not Found"), + sessionDetails.sessionId, + "create" + ); + } + } catch (err) { + errorHandler(err, sessionDetails.sessionId, "create"); + } + return isUpdated; +} + export default retrieveMfaMethods; diff --git a/src/utils/mfa/types.d.ts b/src/utils/mfa/types.d.ts index c7a80befb..3ab8b5ec5 100644 --- a/src/utils/mfa/types.d.ts +++ b/src/utils/mfa/types.d.ts @@ -2,11 +2,12 @@ export type PriorityIdentifier = "PRIMARY" | "SECONDARY"; export type MfaMethodType = "SMS" | "AUTH_APP"; export interface MfaMethod { - mfaIdentifier: number; + mfaIdentifier?: number; priorityIdentifier: PriorityIdentifier; mfaMethodType: MfaMethodType; endPoint?: string; - methodVerified: boolean; + methodVerified?: boolean; + smsPhoneNumber?: string; } export interface ProblemDetail { diff --git a/src/utils/types.ts b/src/utils/types.ts index 19090a8dd..7590b9e62 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -109,6 +109,7 @@ export interface Error { export interface UpdateInformationInput { email: string; + credential?: string; updatedValue?: string; otp: string; mfaMethod?: MfaMethod;