All Ruby on Rails Node JS Android iOS React Native Frontend

A recipe for Apple In-App Purchases with Ruby

In-App Purchases (IAP) is a widely used method for unlocking content in an iOS application. Though the "heavy lifting" (the payment itself) happens on the client side, it's highly probable that you will need to somehow respond to that payment on the server side, and that is what I'd like to talk about briefly in this article.

The problem

Imagine you have an iOS app - a multiplayer RPG game, where players can earn gold through various actions. One day, you decide that your game should change from a free to play to a "pay to win" model (oh, why would you do that to your awesome game?!). You want to allow players to buy in-game gold via IAP. The amount of gold each player has is stored on the server. So... how do you make the purchase done from iOS really have an effect on the server side? You of course have to let the server know. The mobile app needs to talk to the server by some endpoint.

The first, albeit naive thought would be to expose an endpoint where the mobile app sends the amount of gold the user has bought, and the server updates the user's total gold. But what if an evil hacker discovers the endpoint somehow, then starts giving himself huge amounts of gold and is going to become the number one player? You wouldn't want that, would you?

To get around this, Apple gives us a way to verify that a purchase has really been made on the server side. We can also check which products were bought. So no one can trick us that they bought more gold than they did. Splendid! Now, back to the business.

The Apple Receipt

When the mobile app performs a purchase, it gets a receipt object from Apple. As far as I was concerned, there are a few representations of the receipt, depending on your mobile tech (native code or React Native). What we are interested on the server side is a base64 representation of the receipt which the mobile app can provide us with.

On the server side, we can use this base64 string to verify it directly with Apple. You can of course build the verification process on your own, without any libraries - I have checked the existing ones and so far can recommend you the Monza gem. It does not do a lot of heavy lifting, but is a nice wrapper for HTTP request preparation and response parsing.

A basic snippet for performing verification with Monza would be: 

    params do
      requires :base64_receipt, type: String
    end
    post '/iap_purchases' do
      begin
        response = Monza::Receipt.verify(params[:base64_receipt])
        head 200
      rescue Monza::VerificationResponse::VerificationError => e
        { apple_status_code: e.code, message: e.message }
      end
    end

 

Indeed - it's that simple to verify the receipt. But... this code doesn't really do much, right? Yeah. Remember? We wanted to give the user gold based on what he bought. Okay, so now we need to understand what products were bought.

Quantity & Product Identifier

For the sake of simplicity, let's assume that you only want to sell "Gold Packs". Let's say, only one kind for now - "1000 gold pack". Here I assume the product is already registered through the Apple Console, as it is what has to be done in order for the mobile app to perform the purchase.

I also strongly suggest to have a look at the following documents from Apple:
Remote validation (includes information about a few of the fields)
Receipt fields (includes information about the bought items)

If you do so, you can see that the receipt exposes all the data we need. The two very important fields to us are product identifiers and respective quantities. The following snippet shows how you can get them with Monza's help.

    params do
      requires :base64_receipt, type: String
    end
    post '/iap_purchases' do
      begin
        response = Monza::Receipt.verify(params[:base64_receipt])
        products = response.receipt.in_app
        product_ids_with_quantity = products.map do |product|
          [product.product_id, product.quantity]
        end
        # Hmm... how do I know how much gold should I give for each Product id?
        # Did we miss something?
        head 200
      rescue Monza::VerificationResponse::VerificationError => e
        { apple_status_code: e.code, message: e.message }
      end
    end

 

And now we see one more issue - how do we know how much gold should be given for the bought products? We have the product ids, the product quantity... The answer is - we miss a custom mapping between the products that are registered in the Apple Console and our application database.

Apple Console / Application database mapping

Assuming you're using ActiveRecord/Rails, create a model called IapProduct with apple_product_id and gold_amount fields. Create records that resemble the state of your Apple Console.

For example, if you have an Apple product with id myawesomegame.goldpack_1000 (when it's bought you want to give the buyer 1000 gold) create an IapProduct with:


IapProduct.create!(
  apple_product_id: 'myawesomegame.goldpack_1000',
  gold_amount: 1000
)

The endpoint's final shape

Now we're ready to give our simple http endpoint the final shape:

    params do
      requires :base64_receipt, type: String
    end
    post '/iap_purchases' do
      begin
        response = Monza::Receipt.verify(params[:base64_receipt])
        gold_sum = response.receipt.in_app.reduce(0) do |memo, product|
          iap_product = IapProduct.find(product.product_id)
          memo + iap_product.gold_amount * product.quantity
        end
        
	    # Update your user's gold in some way, eg.
        current_user.increment!(:gold, gold_sum)
{ message: "User gold updated by #{gold_sum}!" } rescue Monza::VerificationResponse::VerificationError => e { apple_status_code: e.code, message: e.message } end end

Filling the last gap

Actually, there's still one more way to exploit the system. Consider someone really buys a gold pack. Then, he sends the receipt he received an arbitrary amount of times to our endpoint. What would happen? Well, he would be able to stack his gold indefinitely. Not something we want either.

We definitely need to somehow prevent that. First, we definitely need to store all the receipts we received in our database, let's say, with a base64_receipt field. Then, we have a few options to choose from:


1) Create a unique index in your database on md5(base64_receipt) - the base64 is a very long string and applying a unique index on it could quickly result in poor database performance. Thus, an md5 of it. Depending on your app traffic, it might give a low enough chance of hash collision. Keep in mind - though with a small volume of data the chance of a collision is negligibly small, with huge volumes it can become a real issue.


2) Create a special field in the receipts table specifically for uniqueness verification purposes. I suggest to take a look again at the shape of the receipt from Apple docs, and use some of the verification response's unique data with your internal user id to be a fit for you if you want to be certain that a collision never occurs. If you want to perform some validation in Ruby, not only an SQL index based verification, make sure you use database table locks to prevent the possibility of concurrent requests race condition exploit.

 

But, please, do not implement a pay to win monetisation in any multiplayer game.

Photo by NordWood Themes on Unsplash

We're building our future. Let's do this right - join us
New Call-to-action
READ ALSO FROM Ruby/Ruby on Rails
Read also
Need a successful project?
Estimate project or contact us