Steps for Verifying iOS In-App Purchase Receipts

iOS
2015-06-30 00:30 (10 years ago)
Steps for Verifying iOS In-App Purchase Receipts

Processing on iOS Devices

In SKPaymentTransactionObserver's

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions

If transaction.transactionState == SKPaymentTransactionStatePurchased is reached, it means the transaction with Apple has been completed. (transaction is an element of transactions)

#pragma mark - SKPaymentTransactionObserver
  • (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchasing: // Purchasing process ... break; case SKPaymentTransactionStatePurchased: // Purchase complete [queue finishTransaction:transaction]; [self purchaseProcedure:transaction]; break; case SKPaymentTransactionStateFailed: // Purchase failed ... [queue finishTransaction:transaction]; break; case SKPaymentTransactionStateRestored: // Purchase restored ... [queue finishTransaction:transaction]; break; default: // Leave alone [queue finishTransaction:transaction]; break; } } }

Upon purchase completion, send the receipt to the server for processing.

There are two types of logs to send to the server:

- (void)purchaseProcedure:(SKPaymentTransaction *)transaction {
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
    NSString *base64receiptData = [receiptData base64EncodedStringWithOptions:0];
// Deprecated, but send it just in case
NSString *base64TransactionReceipt = [transaction.transactionReceipt base64EncodedStringWithOptions:0];

Thus, we can create the following strings:

  • base64receiptData (hereafter referred to as receiptData)
  • base64TransactionReceipt (hereafter referred to as transactionReceipt)

Send these to the server along with transaction.transactionIdentifier.

Server-side Processing

When the server receives these receipt details, it will request Apple for validation.

The code for the receipt validation request looks like this (Python):

import requests
import json

class AppleReceiptVerifyStatusError(Exception): pass

class AppleReceipt(object): @staticmethod def verify_request(receipt_data): url = 'https://buy.itunes.apple.com/verifyReceipt'

    data = {'receipt-data': receipt_data}
    headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
    r = requests.post(url, data=json.dumps(data), headers=headers)
    # print(r.content)
    if r.status_code == 200:
        parsed = json.loads(r.content.decode('utf-8'))
        if parsed['status'] == 21007:
            // It was a test environment transaction
            return AppleReceipt.verify_request_sandbox(receipt_data)
        return parsed
    else:
        raise AppleReceiptVerifyStatusError(r.status_code)

@staticmethod
def verify_request_sandbox(receipt_data):
    url = 'https://sandbox.itunes.apple.com/verifyReceipt'

    data = {'receipt-data': receipt_data}
    headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
    r = requests.post(url, data=json.dumps(data), headers=headers)
    # print(r.content)
    if r.status_code == 200:
        return json.loads(r.content.decode('utf-8'))
    else:
        raise AppleReceiptVerifyStatusError(r.status_code)

Validating receiptData

Replace ' ' with '+' in the receiptData received from the iOS device, and pass it to AppleReceipt.verify_request().

verify_result = AppleReceipt.verify_request(
    post_data['receiptData'].replace(' ', '+'))

Check the following content in the returned verify_result:

  • verify_result['status'] == 0
  • verify_result['receipt']['in_app'] is an array with at least one element

For elements of verify_result['receipt']['in_app']:

  • All elements' ['product_id'] should match the products of your app

One of the elements should satisfy the following condition:

  • ['transaction_id'] matches the transactionIdentifier at the time of sale

If the purchase is not a restore,

  • ['original_transaction_id'] matches the transactionIdentifier at the time of sale

* original_transaction_id appears to contain the actual transactionIdentifier at the time of purchase during a restore. I haven't confirmed this since I've only sold non-consumable items.

Validating transactionReceipt

This method is deprecated and may become unusable in the future, but it's still possible, so we do it just in case.

verify_result = AppleReceipt.verify_request(
    post_data['transactionReceipt'])

Check the following content in the returned verify_result:

  • verify_result['status'] == 0
  • verify_result['receipt']['product_id'] matches the products of your app
  • verify_result['receipt']['transaction_id'] matches the transactionIdentifier at the time of sale

If the purchase is not a restore,

  • verify_result['receipt']['original_transaction_id'] matches the transactionIdentifier at the time of sale

Other

Ensure that the transactionIdentifier is not one that has already been processed (not a replay attack).

Supplement

Receipt Validation Programming Guide (TP40010573 0.0.0) - ValidateAppStoreReceipt.pdf https://developer.apple.com/jp/documentation/ValidateAppStoreReceipt.pdf

Be careful of tools that crack in-app purchases. Requests from these tools occasionally appear.

There seems to be a tool circulating that can crack in-app purchases and pass Apple's server authentication - @Yoski Hatena Separate Room http://yoski.hatenablog.com/entry/2013/03/22/Apple%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%82%92%E9%80%9A%E9%81%8E%E3%81%99%E3%82%8B%E3%82%A2%E3%83%97%E3%83%AA%E5%86%85%E8%AA%B2%E9%87%91%E3%81%AE%E3%82%AF%E3%83%A9%E3%83%83_

Currently unrated
The author runs the application development company Cyberneura.
We look forward to discussing your development needs.

Categories

Archive