Skip to content

Commit

Permalink
Merge pull request #3639 from alphagov/PP-11363-use-appropriate-merch…
Browse files Browse the repository at this point in the history
…ant-id-environment-variables

PP-11363 Use correct Apple Pay certificate for payment provider
  • Loading branch information
stephencdaly authored Sep 4, 2023
2 parents 43c7ed0 + c8ac97e commit 4d28d75
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 4d28d75

Please sign in to comment.