apple / app-store-server-library-node

MIT License
162 stars 31 forks source link

Original Store Kit App Receipt Server Validation #130

Closed Apidcloud closed 4 months ago

Apidcloud commented 4 months ago

Hello,

This is similar to https://github.com/apple/app-store-server-library-node/issues/106, but with more detail.

TLDR: what's the flow one should follow to validate incoming Store Kit 1 app receipts? How do we validate that the purchase effectively happened? Would it be enough to just rely on the App Store Notifications V2 with a appAccountToken?

Long description (also for SEO in case people search for some of these keywords): I'm using the flutter package in_app_purchase, which seems to be using the original store kit. After making a purchase, I'm able to retrieve an encoded app receipt that follows this pattern: MIIUZwYJKoZIhvcNAQcCoIIUWDCCFFQCAQE...YHtBSZ/AzFBDREBwc0gprDVu7/Hd3KF9rxE3Q==

From https://github.com/apple/app-store-server-library-node/issues/52 I can see you recommend the use of ReceiptUtility and extractTransactionIdFromAppReceipt, which seems to work on the encoded string from above. Then you say that we can then "call the App Store Server API to get a signed transaction", but I'm a bit confused about this process.

The docs of this project say to use the extracted transaction id for further apple store api calls, like getTransactionInfo or getTransactionHistory, correct? But how can I match the incoming encoded app receipt with the correct transaction? Isn't that the whole point?

Or is it enough to do the following and not loop the whole history?

    const client = new AppStoreServerAPIClient(
          this._config.appleInApp.applePrivateKey,
          this._config.appleInApp.appleKeyID,
          this._config.appleInApp.appleIssuerID,
          this._config.appleInApp.appleBundleID,
          Environment.SANDBOX,
    );

    const receiptUtil = new ReceiptUtility();
    const transactionId = receiptUtil.extractTransactionIdFromAppReceipt(
      'MIIUZwYJKoZIhvcNAQcCoIIUWDCCFFQCAQE...YHtBSZ/AzFBDREBwc0gprDVu7/Hd3KF9rxE3Q==',
    );

    if (transactionId !== null) {
        try {
            const transactionInfo = await client.getTransactionInfo(transactionId);

            const rootCertificates = await Promise.all([
                fs.readFile(`${APPLE_CERTIFICATES_DIR}AppleRootCA-G2.cer`),
                fs.readFile(`${APPLE_CERTIFICATES_DIR}AppleRootCA-G3.cer`),
                fs.readFile(
                  `${APPLE_CERTIFICATES_DIR}AppleComputerRootCertificate.cer`,
                ),
                fs.readFile(`${APPLE_CERTIFICATES_DIR}AppleIncRootCertificate.cer`),
              ]);

              const verifier = new SignedDataVerifier(
                rootCertificates,
                true,
                Environment.SANDBOX,
                this._config.appleInApp.appleBundleID,
                undefined,
              );

              await verifier.verifyAndDecodeTransaction(
                info.signedTransactionInfo as string,
              );

              // if it reaches this point then we consider the purchase valid, correct?
        }
        catch (e) {
            // fail validation
        }
    } else {
        // fail validation
    }

I guess in general I'm just a bit confused about the whole process. This also makes me question: If we do call our server to validate the original store kit purchase, then there is no need to receive apple store notifications, right?

alexanderjordanbaker commented 4 months ago

@Apidcloud The value you are referencing is an App Receipt. It does not contain just one transaction, it contains all of the user's transactions. Therefore it isn't possible to directly link that single Base64 encoded value to a purchase, it is really just a representation of the user. By calling Get Transaction History you are receiving that user's history with the App Store Server API instead of the deprecated Verify Receipt endpoint, but they serve similar purposes. Both are going to give you an entire history, not just a single transaction. If you are looking for a single transaction, we recommend using StoreKit 2 as it makes it easier to understand what transaction is new and act on that transaction alone.

Apidcloud commented 4 months ago

I see. But what’s the correct way of validating then (using the original store kit)? Looping the whole history to then do what, exactly?

On 4 May 2024, at 03:29, Alex Baker @.***> wrote:



@Apidcloudhttps://github.com/Apidcloud The value you are referencing is an App Receipt. It does not contain just one transaction, it contains all of the user's transactions. Therefore it isn't possible to directly link that single Base64 encoded value to a purchase, it is really just a representation of the user. By calling Get Transaction History you are receiving that user's history with the App Store Server API instead of the deprecated Verify Receipt endpoint, but they serve similar purposes. Both are going to give you an entire history, not just a single transaction. If you are looking for a single transaction, we recommend using StoreKit 2 as it makes it easier to understand what transaction is new and act on that transaction alone.

— Reply to this email directly, view it on GitHubhttps://github.com/apple/app-store-server-library-node/issues/130#issuecomment-2093944771, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AA4MM4XKSG3HMI3V2PLKKDLZAQ2XTAVCNFSM6AAAAABG74L2S2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAOJTHE2DINZXGE. You are receiving this because you were mentioned.Message ID: @.***>

alexanderjordanbaker commented 4 months ago

@Apidcloud Generally you would then know what the user purchased and provision content accordingly.

Apidcloud commented 4 months ago

Thanks. So It’s about getting the last transaction in history, running the validators, and providing its content. And I guess I can keep track of the transaction ID the content was provided for to avoid (duplicate) issues with Server Notifications V2, right? (Out of my head I think the notification will provide the same ID in one of the fields)

On 5 May 2024, at 07:20, Alex Baker @.***> wrote:



@Apidcloudhttps://github.com/Apidcloud Generally you would then know what the user purchased and provision content accordingly.

— Reply to this email directly, view it on GitHubhttps://github.com/apple/app-store-server-library-node/issues/130#issuecomment-2094637001, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AA4MM4UJASJ7XVHAJRMMXE3ZAW6Q7AVCNFSM6AAAAABG74L2S2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAOJUGYZTOMBQGE. You are receiving this because you were mentioned.Message ID: @.***>

alexanderjordanbaker commented 4 months ago

@Apidcloud The transaction the user purchased may not be the latest, there may be multiple new transactions since you last queried the endpoint. We recommend keeping the revision you receive from the Get Transaction History endpoint, and using the same revision the next time you get a customer's history. This will then return all new and updated (for example due to a refund) transactions since the last time you queried the endpoint.

Apidcloud commented 4 months ago

Thanks @alexanderjordanbaker for the overall help; I think I have a better understanding now. Keeping the revision token is a good idea to make sure we are only dealing with newer entries.