├── .gitignore ├── package.json ├── server.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apple-pay-merchant-session-server", 3 | "version": "1.0.0", 4 | "description": "Server for obtaining Apple Pay merchant sessions", 5 | "main": "server.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/tomdale/apple-pay-merchant-session-server.git" 10 | }, 11 | "author": "Tom Dale ", 12 | "bugs": { 13 | "url": "https://github.com/tomdale/apple-pay-merchant-session-server/issues" 14 | }, 15 | "homepage": "https://github.com/tomdale/apple-pay-merchant-session-server#readme", 16 | "dependencies": { 17 | "express": "^4.14.0", 18 | "request": "^2.72.0", 19 | "x509": "^0.2.6" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var express = require('express'); 3 | var x509 = require('x509'); 4 | var fs = require('fs'); 5 | 6 | var CERT_PATH = './apple-pay-cert.pem'; 7 | 8 | var cert = fs.readFileSync(CERT_PATH, 'utf8'); 9 | var merchantIdentifier = extractMerchantID(cert); 10 | 11 | var app = express(); 12 | 13 | app.use(function(req, res, next) { 14 | res.header("Access-Control-Allow-Origin", "*"); 15 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 16 | next(); 17 | }); 18 | 19 | app.get('/merchant-session/new', function(req, res) { 20 | var uri = req.query.validationURL || 'https://apple-pay-gateway-cert.apple.com/paymentservices/startSession'; 21 | 22 | var options = { 23 | uri: uri, 24 | json: { 25 | merchantIdentifier: merchantIdentifier, 26 | domainName: process.env.APPLE_PAY_DOMAIN, 27 | displayName: process.env.APPLE_PAY_DISPLAY_NAME 28 | }, 29 | 30 | agentOptions: { 31 | cert: cert, 32 | key: cert 33 | } 34 | }; 35 | 36 | request.post(options, function(error, response, body) { 37 | if (body) { 38 | // Apple returns a payload with `displayName`, but passing this 39 | // to `completeMerchantValidation` causes it to error. 40 | delete body.displayName; 41 | } 42 | 43 | res.send(body); 44 | }); 45 | }); 46 | 47 | var server = app.listen(process.env.PORT || 3000, function() { 48 | console.log('Apple Pay server running on ' + server.address().port); 49 | console.log('GET /merchant-session/new to retrieve a merchant session'); 50 | }); 51 | 52 | function extractMerchantID(cert) { 53 | try { 54 | var info = x509.parseCert(cert); 55 | console.log(info); 56 | return info.extensions['1.2.840.113635.100.6.32'].substr(2); 57 | } catch (e) { 58 | console.error("Unable to extract merchant ID from certificate " + CERT_PATH); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apple Pay Merchant Session Server 2 | 3 | At WWDC 2016, Apple announced they are bringing Apple Pay to Safari via 4 | the [Apple Pay JS 5 | "Framework"](https://developer.apple.com/reference/applepayjs). For an 6 | overview, I recommend watching the [recorded WWDC 7 | talk](https://developer.apple.com/videos/play/wwdc2016/703/), which 8 | covers the details of the architecture. I expect Apple Pay on the web to 9 | be a game changer, because it makes online payment so incredibly 10 | frictionless. 11 | 12 | Unfortunately, Apple Pay for the Web has launched in what I would 13 | consider to be a pre-beta state. Documentation is sparse, and some 14 | critical pieces are undocumented completely. 15 | 16 | In particular, before processing a payment, Apple requires that you 17 | retrieve a "merchant session" using a developer-specific certificate. 18 | This ensures that a compromised site can have its certificate revoked by 19 | Apple if fraudulent charges start appearing. 20 | 21 | As of this writing, the process for retrieving a merchant session is 22 | undocumented and you have to reverse engineer your way into a working 23 | system. This server tries to encode everything I've learned about how to 24 | get a merchant session into a server that you can deploy. 25 | 26 | ## Warnings 27 | 28 | Apple Pay for the Web is still very much a work in progress, and this 29 | server could stop working at any time. Additionally, this is just a 30 | proof of concept; there is no real error handling or security to speak 31 | of. Please use this as a basis for your own work, or submit PRs to 32 | improve it, but at this point I would not consider it production-ready. 33 | 34 | ## How It Works 35 | 36 | Before beginning, you'll want to watch the [WWDC 37 | video](https://developer.apple.com/videos/play/wwdc2016/703/) to 38 | familiarize yourself with the high-level architecture. 39 | 40 | Towards the middle of the session, they will describe the "Merchant 41 | Validation" flow. They show this sample code: 42 | 43 | ```js 44 | session.onvalidatemerchant = function (event) { 45 | var promise = performValidation(event.validationURL); 46 | promise.then(function (merchantSession) { 47 | session.completeMerchantValidation(merchantSession); 48 | }); 49 | } 50 | ``` 51 | 52 | Unfortunately, the `performValidation` function is not shown, nor is it 53 | really explained in any detail. All you know is that you have a 54 | validation URL to get it from. 55 | 56 | ### Obtaining a Certificate 57 | 58 | In order to validate yourself as a merchant, you will need a **Merchant 59 | ID** and an **Apple Pay Merchant Identity Certificate** (note that this 60 | is _not_ the same as an Apple Pay Certificate). You can create these at 61 | . Make 62 | sure you also have verified your domain name. 63 | 64 | Once completed, you should have a certificate you can download and add 65 | to Keychain Access on your Mac. Once added to Keychain Access, it should 66 | up under the Certificates category and start with "Merchant ID: 67 | <your-merchant-id>". It should also have a disclosure triangle to 68 | the left of it, and when clicked, it should show a private key 69 | associated with the certificate. 70 | 71 | ### Converting the Certificate to PEM 72 | 73 | Next we need to convert the certificate into a format that Node.js 74 | understands. Click the certificate, right click, and choose "Export 75 | <certificate-name>". Make sure the export format is set to 76 | Personal Information Exchange (.p12) and save it into this server's git 77 | repo as `apple-pay-cert.p12`. If it asks you to pick a passphrase, you 78 | can just leave it blank since we'll be removing it in a second anyway. 79 | 80 | Now we'll convert from a .p12 file into a .pem file. Run this command in 81 | your terminal: 82 | 83 | ``` 84 | openssl pkcs12 -in apple-pay-cert.p12 -out apple-pay-cert.pem -nodes -clcerts 85 | ``` 86 | 87 | This converts the certificate from .p12 to .pem. 88 | 89 | ### Starting the Server 90 | 91 | Start the server by typing `npm start`. By default it will listen on 92 | port 3000, or you can set the `PORT` environment variable. 93 | 94 | The app has one route: GET 95 | `/merchant-session/new?validationURL=`. 96 | 97 | When you deploy the server, ensure you have the 98 | `apple-pay-cert.pem` file in the root of the server. 99 | 100 | You will also need to set the following environment variables: 101 | 102 | | `APPLE_PAY_DOMAIN_NAME` | The domain name where the merchant session will be used. | 103 | |------------------------|----------------------------------------------------------| 104 | | `APPLE_PAY_DISPLAY_NAME` | The display name of your Merchant ID from Apple. | 105 | 106 | ### Getting a Merchant Session 107 | 108 | To get a merchant session, take the `validationURL` passed to your 109 | `onvalidatemerchant` event handler, make an XHR request to this server 110 | with the validation URL in the query string, and once the request 111 | returns, pass it to `completeMerchantValidation`. The whole thing might 112 | look like this: 113 | 114 | ```js 115 | session.onvalidatemerchant = ({ validationURL }) => { 116 | fetch('https://apple-pay.example.com/merchant-validation/new') 117 | .then(res => res.json()) 118 | .then(json => { 119 | session.completeMerchantValidation(json); 120 | }); 121 | }; 122 | ``` 123 | 124 | ## Thanks 125 | 126 | A huge thanks to [Chris Boulton 127 | (@surfichris)](https://twitter.com/surfichris) who figured all of this 128 | out before I did and walked me through it. His crucial insight was that 129 | the merchant ID passed to the Apple server is not the human readable 130 | form, but a binary value embedded in the certificate, which this server 131 | attempts to extract automatically. 132 | 133 | This work was extracted from a project I am working on for 134 | [Monegraph](https://monegraph.com). 135 | 136 | ## License 137 | 138 | MIT 139 | --------------------------------------------------------------------------------