apple / app-store-server-library-node

MIT License
155 stars 29 forks source link

Missing an example for how to verify an IAP transaction receipt #84

Closed eedeebee closed 5 months ago

eedeebee commented 6 months ago

Either we don't need to do this and we can just look up transaction IDs via this library... wr we could use an example of how to do this with this API. The docs are also unclear if you can look up transactioninfo for an unfinished transaction - for me so far, it doesn't work.

alexanderjordanbaker commented 6 months ago

Hello @eedeebee, can you be a little more specific with what IAP transaction receipt you are referring to, is it a JWSTransaction (https://developer.apple.com/documentation/appstoreserverapi/jwstransaction)? Also, unfinished transactions are visible in the Get Transaction History or Get Transaction Info endpoints in the App Store Server API

eedeebee commented 6 months ago

Thanks I'm just learning here and appreciate help. My question has morphed a bit as I'm for now just testing against the sandbox and I'd like to use the SignedDataVerifier (essentially offline without talking to the App Store best I can tell) with the cert from my XCode to verify and decode a transactionReceipt I get from a purchase in the simulator against a StoreKit config I synced down from the App Store. Can this be done? I tried providing the cert and I couldn't get the receipt to verify/validate:

import { SignedDataVerifier } from "@apple/app-store-server-library"
import { Environment } from "@apple/app-store-server-library"

const fs = require('fs');

function loadCAs() {
    return [
        fs.readFileSync('./StoreKitTestCertificate.cer'),
    ];
}

const bundleId = process.env.APPLE_APP_BUNDLE_ID!
const cas: Buffer[] = loadCAs() 
const environment = Environment.SANDBOX
const enableOnlineChecks = false ; // 0 docs on what this does and it was true in the example

console.log(bundleId)
const verifier = new SignedDataVerifier(cas, enableOnlineChecks, environment, bundleId)

const doit = async function () {
    try {
        const receipt = process.argv[2];  // Passing the receipt on the cmd line
        console.log("receipt", receipt);
        const trans = await verifier.verifyAndDecodeTransaction(receipt);
        console.log(trans)
    } catch (e) {
        console.error(e)
    }
}

doit();

I get an exception as if the receipt is some other data/format than is expected:

VerificationException [Error]
    at SignedDataVerifier.verifyJWT (/Users/ericbloch/repos/notewize-web/node_modules/@apple/app-store-server-library/jws_verification.ts:175:17)
    at SignedDataVerifier.verifyAndDecodeTransaction (/Users/ericbloch/repos/notewize-web/node_modules/@apple/app-store-server-library/jws_verification.ts:70:67)
    ... 6 lines matching cause stack trace ...
    at doit (/Users/ericbloch/repos/notewize-web/store-tools/verification.ts:58:12)
    at Object.<anonymous> (/Users/ericbloch/repos/notewize-web/store-tools/verification.ts:36:1) {
  status: 1,
  cause: TypeError: Cannot read properties of null (reading 'originalTransactionId')
      at JWSTransactionDecodedPayloadValidator.validate (/Users/ericbloch/repos/notewize-web/node_modules/@apple/app-store-server-library/models/JWSTransactionDecodedPayload.ts:213:24)
      at SignedDataVerifier.verifyJWT (/Users/ericbloch/repos/notewize-web/node_modules/@apple/app-store-server-library/jws_verification.ts:140:24)
      at SignedDataVerifier.verifyAndDecodeTransaction (/Users/ericbloch/repos/notewize-web/node_modules/@apple/app-store-server-library/jws_verification.ts:70:67)
      at /Users/ericbloch/repos/notewize-web/store-tools/verification.ts:29:38
      at step (/Users/ericbloch/repos/notewize-web/store-tools/verification.ts:33:23)
      at Object.next (/Users/ericbloch/repos/notewize-web/store-tools/verification.ts:14:53)
      at /Users/ericbloch/repos/notewize-web/store-tools/verification.ts:8:71
      at new Promise (<anonymous>)
      at __awaiter (/Users/ericbloch/repos/notewize-web/store-tools/verification.ts:4:12)
      at doit (/Users/ericbloch/repos/notewize-web/store-tools/verification.ts:58:12)
}

For what it's worth, I got the receipt from a purchase in a sim against my storekit config (using react-native-iap if that matters, but lmk). It looked like this in base64 encoded string form:

MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIIB5DGCAeAwDwIBAAIBAQQHDAVYY29kZTALAgEBAgEBBAMCAQAwJQIBAgIBAQQdDBtjb20ubm90ZXdpemUuTm90ZXdpemVQbGF5ZXIwCwIBAwIBAQQDDAExMBACAQQCAQEECPL7/tkJAAAAMBwCAQUCAQEEFPlHog6PzWYzkRL4nNrQbiDUXck+MAoCAQgCAQEEAhYAMB4CAQwCAQEEFhYUMjAyNC0wMi0xOFQxOTozMzo0OVowcQIBEQIBAQRpMWcwDAICBqUCAQEEAwIBATAoAgIGpgIBAQQfDB1jb3Vyc2Vfbm90ZXdpemViYXNpY2d1aXRhcl8wMTAMAgIGpwIBAQQDDAEwMB8CAgaoAgEBBBYWFDIwMjQtMDItMTRUMjI6MjE6MTBaMIGcAgERAgEBBIGTMYGQMAwCAgalAgEBBAMCAQEwIgICBqYCAQEEGQwXY29tLm5vdGV3aXplLnN1Yi5wcm9fMDEwDAICBqcCAQEEAwwBMzAfAgIGqAIBAQQWFhQyMDI0LTAyLTE2VDA0OjQzOjM4WjAfAgIGrAIBAQQWFhQyMDI1LTAyLTE2VDA0OjQzOjM4WjAMAgIGtwIBAQQDAgEAMB4CARUCAQEEFhYUNDAwMS0wMS0wMVQwMDowMDowMFoAAAAAAACgggN4MIIDdDCCAlygAwIBAgIBATANBgkqhkiG9w0BAQsFADBfMREwDwYDVQQDDAhTdG9yZUtpdDERMA8GA1UECgwIU3RvcmVLaXQxETAPBgNVBAsMCFN0b3JlS2l0MQswCQYDVQQGEwJVUzEXMBUGCSqGSIb3DQEJARYIU3RvcmVLaXQwHhcNMjAwNDAxMTc1MjM1WhcNNDAwMzI3MTc1MjM1WjBfMREwDwYDVQQDDAhTdG9yZUtpdDERMA8GA1UECgwIU3RvcmVLaXQxETAPBgNVBAsMCFN0b3JlS2l0MQswCQYDVQQGEwJVUzEXMBUGCSqGSIb3DQEJARYIU3RvcmVLaXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDbf5A8LHMP25cmS5O7CvihIT7IYdkkyF4fdT7ak9sxGpGAub/lDMs8uw5EYib6BCm2Sedv4BvmDWjNJW7Ddgj1SguuenQ8xKkLs89iD/u0vPfbhF4o60cN8e2LrPWfsAk4o257yyZQChrhidFydgs5TMtPbsCzX7eVurmoXUp0q+9vQaV+CY26PT3NcFfY7e/V2nfIkwQc7wmIeGXOgfKNcucHGm4mEvcysQ27OJBrBsT8DeWVUM2RyLol9FjJjOFx20pF8y0ZlgNWgaZE7nV3W1PPeKxduj5fUCtcKYzdwtcqF98itNfkeKivqG2nwdpoLWbMzykLUCzjwvvmXxLBAgMBAAGjOzA5MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQCyAOA88ejpYr3A1h1Anle5OJB3dlLSqEtwbrhnmfuzilWf7x0ouF8q0XOfNUc3u0bTdhDy8GnszWKZcflgioRIOMS9i2cluatsM2Wt2MKaeEgP6czBJw3Gz2Q8bYBZM4zKNgYqERuNSc4I/2bARyhL61rBKwlWLKWqCQN7MjHc6IV4SM7AxRIRag8Mri8Fym96ZH8gLHXmTLES0/3jH14NfbhY16B85H9jq5eaK8Mq2NCy4dVaDTkbb2coqRKD1od4bZm9XrMK4JjO9urDjm1p67dAgT2HPXBR0cRdjaXcf2pYGt5gdjdS7P+sGV0MFS+KD/WJyNcrHR7sK5EFpz1PMYIBjzCCAYsCAQEwZDBfMREwDwYDVQQDDAhTdG9yZUtpdDERMA8GA1UECgwIU3RvcmVLaXQxETAPBgNVBAsMCFN0b3JlS2l0MQswCQYDVQQGEwJVUzEXMBUGCSqGSIb3DQEJARYIU3RvcmVLaXQCAQEwDQYJYIZIAWUDBAIBBQAwDQYJKoZIhvcNAQELBQAEggEAXx8ll8rVQG6Thg/bnjyRjN81OQLozzp9OShU9DrGzX+zPe5JcET7X9X5SuCXUMPK1uNe4tP/HTjtzl21wvBN5aDwgJPFNfiTRr7mhEME7rq9o89B2lXnprxqUEh+0/S7s0lxB4VHILUD3yITNWtvp/tBplO3Zx0Gy2HSBZgX/nff+4yALkSYcDMFyEPi1DFftGacUoI+E3YAqbceLbWbcorUBaTk9UqQSgTISqV0zXMBWuOQRU+MpVyP16xeG94XIV8KIxctGYIxDPMNgtdzQDolOZpARGcy+WTjljE1FUXkgIbPZ2pBSNyRJ8KNRrTnPgnXgi02+T0u0f1hRGH2BwAAAAAAAA==

Thanks for any help!

alexanderjordanbaker commented 6 months ago

You're probably looking for Environment.XCODE not Environment.SANDBOX as Xcode is used when testing via Xcode without the Sandbox environment that connects to the App Store. enableOnlineChecks enables checking for revocation on the certificates, which requires making an OCSP (RFC 2560) call to check the revocation status. This is documented in https://developer.apple.com/videos/play/wwdc2023/10143/. For Xcode testing that doesn't matter as the certificates you're working with are generated locally and certificate validation is skipped for the Xcode environment.

However, looking at this receipt, this is not an AppTransaction or signed transaction that the library could verify directly. This is an App Receipt from Original Storekit (https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/validating_receipts_with_the_app_store). If you are unable to use StoreKit 2 for some reason, which provides the newer AppTransaction (https://developer.apple.com/documentation/storekit/apptransaction) and can be verified with the SignedDataVerifier, you can see an example from the README for how to work with legacy app receipts. A snippet is provided below. const appReceipt = "MI..." const receiptUtil = new ReceiptUtility() const transactionId = receiptUtil.extractTransactionIdFromAppReceipt(appReceipt)

eedeebee commented 6 months ago

Thanks @alexanderjordanbaker ! I configured react-native-iap to use StoreKit2 (forced my app to iOS 15.0 and later) and with Environment.XCodeand my cert, I can use SignedDataVerifier !

eedeebee commented 6 months ago

I guess my original question stands though - do I need to verify the receipt still or just look up a transaction info to see if it matches what I think it should?

alexanderjordanbaker commented 6 months ago

@eedeebee If you verify the signed data with the SignedDataVerifier, you don't need to do anything else to verify the transaction.