Essays on self-improvement, software development, and esports.
© 2022. All rights reserved.
Apple released the new StoreKit 2 API, and I’ve been working on updating Reiterate to use it.
While Apple is still supporting version 1 of StoreKit, they have stopped supporting Apple-hosted assets for non-consumable products, so all future products for Reiterate will have to use a new purchasing flow. Since I’m updating that, I might as well move everything over to the new API.
There’s two parts to the in-app purchase flow, the client side (on your phone), and the server side (on this server, right here!). When you make a purchase, the Reiterate app communicates the transaction to Apple via StoreKit. Apple verifies payment, and sends the verification back to the app, which then sends it to my own server for recording and additional verification. All sensitive data, like account balances, are kept on the server side, and the server acts as a “single source of truth”.
When the server receives a purchase transaction from a client, it then forwards that transaction to Apple for additional verification (otherwise, a hacked client could send fake purchases and get free stuff that way). The verification is handled through the new App Store Server API, which has also been updated from the old “Original API for In-App Purchase” (that’s its new official name, apparently).
The new API uses JSON Web Tokens to authenticate all transactions. The JWT are used on both sides: you need your own JWT to authenticate yourself to Apple’s servers, and Apple returns its own JWT which you can authenticate on your own to verify the response is indeed from Apple.
First, you need to create the keys that you will use to sign your own requests that you send to Apple. This is done via App Store Connect, under Users > Keys > In-App Purchase. Create a new key, then download it (it only gives you one chance to download). The key is just a text file; you can copy and paste it easily (and I did just that in the next step).
In addition to the key, you need the key id which is the 10-‘digit’ alphanumeric ID on the table, and your issuer id, which is on the other page. This is a little obscure, but you have to click on App Store Connect API to switch to that page, and then you will see your issuer ID, which is a standard UUID. Or, maybe you won’t see it. For some reason Apple doesn’t give you an issuer ID until you’ve made at least one App Store Connect API Key, so make one of those if you have never done that. You’re not actually going to use the App Store Connect Key; you just need one in your account in order to get an issuer id.
For a Rails app, the proper place to store things like signing keys is in the credentials file. You can edit the file and add your key info like so:
Note the use of the
|- syntax. Yaml has many different ways of handling multi-line strings. This one preserves the line breaks in the
middle of the string, but doesn’t add one at the end, which is how we want it for the private key.
The root certificate forms the foundation of trust for the entire trust chain. The JWT is signed by a sequence of keys that are all given in the same response, but the last key should be the root key. If we have our own copy of the root key, then we can be sure it came from Apple.
Apple displays all its root certificates on its Public Key Infrastructure page. You can download all the root certificates, but the only one we seem to need for JWT authentication is the “G3 Root”.
I created a directory at
config/certificates/apple and downloaded the certificate there.
Now we should have all the data we need to send and receive authenticated messages with the App Store Server API. Here is the relevant code I use to verify transactions.
I use an initializer to take the certificate I downloaded from Apple’s PKI, and turn it into an actual working certificate. It’s done in an initializer so I only have to do this once, at start up
If you have multiple root certificates in there, this will process all of them.
When a user purchases cheer in the app, the app will process the transaction with the StoreKit 2 API, and if that comes through okay then it sends the transaction to my server for recording and additional verification. The Rails controller is pretty simple.
I’ll break down each of these methods, but first there’s some utility code. When we send a request to Apple, it has to be signed with our own JWT. This method generates that for us by taking the info we saved in the credentials file, timestamping it, and then encrypting it all into a token.
Note this uses the
jwt gem, so make sure you add that to your
Next is a method that selects the proper host, depending on whether or not we are in a sandbox environment. You will probably have your own method of determining this, or maybe you just hardcode it.
Now we come to the real bits. This next method makes the call to the Apple Store API. It gets a token from our
and puts that in the header of the request. If you don’t do that, then you’ll get an
Unauthorized response back from the API.
We’re calling the
history endpoint, which in my mind is a little strange. You pass a transaction id to the endpoint as part of the URL,
but what it returns is not just that transaction. Instead, it looks up the account that that transaction belongs to, and gives you
a list of every transaction for that account. In fact, if you pass in parameters to filter the results, it’s possible you won’t get
the transaction you asked for at all!
Nonetheless, this seems to be the only way to look up a transaction, so we’ll use it.
Note that we’re filtering by
product_id, which the app passes in to our endpoint. And we also filter in descending order, so it will
return the most recent transaction first. This should, in theory, return the transaction we’re looking for as the first transaction in
Once we have a response back from the App Store Server, we need to check it to make sure the transaction is validated.
valid_purchase? method gets the
transaction_history and first of all checks that the App Store Server returned with an OK status.
If so, we grab the first transaction out of the response (which we assume is the transaction that we are verifying). This transaction is
actually another JSON Web Token, except this one is from Apple, so we need to unpack it and verify the signature. That’s done in the
authenticate_transaction starts by using the
JWT.decode method from the
jwt gem. Because the signing certificates are being passed
in the header of the token itself, we need to use the block parameter to actually pick out the key that’s used to decrypt the token.
After unpacking the certificates in the header, we perform two validation checks. First, we verify that the root certificate in the header matches the root certificate that we downloaded from Apple. Then, we check that each certificate signs the next one down the chain, all the way to the top certificate which is the one used to decrypt the token. Since we have an unbroken chain from the root certificate to the signing certificate, we can know with confidence that this token indeed came from Apple.
After the payload segment of the token is decrypted, we perform three additional checks to verify authenticity. First, we check that the account ID matches. This is a new feature in StoreKit 2 that really helps bring everything together. When the transaction is created, on the app side, a user account token is generated and added to the transaction. This account ID is passed in to the verification endpoint, from the app, and also from Apple, through the token payload. Checking that these match ensures that the transaction really did come from the account in question.
Next, we check that the product ID is the same (it’s also passed from the app), and the transaction ID matches as well. We’ve assumed all this time that the first transaction in the returned list is the transaction in question, and here we verify that with one additional check. If all these checks pass, then the transaction is considered valid.
Let’s look again at the top calling method
valid_purchase? method returns true, then
record_iap_transaction is called. I’m not going to document this here since it will
vary depending on your application, but basically it keeps a record of every purchase transaction and guards against replay attacks. A
replay attack is when someone tries to send the same valid transaction multiple times. If I didn’t check for that it might be possible
for them to buy an item once, but then they would get multiple credits for it.
That’s the final check. If everything passes, then the purchase is recorded and added to the user’s account.
I might package this up into a gem if there’s any interest for it. I can see some old gems for the old StoreKit, but nothing for the new server. If you’re interested in something like that, please let me know in the comments.