What is a JWS and how to encode it for Apple In-App Purchases?

In App Purchase Apple App Store Tech Backend

At WWDC21 Apple has introduced a new way to verify the receipts based on JWS. This changes a lot of things to your sensitive backend code 🥵. Don't worry that article will tell you what to do and how to do it 😌.

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.

Book a demo


How is a JWS composed?

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"

The JWS header

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.

The JWS payload

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 :)

The JWS signature

jws_header.jws_payload.JWS_SIGNATURE

This signature is obtained by encrypting the JWS Signing Input.

This JWS Signing Input is the concatenation of:

  • the Encoded JWS Header (jws_header)
  • a period ('.') character
  • the Encoded JWS Payload (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.

How and where JWS are used by Apple?

To communicate with Apple, your server will need to be able to encrypt and/or decrypt JWS in these cases:

  1. every requests sent to Apple servers will need to have an Authorization header containing a JWS (Authorization: Bearer [signed token])
  2. every time you'll need to read data sent by Apple, you'll need to decrypt a JWS. For example :
    • when your app send a purchase to your servers
    • when your receive a S2S notification
    • when you receive the result of a request you've made to Apple servers

How can I decrypt a JWS sent by Apple?

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:

  • the algorithm type present in the jws_header (as seen before)
  • the certificate chain contained in the x5c claim (ie: in the jws_header.x5c array)

Then use your favorite cryptographic library to verify the data.

💡 For the moment, Apple has not published the root certificate used to validate the chain. We'll update this article when they do.


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.

Book a demo


How can I encrypt the JWS header I will send to Apple?

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.

1. Get a private API key for the App Store Connect

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:

  1. Select Users and Access, and then select the Keys tab.
  2. Select In-App Purchase under the Key Type.
  3. Click Generate API Key or the Add (+) button.
  4. Enter a name for the key. The name is for your reference only and isn’t part of the key itself.
  5. Click Generate.

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.

  1. Log in to App Store Connect.
  2. Select Users and Access, and then select the Keys tab.
  3. Select In-App Purchase under the Key Type.
  4. Click Download API Key next to the new API key.

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.

2. Generating Tokens for API Requests

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:

  1. Select Users and Access, then select the Keys tab.
  2. The key IDs appear in a column under the Active heading. Hover the cursor next to a key ID to display the Copy Key ID link.
  3. Click Copy Key ID.

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 from the App Store Server API, 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.

Ready to increase your in-app revenue?

 
Sign up free Book a demo