bizz84 / SwiftyStoreKit

Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+ ⛺
MIT License
6.56k stars 797 forks source link

`/verifyReceipt` endpoint is forbidden by Apple #490

Open gerchicov-bp opened 5 years ago

gerchicov-bp commented 5 years ago

Platform

In-app purchase type

Environment

Version

Latest (0.15.0), maybe all versions

Related issues

Report

Issue summary

"Important: Do not call the App Store server /verifyReceipt endpoint from your app."

What did you expect to happen

verifyReceipt endpoint is not used (how it is explained in the official Apple documentation)

What happened instead

verifyReceipt endpoint is used

canhth commented 5 years ago

Hi @gerchicov-bp, Do you know any case got rejected by Apple because of this reason? I'm about to use this approach to avoid BE workload, hope this won't be an issue.

gerchicov-bp commented 5 years ago

@canhth the problem I faced is another one - my customers want to have a possibility to move apps from a publisher to another and it is not possible when receipts are checked with remote server.

revg75 commented 4 years ago

Yes, you are correct, Apple says:

Do not call the App Store server verifyReceipt endpoint from your app. You also never want to store your app-specific shared secret in your app binary either.

This can actually be fixed rather easily. Look at the struct AppleReceiptValidator. It conforms to the ReceiptValidator protocol. The AppleReceiptValidator calls Apple's verifyReceipt endpoint from the device which requires you to store your App-Specific Shared Secret in the binary which is not safe! You need to call Apple's verifyReceipt endpoint from your own server in order to be safe.

So in my case, I am using PHP so I make a new PHP page that mimics exactly what apple's verifyReceipt.php does, except that it only requires the receipt-data field because I will provide the app-specific shared secret from my server instead as shown below. My PHP page will return the exact same data as Apple's verifyReceipt page does so it will still be compatible with SwiftyStoreKit

<?php
header('Content-type: application/json');

$encoded = file_get_contents('php://input');
$data = json_decode($encoded, true);

const APPLE_SHARED_SECRET =               "<insert your shared secret here>";
const APPLE_RECEIPT_VERIFY_URL =          "https://buy.itunes.apple.com/verifyReceipt";
const APPLE_SANDBOX_RECEIPT_VERIFY_URL =  "https://sandbox.itunes.apple.com/verifyReceipt";

$request = json_encode(array("receipt-data" => $data["receipt-data"],"password"=>APPLE_SHARED_SECRET));
$ch = curl_init(APPLE_RECEIPT_VERIFY_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
$jsonresult = curl_exec($ch);
$jsonresult = json_decode($jsonresult);
$http_status = $jsonresult->status;
curl_close($ch);

if($http_status == 21007){
      $ch = curl_init(APPLE_SANDBOX_RECEIPT_VERIFY_URL);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($ch, CURLOPT_POST, true);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
      $jsonresult = curl_exec($ch);
      $jsonresult = json_decode($jsonresult);
      $http_status = $jsonresult->status;
      curl_close($ch);
}

echo json_encode($jsonresult);
?>

Create a new struct that conforms to the ReceiptValidator protocol. Then copy and paste all the code out of AppleReceiptValidator into that struct. In the VerifyReceiptURLType enum add an entry called custom that points to a page on your server such as

case custom = "https://www.mywebsite.com/verifyReceipt.php"

Now change the default VerifyReceiptURLType to .custom in the public init function. You might get an error about how receiptStatus.isValid is inaccessible due to protection level, so instead of checking isValid just do this:

if receiptStatus == .valid {
    // The receipt is valid...
}

Since we check for the 21007 error code on our PHP page we can also get rid of that check for .testReceipt too.

I hope this helps someone 🥂 !