Author: iHTCboy
Developers were not notified about the refund of App Store users until June 2020, when Apple provided refund notification. However, developers may not receive refund notification because it is not API. In addition, for example, if the user has successfully recharged but the app does not provide gold coins or services, developers generally cannot tell whether the user has actually paid. To sum up, Apple has brought a new and powerful App Store Server API to WWDC21. This article gives us a comprehensive understanding of the App Store Server API from the process of practice.
One, foreword
Hi, everyone. We published a summary article on June 17th last year after WWDC21 called “The Three Steps of Apple iOS In-app Purchase – WWDC21”. At that time, it was only sorted out according to the content of Apple’s speech, and many interfaces and functions were not online at that time, such as the interface for querying users’ Apple receipts according to players’ invoice and order numbers, and the interface for querying historical orders, etc. At that time, the article did not have in-depth analysis, but now it is 2022. The Apple App Store Server API is now available, so let’s take a look at the actual usage of the API
2. App Store Server API
First, let’s list what Server apis apple provides with WWDC21, then we’ll look at how to implement these interfaces, and finally summarize the considerations.
2.1 introduction of API
Query receipts for user orders
GET https://api.storekit.itunes.apple.com/inApps/v1/lookup/{orderId}
Copy the code
- Look Up Order ID: Use the Order ID to retrieve the user’s in-app purchase item receipt information from the receipt.
Query historical user receipts
GET https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}
Copy the code
- Get Transaction History: Gets a History of in-app purchases made by a user in your app.
Query the user’s internal purchase refund
GET https://api.storekit.itunes.apple.com/inApps/v1/refund/lookup/{originalTransactionId}
Copy the code
- Get Refund History: Gets a list of all in-app purchases that have been refunded to users in the app.
Example Query the status of subscribed items
GET https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}
Copy the code
- Get All Subscription Statuses: Gets the status of All subscriptions by users in your app.
Submit anti-fraud information
PUT https://api.storekit.itunes.apple.com/inApps/v1/transactions/consumption/{originalTransactionId}
Copy the code
The Send Consumption Information: When a user applies for a refund, Apple sends the CONSUMPTION_REQUEST notification to the developer server. Within 12 hours, the developer can provide the user’s information (whether the game gold has been spent, how much the user has topped up, how much the user has been refunded, etc.). Apple receives this information. Assist the Refund decision System in deciding whether to allow a refund.
Extend a user’s subscription period
PUT https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/extend/{originalTransactionId}
Copy the code
Extend a Subscription Renewal Date: Extends the Renewal Date of a valid subscriber Subscription using the original transaction identifier. (It is equivalent to increasing the subscription time for free)
2.2 Interface Parameters
The App Store Server API is a set of APIS (REST apis) provided by Apple for developers to manage users’ purchases in the App Store through the Server.
URL
URL of online environment:
https://api.storekit.itunes.apple.com/
Copy the code
Sandbox environment test:
https://api.storekit-sandbox.itunes.apple.com/
Copy the code
JWT profile
Calling these apis requires authorization from a JWT (JSON Web Token). So what is JWT?
JWT is an open standard (specification file RFC 7519) for securely transferring information between parties as JSON objects. There are two implementations. One jWS-based implementation uses BASE64URL encoding and digital signatures to provide integrity protection for transmitted Claims, that is, only to ensure that the contents of transmitted Claims are not tampered with, but to expose plaintext. The other is a JWE-based implementation that relies on encryption and decryption algorithms, BASE64URL encoding, and identity authentication to make it more difficult to crack transmitted Claims.
JWS
(Specification documentRFC 7515) :JSON Web Signature
, indicates usingJSON
Data structure andBASE64URL
Encoding represents a digital signature or message authentication code (MAC
) the content of the certification.JWE
(Specification documentRFC 7516) :JSON Web Encryption
, represents based onJSON
The encrypted content of a data structure.
Apple’s JWT-related content is currently implemented based on JWS, so JWT in the following context is JWS by default.
JWT (JWS) consists of three parts:
base64(header) + '.' + base64(payload) + '.' + sign( Base64(header) + "." + Base64(payload) )
Copy the code
- Header: mainly declares the signature algorithm of JWT;
- Payload: Carries various declarations and transmits plaintext data.
- Signture: The JWT that owns this part is called JWS, i.e. signed JWS.
Examples of JWT content:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidGVhbSI6IjM35omL5ri45oqA5pyv6L-Q6JCl5Zui6ZifIiwiYXV0aG9 yIjoiaUhUQ2JveSIsImlhdCI6MTUxNjIzOTAyMn0.dL5U_t_DcfLTY9WolmbU-j81jrZqs1HhHqYKM6HSxVgWCGUAKLzwVrnLuuMCnSRnrW9vmGKNqNvrzG8 cEwxvAgCopy the code
Detailed JWT content is skipped here, you can automatically search for more.
Assemble the JWT
Now that we know the basics of JWT, we’re ready to go. There are three steps to generate a signature JWT:
- Create the JWT header.
- Create the JWT payload.
- Sign on JWT.
JWT header example:
{
"alg": "ES256"."kid": "2X9R4HXF34"."typ": "JWT"
}
Copy the code
JWT payload example:
{
"iss": "57246542-96fe-1a63e053-0824d011072a"."iat": 1623085200."exp": 1623086400."aud": "appstoreconnect-v1"."nonce": "6edffe66-b482-11eb-8529-0242ac130003"."bid": "com.example.testbundleid2021"
}
Copy the code
The above is the field specification required by Apple, so the different JWT characters are not the same as the content, so let’s take a look at apple’s definition of these fields:
field | Fields that | Field Value Description |
---|---|---|
alg |
Encryption Algorithm | Default value: ES256. All JWT of the App Store Server API must be signed using ES256 encryption. |
kid |
Key ID: indicates the ID of the Key | Your private key ID, the value from App Store Connect, is described below. |
typ |
Token Type: indicates the Token Type | Default value: JWT |
iss |
Issuer | Your card issuer ID is taken from the App Store Connect key page, as explained below. |
iat |
A: Issued At | Seconds, the time when the token was issued in UNIX time (for example, 1623085200) |
exp |
Expiration Time: Expiration Time | Seconds, the expiration time of the token, in UNIX time. Tokens that expire more than 60 minutes in iAT are invalid (for example: 1623086400) |
aud |
On the Audience | Fixed value: AppStoreConnect -v1 |
nonce |
A Unique Identifier | Any number that you create and use only once (for example, “6EDFFe66-B482-11EB-8529-0242AC130003 “). It can be understood as the UUID value. |
bid |
Bundle ID: package ID | Your app’s suit ID (for example: “com. Example. Testbundleid2021”) |
Where kid and ISS values are created and obtained from the App Store Connect background.
Generate key ID (KID)
To generate a key, you must have an administrator role or an account holder role in App Store Connect. Log in to App Store Connect and complete the following steps:
- Select Users and Access, and then select the Key subtab.
- Under Key Type, select In-App Purchase Items.
- Click Generate purchase project key within API (if you have created one before, click Add (+) button to add one.) .
- Enter the name of the key. The name is for your reference only. The name is not part of the key.
- Click Generate.
In the generated key, a column named “key ID” is the value of KID. When the mouse moves to the text, the copy key ID will be displayed. Click the button to copy the value of Kid.
Generate Issuer (ISS)
Similarly, the generation of iss value is similar to:
The issuer ID value is the value of the ISS.
Generate and sign JWT
Once you get the parameters here, you need a signature, and then you need a signed key file.
Download and save the key file
App Store Connect key file, when kid was just generated, there was a button on the right side of the list to download the in-App purchase project key (the download link will only be displayed if you have not downloaded the private key yet). :
This private key can only be downloaded once!
In addition, Apple does not keep a copy of your private key, so store your private key in a secure place.
Note: Store your private key in a secure place. Do not share keys, do not store keys in code repositories, and do not place keys in client code. If you suspect that your private key has been stolen, please revoke the key in App Store Connect immediately. For more information, see Revoking API Keys.
The API key has two parts: the public key that Apple keeps and the private key that you download. The developer uses the private key to sign the token that authorizes the API to access data in the App Store.
Note that the App Store Server API key is unique to the App Store Server API, Does not work with other Apple services (such as Sign in with Apple services or App Store Connet API services, etc.). .
Generate the token for the API request
Finally, JWT Header and payload examples:
{
"alg": "ES256"."kid": "2X9R4HXF34"."typ": "JWT"
}
{
"iss": "57246542-96fe-1a63-e053-0824d011072a"."iat": 1623085200."exp": 1623086400."aud": "appstoreconnect-v1"."nonce": "6edffe66-b482-11eb-8529-0242ac130003"."bid": "com.apple.test"
}
Copy the code
With the above parameters and the key file, we can generate the token value as required by the JWT specification. The token is generated using Python3 code, other language types.
First, the terminal executes the command to install the ptyhon dependency library:
pip3 install PyJWT
Copy the code
We use Python’s PyJWT library to generate JWT tokens. Sample code:
import jwt
import time
Read the key file certificate
f = open("/Users/37/Downloads/SubscriptionKey_2X9R4HXF34.p8")
key_data = f.read()
f.close()
# JWT Header
header = {
"alg": "ES256"."kid": "2X9R4HXF34"."typ": "JWT"
}
# JWT Payload
payload = {
"iss": "57246542-96fe-1a63-e053-0824d011072a"."aud": "appstoreconnect-v1"."iat": int(time.time()),
"exp": int(time.time()) + 60 * 60.# 60 minutes timestamp
"nonce": "6edffe66-b482-11eb-8529-0242ac130003"."bid": "com.apple.test"
}
# JWT token
token = jwt.encode(headers=header, payload=payload, key=key_data, algorithm="ES256")
print("JWT Token:", token)
Copy the code
Example output:
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjJYOVI0SFhGMzQifQ.eyJpc3MiOiI1NzI0NjU0Mi05NmZlLTFhNjMtZTA1My0wODI0ZDAxMTA3M mEiLCJhdWQiOiJhcHBzdG9yZWNvbm5lY3QtdjEiLCJpYXQiOjE2NDMwMTU1OTQsImV4cCI6MTY0MzAxOTE5NCwibm9uY2UiOiI2ZWRmZmU2Ni1iNDgyLTExZ WItODUyOS0wMjQyYWMxMzAwMDMiLCJiaWQiOiJjb20uYXBwbGUudGVzdCJ9.muBKcbT3AnK3WAivbtIr64d2Gu7bVhGL3AhiYnDjb7D3qslHNnASE2EUUuN2 4jOLsSnLBWkBdwDutl5UU87pawCopy the code
This script generates tokens to request the App Store Server API. Of course, you can wrap the above code into a method, passing in parameters like kid and iss, and then returning the token, which is skipped here.
2.3 Interface Description
Said so much, finally back to below!!
How to request the App Store Server API? Apple gives an example:
curl -v -H 'Authorization: Bearer [signed token]'
"https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{original_transaction_id}"
Copy the code
Jwt-generated tokens have been placed in the header of the App Store Server API request links with key Authorization and value Bearer [signed token].
Next, we request the App Store Server API through Python’s Requests. You can also use other tools to simulate it, such as online tools or Postman.
The terminal executes the command to install the ptyhon dependency library:
pip3 install requests
Copy the code
import requests
import json
# JWT Token
token = "xxxxx"
Request links and parameters
url = "https://api.storekit.itunes.apple.com/inApps/v1/lookup/" + "MK5TTTVWJH"
header = {
"Authorization": f"Bearer {token}"
}
Request and response
rs = requests.get(url, headers=header)
data = json.loads(rs.text)
print(data)
Copy the code
Example Query result:
{
'status': 0.'signedTransactions': [
'eyJhbGciOiJFUz............. '.'eyJ0eXAiOiJKV1............. '.'eyJ0eXAiJhbGci............. ']}Copy the code
Next, the following will not repeat the example of the request, mainly to explain the interface and returned data format, precautions and so on.
Query user’s Order receipt (Look Up Order ID)
GET https://api.storekit.itunes.apple.com/inApps/v1/lookup/{orderId}
Copy the code
Currently, the Look Up Order ID interface is only available in online environment and does not support sandbox environment. Because this interface is when the user buys the project and receives the invoice from Apple, there is a column called Order ID in it. Before, it could not be mapped and associated with the transaction Order ID obtained by the developer from Apple. Now, it can be queried through this interface.
The data format of the response:
The function of this interface is to let the player provide the order ID when the user complains (the recharge is not available), and then query the corresponding status of the order through this interface. If there is an unconsumed receipt (transactionId), it can reissue or provide service support for the user. (Because you can check the transactionId, the player’s recharge order is valid! The server is required to check for unconsumed receipts.
Status =0, indicating a valid order number:
{
'status': 0.'signedTransactions': [
'eyJhbGciOiJFUz............. '.'eyJ0eXAiOiJKV1............. '.'eyJ0eXAiJhbGci............. ']}Copy the code
If you notice, signedTransactions is a receipt for multiple transactions. In fact, an Order ID here can correspond to multiple purchased items. For example, the user buys two items at the same time in one minute. Apple will combine these two orders into one Order when sending invoices to the user, and there is only one Order ID at this time.
So, developers need to note that the Order ID is not unique to a purchase Order. When verifying the user’s Order ID, you also iterate through all signedTransactions to find items that might not be consumed.
After each JWT decode, sample format:
header:
{
"alg":"ES256"."x5c": ["MIIEMDC...."."MIIDFjC...."."MIICQzC...."]}Copy the code
payload
{
"transactionId": "20000964758895"."originalTransactionId": "20000964758895"."bundleId": "com.apple.test"."productId": "com.apple.iap.60"."purchaseDate": 1640409900000."originalPurchaseDate": 1640409900000."quantity": 1."type": "Consumable"."inAppOwnershipType": "PURCHASED"."signedDate": 1642995907240
}
Copy the code
This is the data format for a consumable item.
Finally, as for the analysis of JWT content, I will not go into depth here, but will explain it in a unified way below.
Get Transaction History
GET https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}
Copy the code
According to the WWDC21 video, the interface can obtain a user’s in-app purchase transaction history in your app. In practice, the interface document Get Transaction History contains a new update:
Transaction history returns only the following:
- Automatically renew subscriptions
- Non-renewed subscriptions
- Non-consumable in-app purchases
- Consumable in-app purchase items: If the transaction is refunded, cancelled, or the app has not completed the transaction processing, etc.
The data format of the response:
Note that there is no status field in the returned result.
{
"revision": "1642993906000 _1000000954832195"."bundleId": "com.apple.test"."appAppleId": 925021570."environment": "Production"."hasMore": false."signedTransactions": [
"eyJhbGciOi..."."eyJhbGciOi..."]}Copy the code
By default signedTransactions returns a maximum of 20 transactions, which the developer cannot control at this time. When the number of revisions exceeds 20, hasMore is true, indicating that the updated historical order has been updated. In this case, the developer needs to add the requested query field revision, whose value is the corresponding revision field from the data returned in the last request.
For example, request more data: / inApps/v1 / history/foriginalTransactionId} & revision = 8 a170756 e913-42 f9e1134 fc – 8629-76051.
After each JWT decode, sample format:
payload
{
"transactionId": "1000000954804912"."originalTransactionId": "1000000954804912"."webOrderLineItemId": "1000000071590544"."bundleId": "com.apple.test"."productId": "com.apple.iap.month"."subscriptionGroupIdentifier": "20919269"."purchaseDate": 1642990548000."originalPurchaseDate": 1642990550000."expiresDate": 1642990848000."quantity": 1."type": "Auto-Renewable Subscription"."inAppOwnershipType": "PURCHASED"."signedDate": 1643024941850
}
Copy the code
This is a data format for automatically renewing subscription items.
Get Refund History
GET https://api.storekit.itunes.apple.com/inApps/v1/refund/lookup/{originalTransactionId}
Copy the code
All originalTransactionIDS purchased by a user can be queried through Get Refund History.
The data format of the response:
The signedTransactions contained in the response may be the same as one or more REFUND notifications in the App Store Server Notification. So, use this API to query for any refund notifications that you might have missed, for example during a server outage.
Note, however, that only refunds approved by the App Store are included: consumable, non-consumable, auto-renewed and non-renewed subscriptions. If the user does not receive any App Store approved refunds, an empty signedTransactions array is returned on success.
{
"signedTransactions": []}Copy the code
In the sandbox environment, apple rejected the operation of refund, and the online application was not passed, so there is no format of returned data for the time being. (A refund format will be added later.)
Get All Subscription Statuses
GET https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}
Copy the code
Subscription status query API Get All Subscription Statuses, which gets the status of All subscriptions in your app.
The data format of the response:
{
"environment": "Sandbox"."bundleId": "securitynote"."data": [{"subscriptionGroupIdentifier": "20919269"."lastTransactions": [{"status": 2."originalTransactionId": "1000000954804912"."signedTransactionInfo": "eyJhbGciOiJFUz...."."signedRenewalInfo": "eyJhbGciOiJFUzI1Ni...."}]}]}Copy the code
LastTransactions is the last subscription status for each subscription item, of type status:
- 1: effective
- 2: expired
- 3: Deduct the account fee and retry
- 4: Account grace period (this is set by the developer, such as how long users can be extended when they fail to deduct the fee due).
- 5: It has been withdrawn.
Example signedTransactionInfo format:
{
"transactionId": "1000000955217725"."originalTransactionId": "1000000954804912"."webOrderLineItemId": "1000000071615442"."bundleId": "com.apple.test"."productId": "com.apple.iap.month"."subscriptionGroupIdentifier": "20919269"."purchaseDate": 1643023487000."originalPurchaseDate": 1642990550000."expiresDate": 1643023787000."quantity": 1."type": "Auto-Renewable Subscription"."inAppOwnershipType": "PURCHASED"."signedDate": 1643028928116
}
Copy the code
Example signedRenewalInfo format:
{
"expirationIntent": 1."originalTransactionId": "1000000954804912"."autoRenewProductId": "com.apple.iap.month"."productId": "com.apple.iap.month"."autoRenewStatus": 0."isInBillingRetryPeriod": false."signedDate": 1643028928116
}
Copy the code
Send Consumption Information
PUT https://api.storekit.itunes.apple.com/inApps/v1/transactions/consumption/{originalTransactionId}
Copy the code
The purpose of this interface is to submit anti-fraud Information to Apple, see the document Send Consumption Information.
When a user applies for a refund, Apple sends the CONSUMPTION_REQUEST notification to the developer server. Within 12 hours, the developer can provide the user’s information (whether the game gold has been spent, how much the user has topped up, how much the user has been refunded, etc.). Apple receives this information. Assist the Refund decision System in deciding whether to allow a refund. Check out our previous post for more details.
Users submit a refund request, and Apple will update the results within 48 hours of reporting the problem. As a result, developers have 12 hours to decide whether to provide anti-fraud information to Apple after receiving a refund notification.
Parameters Indicates the description of the field. For details, see Send Consumption Information
Request Response Codes 202 indicate that Apple has received the message.
Extend a Subscription Renewal Date
PUT https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/extend/{originalTransactionId}
Copy the code
Developers have two opportunities a year to add 90 days of free compensation to their in-app subscriptions. Auto-subscription apps allow developers to reimburse users on the server for up to 90 days each time. For details, see the document Extend a Subscription Renewal Date.
The following types of subscriptions are not eligible for renewal date extensions:
- Subscription during the free offer period
- Inactive subscription in billing retry state
- A subscription that has expired and is in a grace period
- Two subscription extensions have been received within the last 365 days
In addition, Apple has a tip: When the App Store calculates developers’ commissions, the extension period does not count toward a year of paid services.
Simply put, after a year of subscription, the developer gets 85% of the net revenue. Developers give users a free extension of time, not counted in the year! (Get it?)
The parameters field description, see detailed Documentation: ExtendRenewalDateRequest | Apple Developer Documentation
The request is successful if its Response Codes are 200.
2.4 Question Answers
Authorization: Bearer [signed token]
Whenever a user accesses a protected route or resource, a user can send a JWT in bearer mode, usually in an Authorization header, in the following format:
Authorization: Bearer [signed token]
Copy the code
The server then retrieves the contents of the token to return the corresponding contents. Note that the token may not be stored in a cookie. If the token is stored in a cookie, set it to HTTP-only to prevent XSS. In addition, it can be seen that if tokens are sent in, say, Authorization: Bearer, then cross-domain resource sharing (CORS) will not be an issue as it does not use cookies.
Therefore, the main purpose of JWT is to transfer declarations between the server and client in a secure manner. Main application scenarios:
- Certification Authentication
- Authorization Authorization
- Joint recognition
- Client session (stateless session)
Error Codes
If the token is invalid or invalid, the content is returned:
Unauthenticated Request ID: 7 f5dbz7vdx677topbaoeuxwscy. 0.0Copy the code
If the request is originalTransactionId does not exist, complains 4040005 (OriginalTransactionIdNotFoundError) :
{
"errorCode": 4040005,
"errorMessage": "Original transaction id not found."
}
Copy the code
Other error codes:
Object | errorCode | errorMessage |
---|---|---|
GeneralBadRequestError | 4000000 | Bad request. |
InvalidAppIdentifierError | 4000002 | Invalid request app identifier. |
InvalidRequestRevisionError | 4000005 | Invalid request revision. |
InvalidOriginalTransactionIdError | 4000008 | Invalid original transaction id. |
InvalidExtendByDaysError | 4000009 | Invalid extend by days value. |
InvalidExtendReasonCodeError | 4000010 | Invalid extend reason code. |
InvalidRequestIdentifierError | 4000011 | Invalid request identifier. |
SubscriptionExtensionIneligibleError | 4030004 | Forbidden – subscription state ineligible for extension. |
SubscriptionMaxExtensionError | 4030005 | Forbidden – subscription has reached maximum extension count. |
AccountNotFoundError | 4040001 | Account not found. |
AccountNotFoundRetryableError | 4040002 | Account not found. Please try again. |
AppNotFoundError | 4040003 | App not found. |
AppNotFoundRetryableError | 4040004 | App not found. Please try again. |
OriginalTransactionIdNotFoundError | 4040005 | Original transaction id not found. |
OriginalTransactionIdNotFoundRetryableError | 4040006 | Original transaction id not found. Please try again. |
GeneralInternalError | 5000000 | An unknown error occurred. |
GeneralInternalRetryableError | 5000001 | An unknown error occurred. Please try again. |
For a detailed description of Error Codes, see the documentation Error Codes.
Query user’s Order receipt (Look Up Order ID)
GET https://api.storekit.itunes.apple.com/inApps/v1/lookup/{orderId}
Copy the code
Can I query all orders through this interface? Or is it only available for orders created using StoreKit2?
A: At present, I have found the order numbers of many projects to be purchased in 2020, which can be queried through API. Therefore, this interface does not limit the purchase period of the order. (At least after 2020 can be checked, if there is any exception, welcome to communicate with you in the comments section.)
JWT signature verification
Each request to the App Store Server API needs to be authorized with a JSON Web Token (JWT). Apple recommends that you do not need to generate a new token for each API request. To get better performance from the App Store Server API, reuse existing signed tokens, each of which has a 60 minute validity period.
If you just want to fetch the Payload of JWT, you can simply base64 Decode Payload, but if you want to validate the signature, you must use Signture, Header.
You can use Python’s PyJWT library to decode:
import jwt
token = "exxxxxx" Tokens need to be decoded
data = jwt.decode(token, options={"verify_signature": False})
Copy the code
Apple’s advice: You can create and sign JWT tokens using various open source libraries. For more information about JWT, see jwt.io.
As you can see from the PyJWT documentation, JWT validates:
- Verify_signature: verifies the JWT encrypted signature
- Verify_aud: Whether to match the audience
- Verify_iss: indicates whether the issuer matches
- Verify_ exp: Indicates whether the value expires
- Verify_iat: indicates whether the value is an integer
- Verify_nbf: past time or Not (NBF stands for: Not Before, indicating that JWT Token is invalid Before this time. The effective time.)
So, we can see, verify that JWT has that content. The most important thing is to verify verify_signature. When verifying a signature, use a public key or key to decrypt Sign. If the value is the same as base64UrlEncode(header) + “.” + base64UrlEncode(Payload), the authentication succeeds.
Example of a validation process:
import jwt
public_key = "xxxx" # Public key certificate content
data = jwt.decode(token, key=public_key, algorithms=["ES256"])
Copy the code
So where does apple get its public key? Here’s a clue from apple’s developer forums:
- Validate StoreKit2 in-app purchase jwsRepresentation in backend
In simple terms, JWS’s X5C header field contains a certificate chain (X509), with the first certificate containing the public key used to verify the JWS signature. From the signedTransactions obtained by the fetch, a token is decoded in the following format:
{
"alg": "ES256"."x5c": [
"MIIEMDCCA7....."."MIIDFjCCApy....."."MIICQzCCAc......"]}Copy the code
The certificate can be downloaded from the Apple PKI page.
The last certificate in the x5c certificate chain corresponds to the Apple certificate Apple Root ca-g3 Root, but we need to convert. Cer to. Pem format.
openssl x509 -inform der -in AppleRootCA-G3.cer -out AppleRootCA-G3.pem
Copy the code
X.509: is a certificate standard that defines what should be included in a certificate. For details, refer to RFC5280, the certificate standard used by SSL. The same X.509 certificate may have different encoding formats. Currently, there are two encoding formats:
- DER: Distinguished Encoding Rules. It is in binary format and cannot be read.
- PEM: Privacy Enhanced Mail: Open the file in the format of — BEGIN… The beginning, “- END…” At the end, the content is BASE64 encoding.
The content of applerootca-g3. pem is the same as that of the last certificate in the X5C certificate chain as follows:
MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYD VQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYD VQQGEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtfTjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA /VhYDKX1DyxNB0cTddqXl5dvMVztK517IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySrMA8GA1UdEwEB/wQFMAMBAf8w DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4at+qIxUCMG1mihDK 1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM6BgD56KyKA==Copy the code
So, for validation, refer to Validate StoreKit2:
def good_signature?(jws_token)
raw = File.read "/Users/steve1/downloads/AppleRootCA-G3.cer"
apple_root_cert = OpenSSL::X509::Certificate.new(raw)
parts = jws_token.split(".")
decoded_parts = parts.map { |part| Base64.decode64(part) }
header = JSON.parse(decoded_parts[0])
cert_chain = header["x5c"].map { |part| OpenSSL::X509::Certificate.new(Base64.decode64(part))}
return false unless cert_chain.last == apple_root_cert
for n in 0..(cert_chain.count - 2)
return false unless cert_chain[n].verify(cert_chain[n+1].public_key)
end
begin
decoded_token = JWT.decode(jws_token, cert_chain[0].public_key, true, { algorithms: ['ES256']})! decoded_token.nil?
rescue JWT::JWKError
false
rescue JWT::DecodeError
false
end
end
Copy the code
The last JWT X5C certificate is verified with the applerootca-g3. cer certificate content, and the x509 certificate chain specification is used to verify each remaining certificate chain. Finally, JWT is verified with the public key of the first certificate in the X5C certificate chain.
Sign in with Apple
In addition to the App Store Server API, there are also services such as Sign in with Apple and App Store Connet API, which are delivered using JWT. The specific requirements and fields may differ from the App Store Server API. For example, the JWT Sign in with Apple does not need TYP, sub and bid are the same meaning, both denote the Bundle ID, app package name. So these specifications are the same, and when these details are different, developers have to step on the hole and then know, which shows the importance of the specification.
{
"alg": "ES256"."kid": "ABC123DEFG"
}
{
"iss": "DEF123GHIJ"."iat": 1637179036."exp": 1693298100."aud": "https://appleid.apple.com"."sub": "com.apple.test"
}
Copy the code
Third, summary
I started to try to cover all the subscription types in detail, but as I wrote it, I found things to be very complicated, because subscription items are complex and have many fields and functions. Due to the limited length of this article and the detailed field description in the Apple documentation, this article mainly explains the overall process and precautions of the App Store Server API. If you have any mistakes or questions, please make corrections and communicate in the comments section
Second, the new interface to the App Store Server API is huge! In the past, internal purchase produced a large number of black and grey products, and took advantage of loopholes in various links of Apple internal purchase, through exchange rate difference, zombie accounts, malicious refund and other ways, formed an industrial chain of studios and gangs. Since last year, Apple has provided internal purchase refund notification, and this year, it has provided inquiry interface and relevant customer service interface. Although these are late responses, to some extent, they are an important blow to fight against evil!
Finally, from the perspective of apple’s open interface and concept, Apple attaches great importance to user experience and hopes that developers can better serve users! Therefore, in 2022, I hope to learn and share interesting technologies with you, and polish excellent product experience and service! Work hard to refuel ~
From all members of the mobile Game iOS technical operation team:
Happy New Year, readers! Tiger Tiger alive!
Please follow us for more information on iOS and Apple
Iv. Reference
- Apple iOS purchase three steps: App refund, historical order query, binding users to prevent the single! — WWDC21
- Look Up Order ID | Apple Developer Documentation
- Get Transaction History | Apple Developer Documentation
- Get Refund History | Apple Developer Documentation
- Get All Subscription Statuses | Apple Developer Documentation
- Send Consumption Information | Apple Developer Documentation
- Extend a Subscription Renewal Date | Apple Developer Documentation
- Generating Tokens for API Requests | Apple Developer Documentation
- Generate and Validate Tokens | Apple Developer Documentation
- RFC 7519 – JSON Web Token (JWT)
- Cold rice new stir-fry: Understand the implementation principle and basic use of JWT – Throwable
- Validating “Sign in with Apple” Authorization Code – Parikshit Agnihotry
- What is a JWS and how to encode it for Apple In-App Purchases?
- Fraud in In-App Subscriptions : how to crack down on fraud from malicious users
- JWT(JSON Web) using _WIChandy technical blog _51CTO blog _JWT using tutorial
- RFC 7519 – JSON Web Token (JWT)
- Automatic Subscription renewal – App Store – Apple Developer
- Getting only decoded payload from JWT in python – Stack Overflow
- Validate StoreKit2 in-app purchase jwsRepresentation in backend| Apple Developer Forums
- How do I convert a .cer certificate to .pem? – Server Fault