Heroic Labs Documentation

In-App Purchase Validation #

The spectrum of monetization models and tools is extremely varied, from ad-supported, microtransactions, freemium, one-off purchases, and everything in between. A key tool in many of these solutions is the In-App Purchase(IAP). IAPs enable single purchases for unlocks, in-game consumables, subscriptions for premium access, and more.

There are a number of readily available attacks against the most common in-app purchase implementations. These are usually focused around:

  • Feeding the client fake purchase responses which indicate success
  • Replaying a valid purchase response multiple times
  • Sharing a purchase response with another client, so multiple players can receive the reward from a single purchase

For in-app purchases a trusted source of truth is required. Nakama checks and tracks purchases and purchase history, solving a significant set of possible vulnerabilities and pain points:

IssueDescription
Fake PurchasesNakama directly connects to Apple, Google and Huawei services to check the validity of all incoming purchase receipts. This verification is completely outside the client’s code, and cannot be intercepted and tampered with. Every purchase receipt is verified, every time, and invalid ones are rejected.
Replay AttacksAll transactions are logged, preventing multiple submissions of the same purchase token or receipt.
Receipt SharingSuccessful transactions are bound to the account that submits them. Different users cannot submit the same transaction, even a valid one, in an attempt to receive the associated reward.
Product MismatchesEach validated purchase receipt exposes data (e.g. product ID) that can be used to tie a purchase to a product, preventing attacks that attempt to use a valid (cheap) purchase to unlock a different (expensive) reward.
Single Source of TruthWhile Nakama maintains an internal record of all transactions, the remote payment provider is always used for validation.

In-App Purchase Validation is available for Apple, Google, and Huawei purchases, regardless of platform. Both single product and subscription purchases are supported.

Apple and Google purchases made via Unity IAP are also supported.

Interpreting validation results #

A validation result contains a list of validated purchases.

Because Apple may contain multiple purchases in a single receipt, each validated purchase in the resulting list will contain only the purchases which have been validated and a boolean value to indicate if it has been seen before or not. If a purchase has already been validated by Nakama previously then the “seen before” value will be true, allowing the developer to discriminate new purchases and protect against replay attacks.

For Google and Huawei, each validation corresponds to a single purchase, which is included in the validation response list and also has a “seen before” value as above.

Each validated purchase also includes the payload of the provider validation response, should the developer need it for any reason.

Should the purchase/receipt be invalid, the validation fail for any reason, or the provider be unreachable, an error will be returned.

Apple #

Nakama supports validating purchases made for products and subscriptions in iOS.

Apple purchase receipts are sent to Apple for validation. As suggested by Apple, both Production and Sandbox servers are used to validate receipts.

Setup #

To validate receipts against the App Store, Nakama requires your app’s shared secret.

  1. Setup a shared secret in App Store Connect under your app’s In-App Purchases management section:

    Apple App Store Connect
    Apple App Store Connect

  2. Make a record of your shared secret for use in your Nakama configuration:

    Apple App Store Connect Shared Secret
    Apple App Store Connect Shared Secret

  3. Set the value of Nakama’s iap.apple.shared_password configuration flag to the value of the shared secret created above.

Validate purchase #

Nakama only supports validating iOS 7+ receipts.

Apple receipts can contain multiple purchases, Nakama will validate all of them and store them as individual purchase records.

Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/purchase/apple \
  --user 'defaultkey:' \
  --data '{"receipt":"base64_encoded_receipt_data"}'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local function validate_receipt(receipt)
  local result = client.validate_purchase_apple(receipt)
  if result.error then
    print(result.message)
    return
  end
  pprint(result)
end

-- Use https://defold.com/extension-iap/
iap.set_listener(function(self, transaction, error)
  if not error then
    validate_receipt(transaction.receipt)
  end
end)
iap.buy("com.defold.nakama.goldbars-10")
Client
1
2
3
4
5
6
string appleReceipt = "<receipt>";
var response = await client.ValidatePurchaseAppleAsync(session, appleReceipt);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
Client
1
2
3
4
5
const appleReceipt = "<receipt>";
const result = await client.validatePurchaseApple(session, appleReceipt);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
Client
1
2
3
4
5
6
7
var apple_receipt = "<receipt>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_apple_async(session, apple_receipt), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
Client
1
2
3
4
5
6
7
8
POST /v2/iap/purchase/apple
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "receipt": "base64_encoded_Apple_receipt_payload"
}

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.

Refer to the function reference page for the provided runtime purchase validation functions.

Validate subscription #

Client
1
2
3
4
string appleReceipt = "<receipt>";
var response = await client.ValidateSubscriptionAppleAsync(session, appleReceipt);

System.Console.WriteLine("Validated subscription: " + response.ValidatedSubscription);
Client
1
2
3
4
5
6
7
8
var apple_receipt = "..."
var response : NakamaAPI.ApiValidateSubscriptionResponse = yield(client.validate_subscription_apple_async(session, apple_receipt), "completed")

if response.is_exception():
    print("An error occurred: %s" % response)
    return

print("Validated subscription: %s" % response.validated_subscription)
Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/subscription/apple \
  --user 'defaultkey:' \
  --data '{"receipt":"base64_encoded_receipt_data"}'
Client
1
2
3
4
5
6
7
8
POST /v2/iap/subscription/apple
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "receipt": "base64_encoded_Apple_receipt_payload"
}
Client
1
2
3
4
5
local apple_receipt = "<receipt>";
local response = client.validate_purchase_apple(receipt)
for i,purchase in ipairs(response.validated_purchase) do
  pprint("Validated purchase: ", purchase)
end

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language JavaScript/Cocos2d-js has not been found. Please choose another language to show equivalent examples.

Refer to the function reference page for the provided runtime subscription validation functions.

Google #

Nakama supports validating purchases made for products and subscriptions on Android.

Setup #

To validate receipts against the Play Store, Nakama requires your Google Service Account ClientEmail and PrivateKey. To get these values:

  1. Setup a Service Account in the Google API Console:

    Create Service Account
    Create Service Account

  2. Once a service account is created, you’ll need to create a key:

    Create Key
    Create Key

  3. Download the key as a JSON file:

    Create JSON Key
    Create JSON Key

  4. Open the JSON file and extract the values of ClientEmail and PrivateKey.

  5. Set those values as the respective Nakama configuration values for iap.google.client_email and purchase.google.private_key, respectively.

  6. Finally you will need to ensure you grant Nakama access to the purchase validation APIs. From the Google Play Developer Console and navigate to Settings > API Access:

    Create API Access
    Create API Access

  7. The service account you created in the previous steps should be listed above. Grant access to the service account to access the API, making sure you give the service account access to Visibility, View Financial Data, and Manage Orders. These permissions are required for Nakama to validate receipts against Google Play.

    Grant Access
    Grant Access

  8. Navigate to Users & Permissions to check that the service account is setup correctly:

    List users with access
    List users with access

Validate purchase #

The Google receipt expected is the string returned by Purchase.getOriginalJson(). See the official Google Developer docs for more details.
Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/purchase/google \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data"}'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local function validate_receipt(receipt)
  local result = client.validate_purchase_google(receipt)
  if result.error then
    print(result.message)
    return
  end
  pprint(result)
end

-- Use https://defold.com/extension-iap/
iap.set_listener(function(self, transaction, error)
  if not error then
    validate_receipt(transaction.receipt)
  end
end)
iap.buy("com.defold.nakama.goldbars-10")
Client
1
2
3
4
5
6
string googleReceipt = "<receipt>";
var response = await client.ValidatePurchaseGoogleAsync(session, googleReceipt);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
Client
1
2
3
4
5
const googleReceipt = "<receipt>";
const result = await client.validatePurchaseGoogle(session, googleReceipt);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
Client
1
2
3
4
5
6
7
var google_receipt = "<receipt>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_google_async(session, google_receipt), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
Client
1
2
3
4
5
6
7
8
POST /v2/iap/purchase/google
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data"
}

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.

Refer to the function reference page for the provided runtime purchase validation functions.

Validate subscription #

Client
1
2
3
4
string googleReceipt = "<receipt>";
var response = await client.ValidateSubscriptionGoogleAsync(session, googleReceipt);

System.Console.WriteLine("Validated subscription: " + response.ValidatedSubscription);
Client
1
2
3
4
5
6
7
8
var google_receipt = "..."
var response : NakamaAPI.ApiValidateSubscriptionResponse = yield(client.validate_subscription_google_async(session, google_receipt), "completed")

if response.is_exception():
    print("An error occurred: %s" % response)
    return

print("Validated subscription: %s" % response.validated_subscription)
Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/subscription/google \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data"}'
Client
1
2
3
4
5
6
7
8
POST /v2/iap/subscription/google
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data"
}
Client
1
2
3
4
5
local google_receipt = "<receipt>";
local response = client.validate_purchase_google(receipt)
for i,purchase in ipairs(response.validated_purchase) do
  pprint("Validated purchase: ", purchase)
end

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language JavaScript/Cocos2d-js has not been found. Please choose another language to show equivalent examples.

Refer to the function reference page for the provided runtime subscription validation functions.

Huawei #

Nakama validates Huawei purchases against their IAP validation service. As suggested by Huawei, the validity of the purchase data is also checked against the provided signature before contacting the Huawei service. If the data is invalid for any reason, the purchase is rejected before validation with Huawei’s validation service.

Validate purchase #

Client
1
2
3
curl "http://127.0.0.1:7350/v2/iap/purchase/huawei \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data","signature":"purchase_data_signature"}'
Client
1
-- Huawei purchases are not yet supported by https://defold.com/extension-iap/
Client
1
2
3
4
5
6
7
string huaweiReceipt = "<receipt>";
string huaweiSignature = "<signature>";
var response = await client.ValidatePurchaseHuaweiAsync(session, huaweiReceipt, huaweiSignature);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
Client
1
2
3
4
5
6
const huaweiReceipt = "<receipt>";
const huaweiSignature = "<signature>";
const result = await client.validatePurchaseHuawei(session, huaweiReceipt, huaweiSignature);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
Client
1
2
3
4
5
6
7
8
var huawei_receipt = "<receipt>"
var huawei_signature = "<signature>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_huawei_async(session, huawei_receipt, huawei_signature), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
Client
1
2
3
4
5
6
7
8
9
POST /v2/iap/purchase/huawei
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data",
  "signature":"purchase_data_signature"
}

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.

Refer to the function reference page for the provided runtime purchase validation functions.

Unity IAP #

Unity IAP has support for integrating with most popular app stores, including Apple and Google.

Unity IAP purchase receipts contain the following information:

KeyValue
StoreThe name of the store in use, such as GooglePlay or AppleAppStore.
TransactionIDThis transaction’s unique identifier, provided by the store.
PayloadFor Apple, a Base64 encoded App Receipt. For Google, JSON encoded purchase data and a signature.

Validate purchase #

Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.
Code snippet for this language TypeScript has not been found. Please choose another language to show equivalent examples.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type unityIAP struct {
	  Payload       string `json:"Payload"`
	  Store         string `json:"Store"`
	  TransactionID string `json:"TransactionID"`
}

switch unityIAP.Store {
    case "GooglePlay":
        validatedReceipt, err = nk.PurchaseValidateGoogle(ctx, userID, wrapper.Payload)
    case "AppleAppStore":
        validatedReceipt, err = nk.PurchaseValidateApple(ctx, userID, wrapper.Payload)
    default:
        logger.Warn("Unrecognised store type in Unity IAP.")
      return ErrBadInput
    }

    if err != nil {
        if err == runtime.ErrPurchaseReceiptAlreadySeen {
            logger.WithField("err", err).Warn("Receipt replay attack.")
        } else {
            logger.WithField("err", err).Error("Receipt validation error.")
        }
        return ErrBadInput
}

Subscriptions #

In addition to validation of purchases and subscriptions, you can also get subscriptions by specific product ID or list all subscriptions for a given user.

Get subscription #

Client
1
2
3
4
string productId = "<productId>";
var response = await client.GetSubscriptionAsync(session, productId);

System.Console.WriteLine("Subscription: " + response);
Client
1
2
3
4
5
6
7
8
var product_id = "..."
var response : NakamaAPI.ApiValidatedSubscription = yield(client.get_subscription_async(session, product_id), "completed")

if response.is_exception():
    print("An error occurred: %s" % response)
    return

print("Validated subscription: %s" % response)
Client
1
2
curl "http://127.0.0.1:7350/v2/iap/subscription/{productId} \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
GET /v2/iap/subscription/{productId}
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
local product_id = "<productId>";
local response = client.get_subscription(product_id)
pprint("Subscription:", response)

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language JavaScript/Cocos2d-js has not been found. Please choose another language to show equivalent examples.

Refer to the function reference page for the provided runtime subscription validation functions.

List user subscriptions #

Client
1
2
3
4
5
6
7
string userId = "<userId>";
var response = await client.ListSubscriptionsAsync(userId);

foreach (var validatedSubscription in response.ValidatedSubscriptions)
{
    System.Console.WriteLine("Subscription: " + validatedSubscription);
}
Client
1
2
3
4
5
6
7
8
var response : NakamaAPI.ApiSubscriptionList = yield(client.get_subscription_list(session), "completed")

if response.is_exception():
    print("An error occurred: %s" % response)
    return

for p in result.validated_subscriptions:
    print("Validated subscription: %s" % p.validated_subscription)
Client
1
2
curl "http://127.0.0.1:7350/v2/iap/subscription/limit=<limit>&cursor=<cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
GET /v2/iap/subscription/limit=<limit>&cursor=<cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
local response = client.list_subscriptions()
for i,subscription in ipairs(response.validatedSubscriptions) do
  pprint("Subscription:", response)
end

Code snippet for this language C++/Unreal/Cocos2d-x has not been found. Please choose another language to show equivalent examples.
Code snippet for this language Java/Android has not been found. Please choose another language to show equivalent examples.
Code snippet for this language JavaScript/Cocos2d-js has not been found. Please choose another language to show equivalent examples.

Refer to the function reference page for the provided runtime subscription validation functions.