Loading
 
 

IAP receipt verification on your server using C#

After an in-app purchase is made in iOS, you need to call your web service to validate the receipt data. This extra step, while cumbersome, is necessary to prevent hackers and cheaters from faking the purchase.

I know that most iOS developers probably don’t use C# on their backend, but there must be some out there. Today I was searching for some code that would validate an in-app purchase receipt with Apple using C#. I came up empty, so I rolled my own, and thought I’d share it in case it’s helpful to others. I’ll show you part of the Objective-C client-side, and the C# server-side. I’m not going to show you how to do the actual IAP transaction. For that, I recommend checking out this tutorial.

Before we start, let me make sure you’ve seen Apple’s official documentation on the subject.

Prerequisites

My server-side code uses JSON.NET, which can be found here: http://json.codeplex.com/

Client Side

After we’ve completed the transaction, your code has an SKPaymentTransaction object. You need to call your web service with the item ID that was purchased, as well as the Base64 encoded receipt data. I recommend doing the encoding on the client. Here are some NSString categories that will do the job.

@implementation NSString (WolfAdditions)

static const char _base64EncodingTable[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

+ (NSString *)encodeBase64WithString:(NSString *)strData
{
 return [NSString encodeBase64WithData:[strData dataUsingEncoding:NSUTF8StringEncoding]];
}

+ (NSString *)encodeBase64WithData:(NSData *)objData
{
 const unsigned char * objRawData = [objData bytes];
 char * objPointer;
 char * strResult;
 
 // Get the Raw Data length and ensure we actually have data
 int intLength = [objData length];
 if (intLength == 0) return nil;
 
 // Setup the String-based Result placeholder and pointer within that placeholder
 strResult = (char *)calloc(((intLength + 2) / 3) * 4 + 1, sizeof(char));
 objPointer = strResult;
 
 // Iterate through everything
 while (intLength > 2) { // keep going until we have less than 24 bits
  *objPointer++ = _base64EncodingTable[objRawData[0] >> 2];
  *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
  *objPointer++ = _base64EncodingTable[((objRawData[1] & 0x0f) << 2) + (objRawData[2] >> 6)];
  *objPointer++ = _base64EncodingTable[objRawData[2] & 0x3f];
  
  // we just handled 3 octets (24 bits) of data
  objRawData += 3;
  intLength -= 3; 
 }
 
 // now deal with the tail end of things
 if (intLength != 0) {
  *objPointer++ = _base64EncodingTable[objRawData[0] >> 2];
  if (intLength > 1) {
   *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
   *objPointer++ = _base64EncodingTable[(objRawData[1] & 0x0f) << 2];
   *objPointer++ = '=';
  } else {
   *objPointer++ = _base64EncodingTable[(objRawData[0] & 0x03) << 4];
   *objPointer++ = '=';
   *objPointer++ = '=';
  }
 }
 
 // Terminate the string-based result
 *objPointer = '\0';
 
 // Return the results as an NSString object
 NSString *s = [NSString stringWithCString:strResult encoding:NSASCIIStringEncoding];
 free(strResult);
 return s;
}

@end

Armed with those categories, it's easy to encode the receipt data:

SKPaymentTransaction *t; // Assuming you've obtained your completed transaction object
NSString *receiptStr = [NSString encodeBase64WithData:t.transactionReceipt];

Use that to call your web service. I'm not going to show you how to do that. I use a modified version of Sudz-C to communicate with my WCF service via SOAP.

Server Side

On the server, let's assume your web service knows the item ID you purchased via StoreKit, as well as the Base64 encoded receipt data. Below is most of the code I use to verify the receipt with Apple. You can do this a few different ways, but I chose to check that a) the receipt is valid, b) the product ID matches what I expected, c) the application who sold the item matches what I expected, and d) the transaction occurred within 24 hours +/- of now. I give it that much leeway because I don't care that much, and I want to avoid any possible time zone glitches.

Why the above checks? Because one way IAP cheaters operate is by capturing a valid IAP receipt, and re-using that receipt over and over. We could be getting a valid receipt from some other app, or a receipt for some other product in our app, or a receipt that happened months ago. We can avoid all those things with the additional checks.

OK, here's the code. It's not commented well, but basically it's doing a JSON POST, then getting the HTTP response and parsing that using JSON.NET.

There are all kinds of little "gotchas" that hit me as I worked through this. I'm sorry for not walking you through all of them; I'll leave it up to you to work through what's going on here.

Hope it helps!

public static string[][] PurchaseItem(string itemID, string receiptData)
{
	try
	{
		// Verify the receipt with Apple
		string postString = String.Format("{{ \"receipt-data\" : \"{0}\" }}", receiptData);
		ASCIIEncoding ascii = new ASCIIEncoding();
		byte[] postBytes = ascii.GetBytes(postString);
		HttpWebRequest request;
		request = WebRequest.Create("https://buy.itunes.apple.com/verifyReceipt") as HttpWebRequest;
		request.Method = "POST";
		request.ContentType = "application/json";
		request.ContentLength = postBytes.Length;
		Stream postStream = request.GetRequestStream();
		postStream.Write(postBytes, 0, postBytes.Length);
		postStream.Close();
		HttpWebResponse response = request.GetResponse() as HttpWebResponse;
		StringBuilder sb = new StringBuilder();
		byte[] buf = new byte[8192];
		Stream resStream = response.GetResponseStream();
		string tempString = null;
		int count = 0;
		do
		{
			count = resStream.Read(buf, 0, buf.Length);
			if (count != 0)
			{
				tempString = Encoding.ASCII.GetString(buf, 0, count);
				sb.Append(tempString);
			}
		}
		while (count > 0);
		var fd = JObject.Parse(sb.ToString());

		// Receipt not valid
		if (fd["status"].ToString() != "0")
		{
			// Error out
		}

		// Product ID does not match what we expected
		var receipt = fd["receipt"];
		if (String.Compare(receipt["product_id"].ToString().Replace("\"", "").Trim(), itemID.Trim(), true) != 0)
		{
			// Error out
		}

		// This product was not sold by the right app
		if (String.Compare(receipt["bid"].ToString().Replace("\"", "").Trim(), "com.wolf-studios.warofwords-pro", true) != 0)
		{
			// Error out
		}

		// This transaction didn't occur within 24 hours in either direction; somebody is reusing a receipt
		DateTime transDate = DateTime.SpecifyKind(DateTime.Parse(receipt["purchase_date"].ToString().Replace("\"", "").Replace("Etc/GMT", "")), DateTimeKind.Utc);
		TimeSpan delay = DateTime.UtcNow - transDate;
		if (delay.TotalHours > 24 || delay.TotalHours < -24)
		{
			// Error out
		}

		// Perform the purchase -- all my purchases are server-side only, which is a very secure way of doing things

		// Success!
	}
	catch (Exception ex)
	{
		// We crashed and burned -- do something intelligent
	}
}