Closed jareddr closed 4 months ago
AppTransaction is (https://developer.apple.com/documentation/storekit/apptransaction)
You probably want verifyAndDecodeSignedTransaction which decodes a (https://developer.apple.com/documentation/appstoreservernotifications/jwstransactiondecodedpayload)
Ah, I just found verifyAndDecodeTransaction
which seems to work.
Thank you so much Alexander for the quick response, I very much appreciate you!
I've been bashing my head against the wall trying to get this to work for a couple hours now.
This comment is not directed at you or anyone in particular, but I just feel the need to send feedback to this repository that it may have been one of the worst developer experiences of my life trying to use this library to interact with the app-store services. I would expect a company as large and as detail oriented as Apple to devote the resources needed to make this experience more seamless for developers.
I've now sat here and reflected on that previous sentence for a few minutes to see if I was being hyperbolic, but I do not believe I was.
I'm new to the app-store developer eco system, but I would suspect that verifying an in-app transaction server side is one of the more common tasks one sets out to accomplish with this library. Having a fully fleshed out example of this use case would be tremendously helpful for future users.
This innocuous seeming line
const appleRootCAs: Buffer[] = loadRootCAs() // Specific implementation may vary
found me scratching my head for some time. After some googling I found my way to the apple certificate page, but which of the 30 certificates do I need here? It wasn't until I found and watched a 20 minute video that I saw which certificates I needed to download and then load. There is also a dire warning that developers should not store these keys, but instead fetch them when needed because they may expire and need to be renewed. An example of that would be great. Do we simply hardcode the urls and then fetch
them in, how long should we cache them? How do we check to see if they are expired?
I was baffled by the documentation not listing the available functions. I was able to see the getTransactionHistory
function in the example, but had to go digging through the code myself to find the getTransactionInfo
function, which seemed to be the only function matching the transactionId
I'm getting back from my frontend IOS Dev. And then I repeated the process to find the function to decode my JWT and as we witnessed, I found the not-quite-right one. async verifyAndDecodeTransaction(signedTransactionInfo)
vs async verifyAndDecodeAppTransaction(signedAppTransaction)
Apologies for the vent, but this was a particularly frustrating block of coding.
Thanks much for your hard work.
Hey @jareddr Is verifyAndDecodeTransaction
equivalent of the "verifyReceipt
" endpoint which we had for StoreKit1? I too am confused on how to validate my receipt in StoreKit2 on server side?
For those who simply want to verify the receipt using this new API, I was able to achieve this using the following code:
To get creds:
Validation using transactionId
:
import {
readBytes,
readFile,
} from '@apple/app-store-server-library/dist/tests/util';
import {
AppStoreServerAPIClient,
Environment,
SignedDataVerifier,
} from '@apple/app-store-server-library';
const issuerId = 'ISSUER_ID';
const keyId = 'KEY_ID';
const bundleId = 'YOUR_BUNDLE_ID_IN_STORE';
const filePath = 'SubscriptionKey_XXXXXXXXX.p8';
const encodedKey = readFile(filePath); // Specific implementation may vary
const environment = Environment.SANDBOX;
// Certs from https://www.apple.com/certificateauthority/
// Apple Inc. Root
// Apple Computer, Inc. Root
// Apple Root CA - G2 Root
// Apple Root CA - G3 Root
const certs = [
readBytes('./cert/AppleIncRootCertificate.cer'),
readBytes('./cert/AppleComputerRootCertificate.cer'),
readBytes('./cert/AppleRootCA-G2.cer'),
readBytes('./cert/AppleRootCA-G3.cer'),
];
const client = new AppStoreServerAPIClient(
encodedKey,
keyId,
issuerId,
bundleId,
environment,
);
const transactionId = '123'
const infoResponse = await client.getTransactionInfo(transactionId);
const verifier = new SignedDataVerifier(certs, true, environment, bundleId);
const payload = await verifier.verifyAndDecodeTransaction(
infoResponse.signedTransactionInfo,
);
console.log(payload);
Use raw receipt to get transactionId
:
const rawReceipt =
'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';
const utils = new ReceiptUtility();
const transactionId = utils.extractTransactionIdFromAppReceipt(rawReceipt);
console.log(transactionId);
@jareddr
Thank you for the feedback, a few responses.
There is also a dire warning that developers should not store these keys, but instead fetch them when needed because they may expire and need to be renewed.
Perhaps I wasn't clear, that is a warning not to store the leaf keys which signed the JWT directly. The root CAs can and should be stored, as they rotate much less frequently.
I've also added a section to the README here which should make it easier to find these certificates.
I was baffled by the documentation not listing the available functions.
All endpoints are listed on the App Store Server API page, and we also list all the functions in the generated documentation here.
which seemed to be the only function matching the transactionId I'm getting back from my frontend IOS Dev.
Generally the frontend should pass back the entire signed JWS if possible, which can be verified directly without needing to call the App Store Server API. Otherwise there is not a direct way to prove the client actually owns that transaction.
I found the not-quite-right one. async verifyAndDecodeTransaction(signedTransactionInfo) vs async verifyAndDecodeAppTransaction(signedAppTransaction)
In PR #116 I updated in-line code documentation for those functions to link to the official documentation to make it easier to understand which parameter goes where.
Thanks so much @alexanderjordanbaker.
Generally the frontend should pass back the entire signed JWS if possible, which can be verified directly without needing to call the App Store Server API. Otherwise there is not a direct way to prove the client actually owns that transaction.
I suspected that this was the case but my IOS dev counter part could not find a way to access that JWT , and seemed to only be able to access a transaction_id. This would be using Swift for a one-off in-app purchase. Any chance you could point us in the right direction there? It would tighten up our loop a bit.
Thank again for taking the time to read my rant (i was feeling particularly frustrated that day) and responding so kindly.
Cheers
@jareddr The https://developer.apple.com/documentation/storekit/verificationresult/3868429-jwsrepresentation field on a VerificationResult
@alexanderjordanbaker Hey. Thanks for the reply
Generally the frontend should pass back the entire signed JWS if possible, which can be verified directly without needing to call the App Store Server API. Otherwise there is not a direct way to prove the client actually owns that transaction.
Does this mean that we can validate any transaction ID, even if it doesn't belong to our application, and that the configured App Store API doesn't check the owner using encodedKey, keyId, issuerId, bundleId?
@gigi A transaction id that does not belong to the specified application will return a 404 not found error, just as if it was completely invalid (as it doesn’t exist for your app). The problem is confirming that the client that passed you the transaction id that is for your app owns that transaction id vs a different user of your app actually owns the transaction id.
@alexanderjordanbaker Thanks for the clarification, but how does the signed JWS help determine that it belongs to the original user?
@gigi Because no other user will have the signed transaction. As it is cryptographically signed by Apple, it is not possible for an unrelated third party to create a "signed transaction" representing that transaction and provide it to your server.
@alexanderjordanbaker I've got your point.
So it's a little more difficult to cheat with in-app currency. But nothing prevents an attacker from copying the valid JWSrepresentation inside the app and passing it on behalf of another user. This is out of the scope of this library, right?
Is there any way to add additional information like the app user ID inside the JWS payload?
But nothing prevents an attacker from copying the valid JWSrepresentation inside the app
Well, the Apple user and user in your system can be disjoint. Over time that mapping could change; the user could use multiple Apple accounts to purchase products for a single account in the developer's system, and vice versa. In general it is the developer's responsibility to make sure a single transaction is not redeemed across multiple accounts in your system.
@alexanderjordanbaker thank you!
@gigi @alexanderjordanbaker. How can i check if purchase is failed or succeeded??? verifyAndDecodeTransaction & getTransactionHistory tried this both methods. But both gives same payload.
@sailingwithsandeep, if the transaction exists, then it was successful, once a transaction has been passed back to the device/your server, you should provision access
I will be closing this thread as the original inquiry seems to have been resolved. If any further questions remain from any participant, please open a new issue
Hello, I'm trying to verify an in app purchase was successful before unlocking digital goods on my node backend.
I'm writing a simple test function to request the transaction JWT by transactionId and then using verifyAndDecodeAppTransaction. I was getting this exception:
Without knowing how to fix this I started adding console.logs in your library code and found that the app transaction was getting decoded properly but was missing a
receiptType
property that is compared against theenvironment
string used to construct the SignedDataVerifier class.Here is the function I'm referring to:
Here is my decoded transaction:
As you can see it does not have a
receiptType
property which causes an exception to be thrown.Could you please advise me if I'm doing something incorrectly on my end or if this is a bug with the library?
Thank you.