Skip to content

Commit

Permalink
feat: log ip address on callback from twilio (#3937)
Browse files Browse the repository at this point in the history
* feat: log mobile number when otp generated

* Revert "feat: log mobile number when otp generated"

This reverts commit 58b4bc5.

* feat: pass senderIp as query param to twilio status callback

* chore: update tests

* chore: use node url api

* chore: pass in full url to URL api

* chore: add try catch block, fix tests
  • Loading branch information
tshuli authored Jun 7, 2022
1 parent e5e173d commit 0493774
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 10 deletions.
16 changes: 16 additions & 0 deletions src/app/modules/twilio/__tests__/twilio.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ describe('twilio.controller', () => {
it('should return 200 when successfully delivered message is sent', async () => {
const mockReq = expressHandler.mockRequest({
body: MOCK_SUCCESSFUL_MESSAGE,
others: {
protocol: 'https',
host: 'webhook-endpoint.gov.sg',
url: `/endpoint?${encodeURI('senderIp=200.0.0.0')}`,
originalUrl: `/endpoint?${encodeURI('senderIp=200.0.0.0')}`,
get: () => 'webhook-endpoint.gov.sg',
},
})
const mockRes = expressHandler.mockResponse()
await twilioSmsUpdates(mockReq, mockRes, jest.fn())
Expand All @@ -57,6 +64,7 @@ describe('twilio.controller', () => {
meta: {
action: 'twilioSmsUpdates',
body: MOCK_SUCCESSFUL_MESSAGE,
senderIp: '200.0.0.0',
},
})
expect(mockLogger.error).not.toBeCalled()
Expand All @@ -66,6 +74,13 @@ describe('twilio.controller', () => {
it('should return 200 when failed delivered message is sent', async () => {
const mockReq = expressHandler.mockRequest({
body: MOCK_FAILED_MESSAGE,
others: {
protocol: 'https',
host: 'webhook-endpoint.gov.sg',
url: `/endpoint?${encodeURI('senderIp=200.0.0.0')}`,
originalUrl: `/endpoint?${encodeURI('senderIp=200.0.0.0')}`,
get: () => 'webhook-endpoint.gov.sg',
},
})
const mockRes = expressHandler.mockResponse()
await twilioSmsUpdates(mockReq, mockRes, jest.fn())
Expand All @@ -76,6 +91,7 @@ describe('twilio.controller', () => {
meta: {
action: 'twilioSmsUpdates',
body: MOCK_FAILED_MESSAGE,
senderIp: '200.0.0.0',
},
})
expect(mockRes.sendStatus).toBeCalledWith(200)
Expand Down
20 changes: 20 additions & 0 deletions src/app/modules/twilio/twilio.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ export const twilioSmsUpdates: ControllerHandler<
* Example: https://www.twilio.com/docs/usage/webhooks/sms-webhooks.
*/

// Extract public sender's ip address which was passed to twilio as a query param in the status callback
let senderIp = null
try {
const url = new URL(
req.protocol + '://' + req.get('host') + req.originalUrl,
)
senderIp = url.searchParams.get('senderIp')
} catch {
logger.error({
message: 'Error occurred when extracting senderIp',
meta: {
action: 'twilioSmsUpdates',
body: req.body,
originalUrl: req.originalUrl,
},
})
}

const ddTags: TwilioSmsStatsdTags = {
// msgSrvcSid not included to limit tag cardinality (for now?)
smsstatus: req.body.SmsStatus,
Expand All @@ -70,6 +88,7 @@ export const twilioSmsUpdates: ControllerHandler<
meta: {
action: 'twilioSmsUpdates',
body: req.body,
senderIp,
},
})
} else {
Expand All @@ -78,6 +97,7 @@ export const twilioSmsUpdates: ControllerHandler<
meta: {
action: 'twilioSmsUpdates',
body: req.body,
senderIp,
},
})
}
Expand Down
1 change: 1 addition & 0 deletions src/app/modules/user/__tests__/user.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe('user.controller', () => {
MOCK_REQ.body.contact,
expectedOtp,
MOCK_REQ.body.userId,
'MOCK_IP',
)
expect(mockRes.sendStatus).toBeCalledWith(200)
})
Expand Down
5 changes: 4 additions & 1 deletion src/app/modules/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ export const _handleContactSendOtp: ControllerHandler<
return res.status(StatusCodes.UNAUTHORIZED).json('User is unauthorized.')
}

const senderIp = getRequestIp(req)

const logMeta = {
action: 'handleContactSendOtp',
userId,
ip: getRequestIp(req),
ip: senderIp,
}

// Step 1: Create OTP for contact verification.
Expand All @@ -67,6 +69,7 @@ export const _handleContactSendOtp: ControllerHandler<
contact,
otp,
userId,
senderIp,
)

// Error sending OTP.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.sendStatus).toHaveBeenCalledWith(StatusCodes.CREATED)
})
Expand All @@ -409,6 +410,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.NOT_FOUND)
expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String) })
Expand All @@ -428,6 +430,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String) })
Expand All @@ -447,6 +450,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.NOT_FOUND)
expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String) })
Expand Down Expand Up @@ -481,6 +485,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(
StatusCodes.UNPROCESSABLE_ENTITY,
Expand All @@ -502,6 +507,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String) })
Expand All @@ -521,6 +527,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String) })
Expand All @@ -540,6 +547,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String) })
Expand All @@ -559,6 +567,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String) })
Expand All @@ -578,6 +587,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String) })
Expand All @@ -597,6 +607,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(
StatusCodes.INTERNAL_SERVER_ERROR,
Expand Down Expand Up @@ -664,6 +675,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(
MockVerificationService.disableVerifiedFieldsIfRequired,
Expand Down Expand Up @@ -702,6 +714,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
Expand Down Expand Up @@ -734,6 +747,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
Expand Down Expand Up @@ -766,6 +780,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
Expand Down Expand Up @@ -799,6 +814,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
Expand Down Expand Up @@ -832,6 +848,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
Expand Down Expand Up @@ -886,6 +903,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
Expand Down Expand Up @@ -944,6 +962,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.NOT_FOUND)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
Expand Down Expand Up @@ -976,6 +995,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.NOT_FOUND)
expect(mockRes.json).toHaveBeenCalledWith(expectedResponse)
Expand Down Expand Up @@ -1008,6 +1028,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(
StatusCodes.UNPROCESSABLE_ENTITY,
Expand Down Expand Up @@ -1067,6 +1088,7 @@ describe('Verification controller', () => {
otp: MOCK_OTP,
hashedOtp: MOCK_HASHED_OTP,
recipient: MOCK_ANSWER,
senderIp: 'MOCK_IP',
})
expect(mockRes.status).toHaveBeenCalledWith(
StatusCodes.INTERNAL_SERVER_ERROR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
MOCK_HASHED_OTP,
MOCK_OTP,
MOCK_RECIPIENT,
MOCK_SENDER_IP,
MOCK_SIGNED_DATA,
} from './verification.test.helpers'

Expand Down Expand Up @@ -327,13 +328,15 @@ describe('Verification service', () => {
hashedOtp: MOCK_HASHED_OTP,
otp: MOCK_OTP,
recipient: MOCK_RECIPIENT,
senderIp: MOCK_SENDER_IP,
})

// Default mock params has fieldType: 'mobile'
expect(MockSmsFactory.sendVerificationOtp).toHaveBeenCalledWith(
MOCK_RECIPIENT,
MOCK_OTP,
mockTransaction.formId,
MOCK_SENDER_IP,
)
expect(MockFormsgSdk.verification.generateSignature).toHaveBeenCalledWith(
{
Expand All @@ -360,6 +363,7 @@ describe('Verification service', () => {
hashedOtp: MOCK_HASHED_OTP,
otp: MOCK_OTP,
recipient: MOCK_RECIPIENT,
senderIp: MOCK_SENDER_IP,
})

expect(MockMailService.sendVerificationOtp).not.toHaveBeenCalled()
Expand All @@ -384,6 +388,7 @@ describe('Verification service', () => {
hashedOtp: MOCK_HASHED_OTP,
otp: MOCK_OTP,
recipient: MOCK_RECIPIENT,
senderIp: MOCK_SENDER_IP,
})

expect(MockMailService.sendVerificationOtp).not.toHaveBeenCalled()
Expand All @@ -403,6 +408,7 @@ describe('Verification service', () => {
hashedOtp: MOCK_HASHED_OTP,
otp: MOCK_OTP,
recipient: MOCK_RECIPIENT,
senderIp: MOCK_SENDER_IP,
})

expect(MockMailService.sendVerificationOtp).not.toHaveBeenCalled()
Expand Down Expand Up @@ -434,6 +440,7 @@ describe('Verification service', () => {
hashedOtp: MOCK_HASHED_OTP,
otp: MOCK_OTP,
recipient: MOCK_RECIPIENT,
senderIp: MOCK_SENDER_IP,
})

expect(MockMailService.sendVerificationOtp).not.toHaveBeenCalled()
Expand Down Expand Up @@ -492,6 +499,7 @@ describe('Verification service', () => {
hashedOtp: MOCK_HASHED_OTP,
otp: MOCK_OTP,
recipient: MOCK_RECIPIENT,
senderIp: MOCK_SENDER_IP,
})

expect(MockMailService.sendVerificationOtp).toHaveBeenCalledWith(
Expand Down Expand Up @@ -526,12 +534,14 @@ describe('Verification service', () => {
hashedOtp: MOCK_HASHED_OTP,
otp: MOCK_OTP,
recipient: MOCK_RECIPIENT,
senderIp: MOCK_SENDER_IP,
})

expect(MockSmsFactory.sendVerificationOtp).toHaveBeenCalledWith(
MOCK_RECIPIENT,
MOCK_OTP,
new ObjectId(mockFormId),
MOCK_SENDER_IP,
)
expect(
MockFormsgSdk.verification.generateSignature,
Expand All @@ -551,13 +561,15 @@ describe('Verification service', () => {
hashedOtp: MOCK_HASHED_OTP,
otp: MOCK_OTP,
recipient: MOCK_RECIPIENT,
senderIp: MOCK_SENDER_IP,
})

// Mock params default to mobile
expect(MockSmsFactory.sendVerificationOtp).toHaveBeenCalledWith(
MOCK_RECIPIENT,
MOCK_OTP,
new ObjectId(mockFormId),
MOCK_SENDER_IP,
)
expect(MockFormsgSdk.verification.generateSignature).toHaveBeenCalledWith(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const MOCK_SIGNED_DATA = 'mockSignedData'
export const MOCK_HASHED_OTP = 'mockHashedOtp'
export const MOCK_OTP = '123456'
export const MOCK_RECIPIENT = '81234567'
export const MOCK_SENDER_IP = '200.0.0.0'

export const VFN_FIELD_DEFAULTS = {
signedData: null,
Expand Down
Loading

0 comments on commit 0493774

Please sign in to comment.