Before moving on to that topic let's see what is JWS.
JSON Web Signature (JWS) is a compact signature format intended for space constrained environments such as HTTP Authorization headers and URI query parameters. It represents signed content using JSON data structures. The JWS signature mechanisms are independent of the type of content being signed, allowing arbitrary content to be signed.
Quickly set-up and launch subscriptions that comply with app stores guidelines to enjoy faster time to revenue and avoid rejections and delays.
A JWS is represented by 3 elements separated by dots: jws_header.jws_payload.jws_signature
For example:
eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
Where:
jws_header = "eyJhbGciOiJIUzI1NiJ9"
jws_payload = "eyJkYXRhIjoidGVzdCJ9"
jws_signature = "pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y"
JWS_HEADER.jws_payload.jws_signature
The members of the JSON object represented by the JWS Header describe the signature applied to the Encoded JWS Header and the Encoded JWS Payload. Optionally, it may contains additional properties of the JWS.
It is Base64url encoded and you have to decode it to access its content.
When decoded, it has this form:
{
"alg": "ES256",
"typ": "JWT"
}
The important part here is "alg": "ES256"
which specifies the algorithm used to sign your content. We'll need it later.
jws_header.JWS_PAYLOAD.jws_signature
It is a Base64url encoded version of the content you want to secure.
This content can be of any format, but with Apple, it will always be a JSON object (a JWS with a JSON body is named JWT).
Even if it's not the intended way, you can get the content without any validation just by Base64url decoding it.
If you want to do it securely though, please continue your reading :)
jws_header.jws_payload.JWS_SIGNATURE
This signature is obtained by encrypting the JWS Signing Input.
This JWS Signing Input is the concatenation of:
jws_header
)jws_payload
)This JWS Signing Input is then encrypted with the algorithm contained in the JWS header (alg=ES256
here), using a private key. The result of this encryption is the jws_signature
.
To communicate with Apple, your server will need to be able to encrypt and/or decrypt JWS in these cases:
Authorization: Bearer [signed token]
)It's probably the easiest part of all!
As explained before, you're not even required to verify the signature and you could just Base64url decode the jws_payload
. For example, in Ruby:
jws_string = "eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y"
jws_payload = jws_string.split('.')[1]
# eyJkYXRhIjoidGVzdCJ9
payload = Base64.decode64(jws_payload)
# {
# "environment": "Production",
# "bundleId": "com.purchasely",
# // ...
# }
Decoding without verifying is not secure and you must check the signature to avoid fraud.
To do so, you'll need 2 things:
jws_header
(as seen before)jws_header.x5c
array)Then use your favorite cryptographic library to verify the data with:
They are both available here: https://www.apple.com/certificateauthority/
Purchasely is the only SaaS to deliver easy In-App Purchase management from Build, User Interface Management, KPI tracking to robust Reporting Analytics for marketers.
As explained before, every request made to the new App Store Server API must contain an Authorization header containing a JWT (Authorization: Bearer [signed token]
).
A JWT is a JWS structure with a JSON object as the payload. Some optional keys (or claims) have been defined such as iss, aud, exp etc.
To generate this token, you have to follow some steps.
To generate keys, you must have an Admin role or Account Holder role in App Store Connect. You may generate multiple API keys.
To generate an API key to use with the App Store Server API, log in to App Store Connect and complete the following steps:
The new keyās name, key ID, a download link, and other information appears on the page.
After generating your API key, App Store Connect gives you the opportunity to download the private half of the key. The private key is only available for download a single time.
The download link appears only if you havenāt yet downloaded the private key. Apple doesnāt keep a copy of the private key.
š” Store your private keys in a secure place. Don't share your keys, don't store keys in a code repository, don't include keys in client-side code. If you suspect a private key is compromised, immediately revoke the key in App Store Connect. See Revoking API Keys for details.
The official documentation can be found here.
This is the hard part... and the official documentation is here.
First, you need to create the header
:
{
"alg": "ES256",
"kid": "2X9R4HXF34",
"typ": "JWT"
}
The kid
is the id of the private key previously generated. It can be found by logging in to App Store Connect, then:
If you have more than one API key, copy the key ID of the private key that you use to sign the JWT.
Then, you'll need to create the payload
:
{
"iss": "57246542-96fe-1a63e053-0824d011072a",
"iat": 1623085200,
"exp": 1623086400,
"aud": "appstoreconnect-v1",
"nonce": "6edffe66-b482-11eb-8529-0242ac130003",
"bid": "com.example.testbundleid2021"
}
With:
iss
(Issuer): Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a"
)
To get your issuer ID, log in to App Store Connect, then:
Select Users and Access, then select the Keys tab.
The issuer ID appears near the top of the page. To copy the issuer ID, click Copy next to the ID.
iat
(Issued At): The time at which you issue the token, in UNIX time (Ex: 1623085200)
exp
(Expiration Time): The tokenās expiration time, in UNIX time. Tokens that expire more than 60 minutes after the time in iat are not valid (Ex: 1623086400)
aud
(Audience): always appstoreconnect-v1
nonce
(Unique Identifier): An arbitrary number you create and use only once (Ex: "6edffe66-b482-11eb-8529-0242ac130003")
bid
(Bundle ID): Your appās bundle ID (Ex: ācom.purchasely.demoā)
Finally, sign the JWT:
Use the private key associated with the key ID you specified in the header to sign the token, and sign using ES256 encryption.
There are a variety of open source libraries available online for creating and signing JWT tokens. See JWT.io for more information.
Here is an example in Ruby:
require 'jwt'
# private API key previously generated in the App Store Connect
PRIVATE_KEY = "S3cretKeĀ„"
header = {
alg: 'ES256',
kid: '2X9R4HXF34',
typ: 'JWT'
}
now = Time.now.to_i
FIFTY_MINUTES = 50 * 60
payload = {
"iss": "57246542-96fe-1a63e053-0824d011072a",
"iat": now,
"exp": now + FIFTY_MINUTES ,
"aud": "appstoreconnect-v1",
"nonce": SecureRandom.uuid,
"bid": "com.purchasely.demo"
}
jwt = JWT.encode(payload, PRIVATE_KEY, 'ES256', header)
š” You don't need to generate a new token for every API request. To get better performance, reuse the same signed token for up to 60 minutes.
Congratulations, you can now send request to the Apple servers.
For example:
curl -v -H 'Authorization: Bearer [signed token]'
"<https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{original_transaction_id}>"
If you have read our previous articles, you already know that this new signature has a lot of benefits. At Purchasely we will be using this mechanism as soon as it is released to fasten receipt verification and not rely on an external call to verifyReceipt.