Skip to content

Commit

Permalink
PP-11363 Use correct Apple Pay certificate for payment provider
Browse files Browse the repository at this point in the history
Determine which certificate environment variables to use
(stripe/worldpay) by looking at the payment provider for the charge.

Convert tests to unit tests using sinon/proxyquire to make them easier
to write/debug.

Add in additional logging for the expiry date for the Stripe certificate
making sure not to break the Splunk alert for when the certificate is
close to expiring. The Splunk alert will fire for both Worldpay/Stripe
  • Loading branch information
stephencdaly committed Sep 4, 2023
1 parent 43c7ed0 commit c8ac97e
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 81 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ SECURE_COOKIE_OFF=false
COOKIE_MAX_AGE=5400000
SESSION_ENCRYPTION_KEY=naskjwefvwei72rjkwfmjwfi72rfkjwefmjwefiuwefjkbwfiu24fmjbwfk
CSRF_USER_SECRET=123456789012345678
APPLE_PAY_MERCHANT_ID=merchant.uk.gov.service.payments.test
WORLDPAY_APPLE_PAY_MERCHANT_ID=merchant.uk.gov.service.payments.test
STRIPE_APPLE_PAY_MERCHANT_ID=merchant.uk.gov.service.payments.stripe.test
APPLE_PAY_MERCHANT_DOMAIN=www.pymnt.uk
GOOGLE_PAY_GATEWAY_MERCHANT_ID=exampleGatewayMerchantId
GOOGLE_PAY_MERCHANT_ID=01234567890123456789
Expand Down
13 changes: 11 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
"filename": "app/controllers/web-payments/apple-pay/merchant-validation.controller.js",
"hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9",
"is_verified": false,
"line_number": 13,
"line_number": 14,
"is_secret": false
}
],
Expand All @@ -132,6 +132,15 @@
"is_secret": false
}
],
"test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js": [
{
"type": "Private Key",
"filename": "test/controllers/web-payments/apple-pay/merchant-validation.controller.test.js",
"hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9",
"is_verified": false,
"line_number": 68
}
],
"test/controllers/web-payments/apple-pay/normalise-apple-pay-payload.test.js": [
{
"type": "Base64 High Entropy String",
Expand Down Expand Up @@ -409,5 +418,5 @@
}
]
},
"generated_at": "2023-08-23T10:41:36Z"
"generated_at": "2023-08-25T15:23:05Z"
}
5 changes: 4 additions & 1 deletion app/assets/javascripts/browsered/web-payments/apple-pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ module.exports = () => {
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url })
body: JSON.stringify({
url,
paymentProvider: window.Charge.payment_provider
})
}).then(response => {
if (response.status >= 200 && response.status < 300) {
return response.json().then(data => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,36 @@ const request = require('requestretry')
const logger = require('../../../utils/logger')(__filename)
const { getLoggingFields } = require('../../../utils/logging-fields-helper')

// Local constants
const { APPLE_PAY_MERCHANT_ID, APPLE_PAY_MERCHANT_DOMAIN, APPLE_PAY_MERCHANT_ID_CERTIFICATE, APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY } = process.env

const APPLE_PAY_MERCHANT_ID_CERTIFICATE_MULTILINE = `-----BEGIN CERTIFICATE-----
${APPLE_PAY_MERCHANT_ID_CERTIFICATE}
function getCertificateMultiline (cert) {
return `-----BEGIN CERTIFICATE-----
${cert}
-----END CERTIFICATE-----`
const APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY_MULTILINE = `-----BEGIN PRIVATE KEY-----
${APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY}
}

function getPrivateKeyMultiline (key) {
return `-----BEGIN PRIVATE KEY-----
${key}
-----END PRIVATE KEY-----`
}

function getApplePayMerchantIdentityVariables (paymentProvider) {
if (paymentProvider === 'worldpay') {
return {
merchantIdentifier: process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID,
cert: getCertificateMultiline(process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE),
key: getPrivateKeyMultiline(process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY)
}
} else if (paymentProvider === 'stripe') {
return {
merchantIdentifier: process.env.STRIPE_APPLE_PAY_MERCHANT_ID,
cert: getCertificateMultiline(process.env.STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE),
key: getPrivateKeyMultiline(process.env.STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY)
}
} else {
logger.error(`Unexpected payment provider [${paymentProvider}] when getting Merchant Identity variables for Apple Pay`)
return false
}
}

// When an Apple payment is initiated in Safari, it must check that the request
// is coming from a registered and authorised Apple Merchant Account. The
Expand All @@ -21,17 +42,22 @@ module.exports = (req, res) => {
if (!req.body.url) {
return res.sendStatus(400)
}
const { url, paymentProvider } = req.body
const merchantIdentityVars = getApplePayMerchantIdentityVariables(paymentProvider)
if (!merchantIdentityVars) {
return res.sendStatus(400)
}

const options = {
url: req.body.url,
cert: APPLE_PAY_MERCHANT_ID_CERTIFICATE_MULTILINE,
key: APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY_MULTILINE,
url: url,
cert: merchantIdentityVars.cert,
key: merchantIdentityVars.key,
method: 'post',
body: {
merchantIdentifier: APPLE_PAY_MERCHANT_ID,
merchantIdentifier: merchantIdentityVars.merchantIdentifier,
displayName: 'GOV.UK Pay',
initiative: 'web',
initiativeContext: APPLE_PAY_MERCHANT_DOMAIN
initiativeContext: process.env.APPLE_PAY_MERCHANT_DOMAIN
},
json: true
}
Expand Down
3 changes: 2 additions & 1 deletion app/views/includes/scripts.njk
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
collect_billing_address: {{ "true" if collectBillingAddress else "false" }},
collect_additional_browser_data_for_epdq_3ds: {{"true" if collectAdditionalBrowserDataForEpdq3ds else "false" }},
worldpay_3ds_flex_ddc_jwt: '{{ worldpay3dsFlexDdcJwt }}',
worldpay_3ds_flex_ddc_url: '{{ worldpay3dsFlexDdcUrl }}'
worldpay_3ds_flex_ddc_url: '{{ worldpay3dsFlexDdcUrl }}',
payment_provider: '{{ paymentProvider }}'
}
var mainWrap = document.getElementsByTagName('main')[0]
Expand Down
22 changes: 18 additions & 4 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ const correlationHeader = require('./app/middleware/correlation-header')
const errorHandlers = require('./app/middleware/error-handlers')

// Global constants
const { NODE_ENV, PORT, ANALYTICS_TRACKING_ID, GOOGLE_PAY_MERCHANT_ID, APPLE_PAY_MERCHANT_ID_CERTIFICATE } = process.env
const {
NODE_ENV,
PORT,
ANALYTICS_TRACKING_ID,
GOOGLE_PAY_MERCHANT_ID,
WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE,
STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE
} = process.env
const CSS_PATH = '/stylesheets/application.min.css'
const JAVASCRIPT_PATH = '/javascripts/application.min.js'
const argv = require('minimist')(process.argv.slice(2))
Expand Down Expand Up @@ -155,10 +162,17 @@ function listen () {
}

function logApplePayCertificateTimeToExpiry () {
if (APPLE_PAY_MERCHANT_ID_CERTIFICATE !== undefined) {
const merchantIdCert = certinfo.info(APPLE_PAY_MERCHANT_ID_CERTIFICATE)
if (WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE !== undefined) {
const merchantIdCert = certinfo.info(WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE)
const certificateTimeToExpiry = Math.floor((merchantIdCert.expiresAt - Date.now()) / 1000 / 60 / 60 / 24)
logger.info(`The Apple Pay Merchant identity cert will expire in ${certificateTimeToExpiry} days`)
// used by Splunk alert
logger.info(`The Apple Pay Merchant identity cert will expire in ${certificateTimeToExpiry} days for Worldpay`)
}
if (STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE !== undefined) {
const merchantIdCert = certinfo.info(STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE)
const certificateTimeToExpiry = Math.floor((merchantIdCert.expiresAt - Date.now()) / 1000 / 60 / 60 / 24)
// used by Splunk alert
logger.info(`The Apple Pay Merchant identity cert will expire in ${certificateTimeToExpiry} days for Stripe`)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,74 +1,150 @@
'use strict'

// NPM dependencies
const { expect } = require('chai')
const request = require('supertest')
const nock = require('nock')
const sinon = require('sinon')
const proxyquire = require('proxyquire')

// Local constants
const { APPLE_PAY_MERCHANT_ID, APPLE_PAY_MERCHANT_DOMAIN } = process.env
const merchantDomain = 'www.pymnt.uk'
const worldpayMerchantId = 'worldpay.merchant.id'
const worldpayCertificate = 'A-WORLDPAY-CERTIFICATE'
const worldpayKey = 'A-WORLDPAY-KEY'
const stripeMerchantId = 'stripe.merchant.id'
const stripeCertificate = 'A-STRIPE-CERTIFICATE'
const stripeKey = 'A-STRIPE-KEY'
const url = 'https://fakeapple.url'

// Local dependencies
const { getApp } = require('../../../../server')
const paths = require('../../../../app/paths')
require('../../../test-helpers/html-assertions')
const appleResponse = { status: 200 }
const appleResponseBody = { foo: 'bar' }

function getControllerWithMocks (requestMock) {
return proxyquire('../../../../app/controllers/web-payments/apple-pay/merchant-validation.controller', {
requestretry: requestMock
})
}

describe('Validate with Apple the merchant is legitimate', () => {
it('should return a payload if Merchant is valid', done => {
const url = 'https://fakeapple.url'
const body = {
merchantIdentifier: APPLE_PAY_MERCHANT_ID,
displayName: 'GOV.UK Pay',
initiative: 'web',
initiativeContext: APPLE_PAY_MERCHANT_DOMAIN
let res, sendSpy

beforeEach(() => {
process.env.APPLE_PAY_MERCHANT_DOMAIN = merchantDomain
process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID = worldpayMerchantId
process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE = worldpayCertificate
process.env.WORLDPAY_APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY = worldpayKey
process.env.STRIPE_APPLE_PAY_MERCHANT_ID = stripeMerchantId
process.env.STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE = stripeCertificate
process.env.STRIPE_APPLE_PAY_MERCHANT_ID_CERTIFICATE_KEY = stripeKey

sendSpy = sinon.spy()
res = {
status: sinon.spy(() => ({ send: sendSpy })),
sendStatus: sinon.spy()
}
})

it('should return a payload for a Worldpay payment if Merchant is valid', async () => {
const mockRequest = sinon.stub().yields(null, appleResponse, appleResponseBody)
const controller = getControllerWithMocks(mockRequest)

const req = {
body: {
url,
paymentProvider: 'worldpay'
}
}
await controller(req, res)

sinon.assert.calledWith(mockRequest, {
url,
body: {
merchantIdentifier: worldpayMerchantId,
displayName: 'GOV.UK Pay',
initiative: 'web',
initiativeContext: merchantDomain
},
method: 'post',
json: true,
cert: `-----BEGIN CERTIFICATE-----
${worldpayCertificate}
-----END CERTIFICATE-----`,
key: `-----BEGIN PRIVATE KEY-----
${worldpayKey}
-----END PRIVATE KEY-----`
})
sinon.assert.calledWith(res.status, 200)
sinon.assert.calledWith(sendSpy, appleResponseBody)
})

it('should return a payload for a Stripe payment if Merchant is valid', async () => {
const mockRequest = sinon.stub().yields(null, appleResponse, appleResponseBody)
const controller = getControllerWithMocks(mockRequest)

const req = {
body: {
url,
paymentProvider: 'stripe'
}
}
const response = { encryptedThing: 'cryptoMagic' }

nock(url)
.post('/', body)
.reply(200, response)

request(getApp())
.post(paths.applePay.session.path)
.set('Accept', 'application/json')
.send({
url
})
.expect(200)
.expect(res => {
expect(res.body).to.deep.equal(response)
})
.end(done)
await controller(req, res)

sinon.assert.calledWith(mockRequest, {
url,
body: {
merchantIdentifier: stripeMerchantId,
displayName: 'GOV.UK Pay',
initiative: 'web',
initiativeContext: merchantDomain
},
method: 'post',
json: true,
cert: `-----BEGIN CERTIFICATE-----
${stripeCertificate}
-----END CERTIFICATE-----`,
key: `-----BEGIN PRIVATE KEY-----
${stripeKey}
-----END PRIVATE KEY-----`
})
sinon.assert.calledWith(res.status, 200)
sinon.assert.calledWith(sendSpy, appleResponseBody)
})

it('should return 400 if no url is provided', done => {
request(getApp())
.post(paths.applePay.session.path)
.set('Accept', 'application/json')
.expect(400)
.end(done)
it('should return 400 if no url is provided', async () => {
const mockRequest = sinon.stub().yields(null, appleResponse, appleResponseBody)
const controller = getControllerWithMocks(mockRequest)

const req = {
body: {
paymentProvider: 'worldpay'
}
}
await controller(req, res)
sinon.assert.calledWith(res.sendStatus, 400)
})

it('should return an error if Merchant is invalid, the merchant details or crypto stuff', done => {
const url = 'https://fakeapple.url'
const body = {
merchantIdentifier: APPLE_PAY_MERCHANT_ID,
displayName: 'GOV.UK Pay',
initiative: 'web',
initiativeContext: APPLE_PAY_MERCHANT_DOMAIN
it('should return 400 for unexpected payment provider', async () => {
const mockRequest = sinon.stub().yields(null, appleResponse, appleResponseBody)
const controller = getControllerWithMocks(mockRequest)

const req = {
body: {
url,
paymentProvider: 'sandbox'
}
}
await controller(req, res)
sinon.assert.calledWith(res.sendStatus, 400)
})

nock(url)
.post('/', body)
.replyWithError('nope')

request(getApp())
.post(paths.applePay.session.path)
.set('Accept', 'application/json')
.send({
url
})
.expect(500)
.end(done)
it('should return an error if Apple Pay returns an error', async () => {
const mockRequest = sinon.stub().yields(new Error(), appleResponse, appleResponseBody)
const controller = getControllerWithMocks(mockRequest)

const req = {
body: {
url,
paymentProvider: 'worldpay'
}
}
await controller(req, res)
sinon.assert.calledWith(res.status, 500)
sinon.assert.calledWith(sendSpy, appleResponseBody)
})
})
2 changes: 2 additions & 0 deletions test/cypress/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ NODE_ENV=test
NODE_WORKER_COUNT=1
APPLE_PAY_STUBS_URL=http://127.0.0.1:8000
WORLDPAY_3DS_FLEX_DDC_TEST_URL=http://127.0.0.1:8000/shopper/3ds/ddc.html
WORLDPAY_APPLE_PAY_MERCHANT_ID=worldpay.merchant.id
STRIPE_APPLE_PAY_MERCHANT_ID=stripe.merchant.id
3 changes: 2 additions & 1 deletion test/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ COOKIE_MAX_AGE=5400000
SESSION_ENCRYPTION_KEY=naskjwefvwei72rjkwfmjwfi72rfkjwefmjwefiuwefjkbwfiu24fmjbwfk
CSRF_USER_SECRET=123456789012345678
CARDID_HOST=http://cardid.pymnt.localdomain:65530
APPLE_PAY_MERCHANT_ID=merchant.uk.gov.service.payments.test
WORLDPAY_APPLE_PAY_MERCHANT_ID=merchant.uk.gov.service.payments.test
STRIPE_APPLE_PAY_MERCHANT_ID=merchant.uk.gov.service.payments.stripe.test
APPLE_PAY_MERCHANT_DOMAIN=www.pymnt.uk
GOOGLE_PAY_GATEWAY_MERCHANT_ID=exampleGatewayMerchantId
GOOGLE_PAY_MERCHANT_ID=01234567890123456789
Expand Down

0 comments on commit c8ac97e

Please sign in to comment.