Skip to content

Commit

Permalink
ATO-814: Added error handling to cognito sms lambda.
Browse files Browse the repository at this point in the history
  • Loading branch information
kalpaitch committed Aug 7, 2024
1 parent 770c244 commit 5fdd807
Show file tree
Hide file tree
Showing 15 changed files with 1,183 additions and 1,083 deletions.
1 change: 1 addition & 0 deletions backend/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@aws-sdk/client-sqs": "^3.609.0",
"@aws-sdk/client-dynamodb": "^3.609.0",
"@aws-sdk/util-dynamodb": "^3.609.0",
"@aws-lambda-powertools/logger": "2.6.0",
"axios": "^1.7.2",
"esbuild": "^0.23.0",
"sinon": "^17.0.1"
Expand Down
29 changes: 21 additions & 8 deletions backend/api/src/handlers/auth/register-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {APIGatewayProxyResult} from "aws-lambda";
import {Context, APIGatewayProxyResult} from "aws-lambda";
import axios from "axios";
import {Logger} from "@aws-lambda-powertools/logger";

export const logger = new Logger({
serviceName: "self-service-experience"
});

export type RegisterClientPayload = {
service: {
Expand All @@ -14,7 +19,9 @@ const public_key =

// Handler is invoked by step function not API Gateway
//The event type is not APIGatewayProxyEvent but the custom JSON payload from the step function invoke
export const registerClientHandler = async (event: RegisterClientPayload): Promise<APIGatewayProxyResult> => {
export const registerClientHandler = async (event: RegisterClientPayload, context: Context): Promise<APIGatewayProxyResult> => {
logger.addContext(context);

const redirect_uris = ["http://localhost/"];
const scopes = ["openid", "email", "phone"];
const subject_type = "pairwise";
Expand All @@ -35,12 +42,18 @@ export const registerClientHandler = async (event: RegisterClientPayload): Promi
};

const url = process.env.AUTH_REGISTRATION_BASE_URL + "/connect/register";
const result = await axios.post(url, JSON.stringify(clientConfig), {
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.AUTH_API_KEY as string
}
});
const result = await axios
.post(url, JSON.stringify(clientConfig), {
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.AUTH_API_KEY as string
}
})
.catch(error => {
logger.error("Client registry request failed with response.", error as Error);
throw error;
});

const body = {...clientConfig, ...result.data, ...event};

return {
Expand Down
20 changes: 11 additions & 9 deletions backend/api/src/handlers/auth/update-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {APIGatewayProxyResult} from "aws-lambda";
import axios, {AxiosResponse, isAxiosError} from "axios";
import {Context, APIGatewayProxyResult} from "aws-lambda";
import axios, {AxiosResponse} from "axios";
import {Logger} from "@aws-lambda-powertools/logger";

export const logger = new Logger({
serviceName: "self-service-experience"
});

export type UpdateClientPayload = {
clientId: string;
Expand All @@ -10,8 +15,9 @@ export type UpdateClientPayload = {

// Handler is invoked by step function not API Gateway
//The event type is not APIGatewayProxyEvent but the custom JSON payload from the step function invokes
export const updateClientInRegistryHandler = async (event: UpdateClientPayload): Promise<APIGatewayProxyResult> => {
console.log("Update client request received");
export const updateClientInRegistryHandler = async (event: UpdateClientPayload, context: Context): Promise<APIGatewayProxyResult> => {
logger.addContext(context);

const url = process.env.AUTH_REGISTRATION_BASE_URL + "/connect/register/" + event.clientId;

if (event.updates.hasOwnProperty("service_name")) {
Expand All @@ -28,11 +34,7 @@ export const updateClientInRegistryHandler = async (event: UpdateClientPayload):
})
.then(handleResponse(event))
.catch(error => {
if (isAxiosError(error)) {
console.error(
`Client registry request failed with response: "${error.response?.status}" and message "${error.response?.data}"`
);
}
logger.error("Client registry request failed with response.", error as Error);
throw error;
});

Expand Down
3 changes: 2 additions & 1 deletion backend/api/tests/handlers/auth/register-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import sinon from "sinon";
import axios from "axios";
import {RegisterClientPayload, registerClientHandler} from "../../../src/handlers/auth/register-client";
import {mockLambdaContext} from "../utils";

const postRequest = async (data: RegisterClientPayload): Promise<string> => {
const result = await registerClientHandler(data);
const result = await registerClientHandler(data, mockLambdaContext);
return JSON.stringify(result);
};

Expand Down
9 changes: 5 additions & 4 deletions backend/api/tests/handlers/auth/update-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import axios, {AxiosError, AxiosResponse} from "axios";
import {UpdateClientPayload, updateClientInRegistryHandler} from "../../../src/handlers/auth/update-client";
import {UpdateClientPayload, updateClientInRegistryHandler, logger} from "../../../src/handlers/auth/update-client";
import {mockLambdaContext} from "../utils";

const postRequest = async (data: UpdateClientPayload): Promise<string> => {
const result = await updateClientInRegistryHandler(data);
const result = await updateClientInRegistryHandler(data, mockLambdaContext);
return JSON.stringify(result);
};

Expand All @@ -24,7 +25,7 @@ describe("exercise update-client api", () => {

beforeEach(() => {
axiosPutStub = jest.spyOn(axios, "put");
consoleErrorStub = jest.spyOn(console, "error");
consoleErrorStub = jest.spyOn(logger, "error");
});

afterEach(() => {
Expand Down Expand Up @@ -56,7 +57,7 @@ describe("exercise update-client api", () => {
axiosPutStub.mockRejectedValue(axiosError);

await expect(postRequest(updateClientRequest)).rejects.toThrow(AxiosError);
expect(consoleErrorStub).toHaveBeenCalledWith('Client registry request failed with response: "400" and message "Invalid Request"');
expect(consoleErrorStub).toHaveBeenCalledWith("Client registry request failed with response.", axiosError);
});

it("should throw an non axios error", async () => {
Expand Down
1 change: 1 addition & 0 deletions backend/cognito/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@aws-sdk/client-ses": "^3.609.0",
"@aws-lambda-powertools/logger": "2.6.0",
"notifications-node-client": "^7.0.0",
"esbuild": "^0.23.0",
"@aws-crypto/client-node": "^3.2.0"
Expand Down
28 changes: 20 additions & 8 deletions backend/cognito/src/handlers/send-security-code-text-message.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {CustomSMSSenderTriggerEvent} from "aws-lambda";
import {Context, CustomSMSSenderTriggerEvent} from "aws-lambda";
import {NotifyClient} from "notifications-node-client";
import {toByteArray} from "base64-js";
import {buildClient, CommitmentPolicy, KmsKeyringNode} from "@aws-crypto/client-node";
import {Logger} from "@aws-lambda-powertools/logger";

const {decrypt} = buildClient(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT);
const generatorKeyId = process.env.DECRYPTION_KEY_ARN;
Expand All @@ -10,19 +11,30 @@ const keyring = new KmsKeyringNode({generatorKeyId});
const apiKey = process.env.NOTIFY_API_KEY;
const templateId = process.env.SECURITY_CODE_TEXT_MESSAGE_TEMPLATE;
const notifyClient = new NotifyClient(apiKey);
const logger = new Logger({
serviceName: "self-service-experience"
});

export const lambdaHandler = async (event: CustomSMSSenderTriggerEvent, context: Context): Promise<void> => {
logger.addContext(context);

export const lambdaHandler = async (event: CustomSMSSenderTriggerEvent): Promise<void> => {
if (!event.request.code) {
logger.error("No request code provided, failed to send sms.");
throw new Error("Missing code parameter");
}

const plainTextCode = await decryptText(event.request.code);
const phoneNumber = event.request.userAttributes.phone_number;
try {
const plainTextCode = await decryptText(event.request.code);
const phoneNumber = event.request.userAttributes.phone_number;

await notifyClient.sendSms(templateId, phoneNumber, {
personalisation: {code: plainTextCode},
reference: null
});
await notifyClient.sendSms(templateId, phoneNumber, {
personalisation: {code: plainTextCode},
reference: null
});
} catch (error) {
logger.error("Failed to send security code sms", error as Error);
throw error;
}
};

async function decryptText(encryptedText: string): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ process.env.SECURITY_CODE_TEXT_MESSAGE_TEMPLATE = templateId;

import {toByteArray} from "base64-js";
import {lambdaHandler} from "../../src/handlers/send-security-code-text-message";
import {smsSenderTrigger} from "../mocks";
import {mockLambdaContext, smsSenderTrigger} from "../mocks";

describe("Custom SMS sender", () => {
it("Send SMS with a security code", async () => {
await lambdaHandler(smsSenderTrigger(phoneNumber, encryptedCode));
await lambdaHandler(smsSenderTrigger(phoneNumber, encryptedCode), mockLambdaContext);

expect(sendSms).toBeCalledTimes(1);
expect(sendSms.mock.calls[0][0]).toBe(templateId);
Expand All @@ -52,6 +52,6 @@ describe("Custom SMS sender", () => {
});

it("Throw an error for missing code", async () => {
await expect(lambdaHandler(smsSenderTrigger(phoneNumber))).rejects.toThrow(/missing.*code/i);
await expect(lambdaHandler(smsSenderTrigger(phoneNumber), mockLambdaContext)).rejects.toThrow(/missing.*code/i);
});
});
17 changes: 16 additions & 1 deletion backend/cognito/tests/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import {CustomMessageForgotPasswordTriggerEvent, CustomSMSSenderTriggerEvent} from "aws-lambda";
import {Context, CustomMessageForgotPasswordTriggerEvent, CustomSMSSenderTriggerEvent} from "aws-lambda";

export const smsSenderTrigger = (number: string, code?: string) =>
({request: {code: code, userAttributes: {phone_number: number}}} as unknown as CustomSMSSenderTriggerEvent);

export const mockLambdaContext: Context = {
callbackWaitsForEmptyEventLoop: false,
functionName: "someFunction",
functionVersion: "someVersion",
invokedFunctionArn: "someFunctionArn",
memoryLimitInMB: "1",
awsRequestId: "someRequestId",
logGroupName: "someLogGroupName",
logStreamName: "someLogStreamName",
getRemainingTimeInMillis: () => 1,
done: jest.fn(),
fail: jest.fn(),
succeed: jest.fn()
};

export const TEST_PROTOCOL = "https";
export const TEST_HOST = "some.service.gov.uk";
export const TEST_CODE_PARAM = "12345";
Expand Down
Loading

0 comments on commit 5fdd807

Please sign in to comment.