42

I have been playing around with in app purchases for a few days, everything works fine up until the point where I try to validate the receipt with the app store, as i am constantly getting back an invalid status.

I am passing the receipt data to my PHP server then forwarding from there to the app store and once I get a valid response I intend to add the receipt data to my database.

The store kit programming guide and the class references are less than useless for this particular area as they don't really give you any sort of example, I did find one useful article which helped me out a bit but something is still wrong.

Basically I am wondering if someone who has receipt validation working would be willing to share their code as I'm getting nowhere.

Thanks

Alex Bitek
  • 6,529
  • 5
  • 47
  • 77
Andy
  • 423
  • 1
  • 5
  • 4

6 Answers6

72

First, there are a few typos in the posted code. Try this. (Disclaimer: Refactoring et. al is left as an exercise for the readership!)

- (BOOL)verifyReceipt:(SKPaymentTransaction *)transaction {
    NSString *jsonObjectString = [self encode:(uint8_t *)transaction.transactionReceipt.bytes length:transaction.transactionReceipt.length];      
    NSString *completeString = [NSString stringWithFormat:@"http://url-for-your-php?receipt=%@", jsonObjectString];               
    NSURL *urlForValidation = [NSURL URLWithString:completeString];       
    NSMutableURLRequest *validationRequest = [[NSMutableURLRequest alloc] initWithURL:urlForValidation];              
    [validationRequest setHTTPMethod:@"GET"];         
    NSData *responseData = [NSURLConnection sendSynchronousRequest:validationRequest returningResponse:nil error:nil];  
    [validationRequest release];
    NSString *responseString = [[NSString alloc] initWithData:responseData encoding: NSUTF8StringEncoding];
    NSInteger response = [responseString integerValue];
    [responseString release];
    return (response == 0);
}

- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length {
    static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
    uint8_t *output = (uint8_t *)data.mutableBytes;

    for (NSInteger i = 0; i < length; i += 3) {
        NSInteger value = 0;
        for (NSInteger j = i; j < (i + 3); j++) {
            value <<= 8;

            if (j < length) {
                value |= (0xFF & input[j]);
            }
        }

        NSInteger index = (i / 3) * 4;
        output[index + 0] =                    table[(value >> 18) & 0x3F];
        output[index + 1] =                    table[(value >> 12) & 0x3F];
        output[index + 2] = (i + 1) < length ? table[(value >> 6)  & 0x3F] : '=';
        output[index + 3] = (i + 2) < length ? table[(value >> 0)  & 0x3F] : '=';
    }

    return [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease];
}

You can make these Internal methods on the class that handles your SKPaymentTransactionObserver messages:

@interface YourStoreClass (Internal)
- (BOOL)verifyReceipt:(SKPaymentTransaction *)transaction;
- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length;
@end

Note: You could use something like libcrypto to handle base64 encoding, but then you're looking at export restrictions and extra steps at app approval time. But I digress ...

Then, wherever you intend to kick-off recording the transaction on your remote server, call verifyReceipt: with your transaction and make sure it comes back positive.

Meanwhile, on your server, here's some super-stripped-down PHP to handle things:

$receipt = json_encode(array("receipt-data" => $_GET["receipt"]));
// NOTE: use "buy" vs "sandbox" in production.
$url = "https://sandbox.itunes.apple.com/verifyReceipt";
$response_json = call-your-http-post-here($url, $receipt);
$response = json_decode($response_json);

// Save the data here!

echo $response->status;

Where call-your-http-post-here is your favorite HTTP post mechanism. (cURL is one possible choice. YMMV. PHP.net has the scoop!)

One thing that has me slightly concerned is the length of the payload in the URL going from the app to the server (via GET). I forget if there's a length issue there per the RFCs. Maybe it's OK, or maybe it's server-specific. (Readers: Advisement welcome on this part!)

There may also be some balking at making this a synchronous request. You may want to post it asynchronously and put up the ol' UIActivityIndicatorView or some other HUD. Case in point: That initWithData:encoding: call takes a loooooong time for me. A few seconds, which is a small eternity in iPhone land (or anywhere else online, for that matter). Showing some sort of indeterminate progress indicator may be advisable.

Undo
  • 25,519
  • 37
  • 106
  • 129
Joe D'Andrea
  • 5,141
  • 6
  • 49
  • 67
  • Thanks for ths finally got it working, think my main problem was in my php code. Thanks again, Andy – Andy Aug 25 '09 at 10:07
  • 1
    Hi, using the script you provided here, I receive the following response from the server: 21002... can you please tell me what does that mean? I see another guy on this post mentioning a similar problem. thanks for your time. – Duck Oct 04 '09 at 03:07
  • Hmm … can't say I've ever seen that response. :( The only thing I would want to double-check is that you're doing a HTTP _post_ and not a HTTP _get_. That is, the payload goes in the post data, not the query string. – Joe D'Andrea Oct 07 '09 at 02:51
  • 6
    You should note that this is vulnerable to replay attacks using someone else's receipt. Also, your iPhone application is vulnerable to a server spoof attack. See this answer for how to fix these vulnerabilities: http://stackoverflow.com/questions/1581246/how-can-my-server-securely-authenticate-iphone-in-in-app-purchase/1794470#1794470 – Ben S Nov 25 '09 at 14:45
  • Thank you VERY MUCH Ben! I'll read that and see if I can adjust the code above to account for the changes. – Joe D'Andrea Nov 25 '09 at 15:52
  • Ahh, good - it turns out I'm already storing the receipt and UDID pair in a database, but that was long after I posted this. (There's so much more this can do. It's more of a starting point, but the security vulnerabilities are important and should be nipped in the bud, agreed.) The only gotcha for me here is that I'm using subscriptions in my case, which don't involve restored transactions (or so I'm told). Thoughts? – Joe D'Andrea Nov 25 '09 at 15:56
  • zardon: There's no shared secret. The payload is already encrypted. You just send it off to Apple's verifyReceipt API endpoint and they do the rest. (See Chris Maddern's response for a handy PHP implementation!) – Joe D'Andrea Mar 08 '12 at 00:30
  • arnaud576875: Thanks for the adjustment (from print to echo). Every bit of performance helps! – Joe D'Andrea Jul 18 '12 at 16:15
  • Thanks. I was accidentally using urlencode instead of jsonencode for my params when I sent the POST request :) – Evan Layman Apr 27 '13 at 21:14
  • Thanks Benoit for the comment. I was getting status 0 from the replay attacks. – berkayk Oct 08 '14 at 12:26
  • still im gettitnd status = 21002 – Patel Jigar Dec 24 '15 at 08:58
  • Well it's certainly ancient history. Also, I wrote the Disclaimer for a reason. :) – Joe D'Andrea Sep 04 '16 at 01:39
4

For anyone who's wondering how to handle connection or verification errors that might occur when you're using the In-App-Purchase server model. Receipt validation ensures that the transaction is complete and successful. You don't want to do that from the iPhone because you can't really trust the user's phone.

  1. The user initiates an in-app purchase
  2. When complete, the app asks your server for validation
  3. You validate the receipt with Apple: if it's valid, you can perform whatever action linked to the purchase (unlock/deliver content, register subscription...)
  4. The app removes the transaction from the queue (finishTransaction)

If the server is down, you shouldn't finish the transaction, but display an "unavailability message" to the user.

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

will be called again later.

But if you find out that a receipt is invalid, you should finish the associated transaction. If not, you may have extra-transactions living forever in the transaction queue. That means that each time your app runs, paymentQueue:updatedTransaction: will be called once per transaction...

In my apps, receipt validation is done through a web service, returning an error code in case of an invalid receipt. That's why an external server is needed. If a user somehow manages to skip receipt validation (by faking the web service "success" response), he won't be able to unlock the content / access functionality because the server has no trace of the purchase.

leviathan
  • 11,080
  • 5
  • 42
  • 40
1

You must send the receipt as a file to your PHP server. In your PHP side you can use this script to validate:

<?php

$path = 'receipt'; // $_FILE['receipt-data']["tmp_name"];
$receipt = file_get_contents($path);

$json['receipt-data'] = base64_encode($receipt);

$post = json_encode($json);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,"https://buy.itunes.apple.com/verifyReceipt");
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
$result=curl_exec ($ch);

curl_close ($ch);

?>

https://gist.github.com/eduardo22i/9adc2191f71ea612a7d071342e1e4a6f

Eduardo Irias
  • 1,043
  • 11
  • 12
1

After fighting with this for awhile, I finally found a listing of status codes in Apple's documentation, including the dreaded 21002 (which is "The data in the receipt-data property was malformed."). While I've seen reports of other status codes not included in this list, I have thus far not seen any beyond what Apple has documented. Note that these codes are only valid for auto-renew subscriptions, not other sorts of in-app purchases (or so the document says).

The document in question can be found here.

t-dub
  • 812
  • 6
  • 6
0

Just to open this again and add my 2-cents in return for scourging these forms for information.

I just setup an IAP service in my app and ran into the same 21002 error. I found the 21002 happens when either the post to your PHP server is empty (thus the HTTP request to the app store is empty) or improperly formatted. To get ours working, on the iPhone side we set the post data in a NSString as base64 encoded then sent it to our server as a HTTP request.

Then on our server, we stuck it into and array and json-ed it. Like this:

$receipt = json_encode(array("receipt-data"=>$_POST['receipt-data']));

You'll notice it is the same as above except we are using a POST instead of a GET. Personal preference really.

We then used CURL to post it to the sandbox and used json_decode on the response.

  • I've been having trouble using CURL with my request. The request comes back blank. And I've verified that my $receipt is equal to the 64 bit encoded string using the objective c code above $receipt = json_encode(array("receipt-data" => $_GET["receipt"])); $url = "https://sandbox.itunes.apple.com/verifyReceipt"; echo 'Receipt: '.$receipt; $ch = curl_init(); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $receipt); curl_setopt($ch, CURLOPT_URL, 'https://sandbox.itunes.apple.com/verifyReceipt'); $response_json = curl_exec($ch); Any ideas? – Convolution Aug 12 '10 at 14:29
-2

If you are getting null responses or error codes eg 21002, try adding these lines. If you checked the curl error codes, it is a SSL certificate error...

curl_setopt ($curl_handle, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt ($curl_handle, CURLOPT_SSL_VERIFYPEER, 0);
  • 5
    Blindly disabling peer verification is not smart. This allows anyone else to impersonate the Apple verification server, and security is the exercise here. – David Snabel-Caunt Feb 09 '12 at 16:01
  • Hi @Kyle Poole - you're right. Sort of. Unfortunately Apple's error codes overlap with the SSL Error Codes, so it could be either! Great planning eh?! – Chris Maddern Aug 08 '12 at 00:20