Giter Site home page Giter Site logo

app-store-server-library-python's Introduction

Apple App Store Server Python Library

The Python server library for the App Store Server API and App Store Server Notifications. Also available in Swift, Node.js, and Java.

Table of Contents

  1. Installation
  2. Documentation
  3. Usage
  4. Support

Installation

Requirements

  • Python 3.7+

pip

pip install app-store-server-library

Documentation

Documentation

WWDC Video

Obtaining an In-App Purchase key from App Store Connect

To use the App Store Server API or create promotional offer signatures, a signing key downloaded from App Store Connect is required. To obtain this key, you must have the Admin role. Go to Users and Access > Integrations > In-App Purchase. Here you can create and manage keys, as well as find your issuer ID. When using a key, you'll need the key ID and issuer ID as well.

Obtaining Apple Root Certificates

Download and store the root certificates found in the Apple Root Certificates section of the Apple PKI site. Provide these certificates as an array to a SignedDataVerifier to allow verifying the signed data comes from Apple.

Usage

API Usage

from appstoreserverlibrary.api_client import AppStoreServerAPIClient, APIException
from appstoreserverlibrary.models.Environment import Environment

private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary

key_id = "ABCDEFGHIJ"
issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55"
bundle_id = "com.example"
environment = Environment.SANDBOX

client = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)

try:    
    response = client.request_test_notification()
    print(response)
except APIException as e:
    print(e)

Verification Usage

from appstoreserverlibrary.models.Environment import Environment
from appstoreserverlibrary.signed_data_verifier import VerificationException, SignedDataVerifier

root_certificates = load_root_certificates()
enable_online_checks = True
bundle_id = "com.example"
environment = Environment.SANDBOX
app_apple_id = None # appAppleId must be provided for the Production environment
signed_data_verifier = SignedDataVerifier(root_certificates, enable_online_checks, environment, bundle_id, app_apple_id)

try:    
    signed_notification = "ey.."
    payload = signed_data_verifier.verify_and_decode_notification(signed_notification)
    print(payload)
except VerificationException as e:
    print(e)

Receipt Usage

from appstoreserverlibrary.api_client import AppStoreServerAPIClient, APIException, GetTransactionHistoryVersion
from appstoreserverlibrary.models.Environment import Environment
from appstoreserverlibrary.receipt_utility import ReceiptUtility
from appstoreserverlibrary.models.HistoryResponse import HistoryResponse
from appstoreserverlibrary.models.TransactionHistoryRequest import TransactionHistoryRequest, ProductType, Order

private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary

key_id = "ABCDEFGHIJ"
issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55"
bundle_id = "com.example"
environment = Environment.SANDBOX

client = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)
receipt_util = ReceiptUtility()
app_receipt = "MI.."

try:    
    transaction_id = receipt_util.extract_transaction_id_from_app_receipt(app_receipt)
    if transaction_id != None:
        transactions = []
        response: HistoryResponse = None
        request: TransactionHistoryRequest = TransactionHistoryRequest(
            sort=Order.ASCENDING,
            revoked=False,
            productTypes=[ProductType.AUTO_RENEWABLE]
        )
        while response == None or response.hasMore:
            revision = response.revision if response != None else None
            response = client.get_transaction_history(transaction_id, revision, request, GetTransactionHistoryVersion.V2)
            for transaction in response.signedTransactions:
                transactions.append(transaction)
        print(transactions)
except APIException as e:
    print(e)

Promotional Offer Signature Creation

from appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator
import time

private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary

key_id = "ABCDEFGHIJ"
bundle_id = "com.example"

promotion_code_signature_generator = PromotionalOfferSignatureCreator(private_key, key_id, bundle_id)

product_id = "<product_id>"
subscription_offer_id = "<subscription_offer_id>"
application_username = "<application_username>"
nonce = "<nonce>"
timestamp = round(time.time()*1000)
base64_encoded_signature = promotion_code_signature_generator.create_signature(product_id, subscription_offer_id, application_username, nonce, timestamp)

Support

Only the latest major version of the library will receive updates, including security updates. Therefore, it is recommended to update to new major versions.

app-store-server-library-python's People

Contributors

alexanderjordanbaker avatar callumwatkins avatar dependabot[bot] avatar devinwang avatar elonzh avatar hakusai22 avatar igotit avatar izanger avatar krimkus avatar reskov avatar rickwierenga avatar shimastripe avatar tbenhamou avatar wft avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

app-store-server-library-python's Issues

Please declare machine-readable license metadata

👋 Thanks for this package! It'd simplify our compliance processes if you added some license metadata, so that our tools could tell that this package is MIT licensed. Example for how to do that in setup.py:

setup(
    ...,
    license="MIT",
    classifiers=[
        "License :: OSI Approved :: MIT License",
    ],
)

Memory leaks at cattrs converter

I suppose here is a memory leak. cattrs converter function use linecache by default to store a byte code of the generated converted function, so each time we called _get_cattrs_converter our custom dict function is cached.

POC

Click to expand
from appstoreserverlibrary.models.JWSTransactionDecodedPayload import JWSTransactionDecodedPayload
from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter

import gc


def leaky_function():
    c = _get_cattrs_converter(JWSTransactionDecodedPayload)  # cattrs.Converter()
    c.structure(
        {"originalTransactionId": "123"},
        JWSTransactionDecodedPayload,
    )


def count_objects_by_type():
    gc.collect()

    type_count = {}
    for obj in gc.get_objects():
        obj_type = type(obj)
        type_count[obj_type] = type_count.get(obj_type, 0) + 1
    return type_count


def diff_snapshots(before, after):
    diff = {}
    for key in after:
        before_count = before.get(key, 0)
        after_count = after[key]
        if after_count != before_count:
            diff[key] = (before_count, after_count, after_count - before_count)
    for key in before:
        if key not in after:
            diff[key] = (before[key], 0, -before[key])
    return diff


def print_diff(diff):
    sorted_diff = sorted(diff.items(), key=lambda item: abs(item[1][2]), reverse=True)

    print("Type                 | Before | After | Diff")
    print("---------------------|--------|-------|------")
    for key, (before_count, after_count, diff_count) in sorted_diff:
        print(
            f"{key.__name__:<20} | {before_count:6} | {after_count:6} | {diff_count:+6}")


def test_memory_leak():
    print("Taking snapshot before calling leaky_function...")
    before = count_objects_by_type()

    # Call the suspected leaky function multiple times
    for _ in range(1000):
        leaky_function()

    print("Taking snapshot after calling leaky_function...")
    after = count_objects_by_type()

    diff = diff_snapshots(before, after)
    print_diff(diff)


if __name__ == "__main__":
    test_memory_leak()
Taking snapshot before calling leaky_function...
Taking snapshot after calling leaky_function...
Type                 | Before | After | Diff
---------------------|--------|-------|------
list                 |    460 |   1460 |  +1000
tuple                |   2052 |   3052 |  +1000
dict                 |   1493 |   1497 |     +4

Leaking code

c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, **cattrs_overrides))
c.register_unstructure_hook_factory(has, lambda cl: make_dict_unstructure_fn(cl, c, **cattrs_overrides))

Possible fix

Looks like we can cache entirely _get_cattrs_converter

@lru_cache(maxsize=None)
def _get_cattrs_converter(destination_class: Type[T]) -> cattrs.Converter:

or disable line caching at the cattrs

def _get_cattrs_converter
...
make_dict_structure_fn(cl, c, _cattrs_use_linecache=False,

How to decode JWS signedTransactionInfo ?

Hey, i have problem, that can not decode signedTransactionInfo , it is not base64 string, it is not posible to decode with jwt.decode, how i can do that ?
Where i can find root certificates? Why it so difficult to simply decode string ?

How to implement load_root_certificates in the README

from appstoreserverlibrary.models.Environment import Environment
from appstoreserverlibrary.signed_data_verifier import VerificationException, SignedDataVerifier

root_certificates = load_root_certificates()
enable_online_checks = True
bundle_id = "com.example"
environment = Environment.SANDBOX
signed_data_verifier = SignedDataVerifier(root_certificates, enable_online_checks, environment, bundle_id)

try:    
    signed_notification = "ey.."
    payload = signed_data_verifier.verify_and_decode_notification()
    print(payload)
except VerificationException as e:
    print(e)

In the Verification Usage section, there is a load_root_certificates function, but I don't find the implementation. How should I implement this correctly?

Support stubbing AppStoreServerAPIClient for unit tests

Feature request: I'd like the ability to create mock responses to API client calls which will be returned by the next call to a specific client method.

Use case

I would like to do unit testing for my server's use of the library. I want to test the following features of my server:

  1. Customer service requests to extend subscription renewals
  2. Requests to get the latest subscription status of a customer (e.g. for testing my handling of older clients with ReceiptUtility)

Example code

Here's an example test one could write:

from appstoreserverlibrary import testing as appstore

replace_server_api_client(appstore.TestingAPIClient(...))

def test_customer_service_extension(client):
    def callback(original_transaction_id: str, extend_renewal_date_request: ExtendRenewalDateRequest) -> ExtendRenewalDateResponse:
        assert stuff_about_the_request(original_transaction_id, extend_renewal_date_request)
        return ExtendRenewalDateResponse(...)

    with appstore.client_stubber as stubber: # Asserts on leaving the `with` that the expected responses were consumed
        stubber.extend_subscription_renewal_date.add_response(callback)
        response = client.post('/api/customer-service/app-store/extend')
        assert response.code == 200

    # Check that our server is properly handling saving the effectiveDate to the database
    response = client.get('/api/app-store/renewal-date')
    assert response.code == 200
    assert has_been_extended(response)

[Feature] Asynchronous Implementation and Pydantic Support

Due to the use of synchronous requests in the library, functions such as get_transaction_history can take a long time to execute, posing significant challenges for developers looking to build async APIs. Additionally, many Python developers utilize Pydantic for data validation, so incorporating Pydantic into the library can greatly simplify its usage.

For those interested in an asynchronous implementation, I have created a library, app-store-server-library-python-async, which uses httpx for async operations. The library also includes guidance on how to incorporate Pydantic for data validation. You can review the code and see how to implement these features.

If you have any questions or suggestions, feel free to reach out!

Allow more recent versions of cattrs

This library current depends on cattrs version 23.1.2 specifically:

install_requires=["attrs >= 21.3.0", 'PyJWT >= 2.6.0, < 3', 'requests >= 2.28.0, < 3', 'cryptography >= 40.0.0, < 43', 'pyOpenSSL >= 23.1.1, < 25', 'asn1==2.7.0', 'cattrs==23.1.2'],

Is there any good reason to not set that as the minimum version, and allow more recent versions of that library to be installed, like what is done with attrs?

It means we are stuck on an old version of cattrs in our code base where we also have app-store-server-library installed.

`AppTransaction.appAppleId` is `None `when requesting `get_notification_history` and that makes verify_and_decode_app_transaction failed with `VerificationException: Verification failed with status INVALID_APP_IDENTIFIER`

image

...
  File "/home/elonzh/.cache/pypoetry/virtualenvs/unob-hX3-r8Vo-py3.10/lib/python3.10/site-packages/appstoreserverlibrary/signed_data_verifier.py", line 110, in verify_and_decode_app_transaction
    raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)
          │                     └ <enum 'VerificationStatus'>
          └ <class 'appstoreserverlibrary.signed_data_verifier.VerificationException'>
appstoreserverlibrary.signed_data_verifier.VerificationException: Verification failed with status INVALID_APP_IDENTIFIER

test notification request not working (401)in production enviroment requesting

even everything is set up correctly and i am requesting sandbox correctly and i have maximal rights on this auth_key i am getting 401 and i am not aware there is need for special auth key for prod
here is code

can someone help ?

import time

from appstoreserverlibrary.api_client import AppStoreServerAPIClient, Environment, APIException
from pydantic import BaseModel, Field, SecretBytes, SecretStr
from pydantic_settings import BaseSettings


class TestNotificationToken(BaseModel):
    test_notification_token: str = Field(alias='testNotificationToken')


class Settings(BaseSettings):
    issuer_id: SecretStr
    bundle_id: SecretStr
    key_id: SecretStr 

    class Config:
        extra = 'ignore'


settings = Settings(_env_file='.env.notification_test')
with open('auth_key.p8', 'rb') as f:
    private_key = SecretBytes(f.read())

print(f'this is testing {settings.bundle_id.get_secret_value()}')

client_sandbox = AppStoreServerAPIClient(private_key.get_secret_value(),
                                         settings.key_id.get_secret_value(),
                                         settings.issuer_id.get_secret_value(),
                                         settings.bundle_id.get_secret_value(),
                                         Environment.SANDBOX)

client_prod = AppStoreServerAPIClient(private_key.get_secret_value(),
                                      settings.key_id.get_secret_value(),
                                      settings.issuer_id.get_secret_value(),
                                      settings.bundle_id.get_secret_value(),
                                      Environment.PRODUCTION)


def make_request(client: AppStoreServerAPIClient):
    try:
        token=client.request_test_notification().testNotificationToken
        while True:
            try:
                resp=client.get_test_notification_status(token)
                print(resp.sendAttempts[0].sendAttemptResult.value)
                break
            except APIException as e:
                if e.http_status_code==404:
                    print('waiting for notification')
                    time.sleep(1)
                else:
                    raise e
    except APIException as e:
        print(f"error {e}")


if __name__ == '__main__':
    print("Sending test notification to sandbox")
    make_request(client_sandbox)
    print("testing prod")

this prints

this is testing <REDACTED>
Sending test notification to sandbox
SUCCESS
testing prod
error 401

Test notifications for production env fails

I realize that this is Beta, but I'm not seeing any reason in the code why we wouldn't be able to send a test notification request for the Production environment. We need to be able to test our server side stuff to make sure that production requests are handled correctly. However, when requesting test notifications with production as the environment, the calls fail with 401.

Don't think this is a configuration issue, because everything works in Sandbox.

Is there something I'm missing in the code that prevents calls to Production links? Or is this some other bug?

Local receipt validation

The App Store developer documentation describes validating receipts with the App Store as deprecated, and recommends performing the validation on the server locally:

To validate receipts on your server, follow the steps in Validating receipts on the device on your server.

- https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/validating_receipts_with_the_app_store

Validating the receipt locally requires you to develop or use code to read and decode the receipt as a PKCS #7 container, as defined by RFC 2315.

- https://developer.apple.com/documentation/appstorereceipts/validating_receipts_on_the_device

The README describes using ReceiptUtility.extract_transaction_id_from_app_receipt, but this only gets the transaction ID, and does not perform any validation:

Extracts a transaction id from an encoded App Receipt [...] NO validation is performed on the receipt

The example goes on to use AppStoreServerAPIClient.get_transaction_history, which seems to contradict the advice of performing the validation locally, and makes the receipt signing entirely redundant.

Does this library provide any way of performing this validation and extracting the fields of the receipt locally?

Also, given a receipt from a customer device, can I safely call AppStoreServerAPIClient.get_transaction_history when no validation of the receipt has taken place? Could the transaction ID in this case not have been spoofed?

Perhaps there's a different library I need. Any help on this is greatly appreciated.

extract_transaction_id_from_app_receipt problem

If an order contains two transaction information, two transaction IDs will be generated, but this extract_transaction_id_from_app_receipt function can only parse out one ID, which will cause the transaction ID of the other order to be lost.

Readme Documentation Examples

Examples in the readme are not fully contained examples. For instance there are references to functions not defined in this repo load_root_certificates

Type Error on Python 3.11

When running the first example, I get the following error:

Traceback (most recent call last):
File "C:\code\qoria\appstore-client\main.py", line 14, in
client = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\code\qoria\appstore-client\venv\Lib\site-packages\appstoreserverlibrary\api_client.py", line 97, in init
self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\code\qoria\appstore-client\venv\Lib\site-packages\cryptography\hazmat\primitives\serialization\base.py", line 25, in load_pem_private_key
return ossl.load_pem_private_key(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\code\qoria\appstore-client\venv\Lib\site-packages\cryptography\hazmat\backends\openssl\backend.py", line 747, in load_pem_private_key
return self._load_key(
^^^^^^^^^^^^^^^
File "C:\code\qoria\appstore-client\venv\Lib\site-packages\cryptography\hazmat\backends\openssl\backend.py", line 897, in _load_key
mem_bio = self._bytes_to_bio(data)
^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\code\qoria\appstore-client\venv\Lib\site-packages\cryptography\hazmat\backends\openssl\backend.py", line 479, in _bytes_to_bio
data_ptr = self._ffi.from_buffer(data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: from_buffer() cannot return the address of a unicode object

Missing py.typed file causes type checkers to ignore type annotations

Hi, thanks for the great package! I'm already using it to explore migration from verifyReceipt.

The core issue is this:

$ pip3 install app-store-server-library
$ pip3 install mypy
$ cat test.py
import appstoreserverlibrary as aslib

print(aslib.models.Environment.PRODUCTION)
$ mypy test.py
test.py:1: error: Skipping analyzing "appstoreserverlibrary": module is installed, but missing library stubs or py.typed marker  [import]
test.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
Found 1 error in 1 file (checked 1 source file)

I believe the issue is that while this project includes type annotations (which is great), it is missing the py.typed file. That tells mypy and other type checkers to treat it as having type annotations. You can find details either in PEP 561:

Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing. This marker applies recursively: if a top-level package includes it, all its sub-packages MUST support type checking as well.

or in mypy's documentation:

If you would like to publish a library package to a package repository yourself (e.g. on PyPI) for either internal or external use in type checking, packages that supply type information via type comments or annotations in the code should put a py.typed file in their package directory.

Getting Authentication Error

Hi, I am getting 401 error. I have tried both APIs information from App Store Connect API and In-App Purchase. Both didn't worked for me. I got key id, issuer id and bundle id correctly. Not sure if I am loading p8 file correctly having Developer Permissions. Check this out:

def read_private_key(file_path):
    with open(file_path, "rb") as key_file:
        signing_key = key_file.read()
    return signing_key

The error I get is while verifying receipt data:

Traceback (most recent call last):
  File "d:\Bots\sandrastone763\test7.py", line 41, in <module>
    response = client.get_transaction_history(transaction_id, revision, request)
  File "C:\Users\hp\AppData\Local\Programs\Python\Python310\lib\site-packages\appstoreserverlibrary\api_client.py", line 635, in get_transaction_history
    return self._make_request("/inApps/v1/history/" + transaction_id, "GET", queryParameters, None, HistoryResponse)
  File "C:\Users\hp\AppData\Local\Programs\Python\Python310\lib\site-packages\appstoreserverlibrary\api_client.py", line 490, in _make_request
    raise APIException(response.status_code)
appstoreserverlibrary.api_client.APIException: 401

Support Xcode-signed JWS in SignedDataVerifier

Feature request: I think SignedDataVerifier should support an Environment.XCODE environment which verifies JWS values signed by Xcode StoreKit testing.

The problem

Transaction JWS produced by StoreKit testing have a few differences from normal signed JWS:

  • Environment is Xcode
  • x5c claim is 1 certificate long (because the Xcode certificate is a root cert)

So trying to use SignedDataVerifier to verify such a claim will immediately throw an INVALID_CHAIN_LENGTH error. If this were to be bypassed, I imagine we'd run into an error about the JWS having the wrong environment.

Use case

When testing our iOS app during development, we use a local development server. To really test this well I want to use almost exactly the same code paths for StoreKit testing as I do for production.

Right now there’s no way to get a SignedDataVerifier to actually verify & decode a transaction or renewal info JWS.

I want code that looks like this:

def _get_verifier() -> SignedDataVerifier:
    if is_in_local_testing_environment():
        return SignedDataVerifier(
            root_certificates=[load_xcode_testing_certificate()],
            enable_online_checks=False,
            environment=Environment.XCODE,
            bundle_id='my.bundle.id'
        )
    else:
        # Production & Sandbox/staging cases
        return SignedDataVerifier(...)

@app.post('/api/app-store/transaction')
def process_transaction():
    new_transaction = request.json['transaction']
    verifier = _get_verifier()
    verified_transaction = verifier.verify_and_decode_signed_transaction(new_transaction)
    response = deliver_content(verified_transaction)
    return response

Workaround

Right now my test server can't actually just use the same code for both cases, so we'll have to do something like the following:

import jwt
import cattrs

def _verify_transaction(signed_transaction: str) -> JWSTransactionDecodedPayload:
    if is_in_local_testing_environment():
        # Just for simplicity; really I should load the signing key & verify the whole chain.
        # It doesn't really matter in my case, but it may matter in general.
        data = jwt.decode(signed_transaction, options={'verify_signature':False})
        return cattrs.structure(data, JWSTransactionDecodedPayload)
    else:
        # Production & Sandbox/staging cases
        verifier = SignedDataVerifier(...)
        return verifier.verify_and_decode_signed_transaction(signed_transaction)

@app.post('/api/app-store/transaction')
def process_transaction():
    new_transaction = request.json['transaction']
    verifier = _get_verifier()
    verified_transaction = verifier.verify_and_decode_signed_transaction(new_transaction)
    response = deliver_content(verified_transaction)
    return response

This looks not too bad, but there are a couple problems:

  1. I have to repeat this for each kind of JWS (except notifications since I can't get those on my local server anyway)
  2. Now I'm not testing my use of the library, I'm testing my use of the cattrs and jwt libraries.

Proposed solutions

  1. Support an Environment.XCODE, which disables checks for chain length & whatever else would normally fail with Xcode testing
  2. Alternatively, add a subclass of SignedDataVerifier called StoreKitTestingSignedDataVerifier which overrides the relevant functions (at least _decode_signed_object).

Optional fields in models

Because all model fields are labelled as optional, it's unclear when a field is expected to always be available but is mislabelled, versus when it is actually known to be optional. It necessitates adding many additional checks when accessing the fields of the models, especially when using type-checking such as Mypy (where you simply can't access an optional without first checking if it is None).

Unfortunately, the App Store Server API documentation (e.g. https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload) does not say which fields are optional in many cases. I would open a PR to mark non-optional fields as such, but I do not have enough information from Apple's own documentation. Perhaps someone working on this project from Apple would have the knowledge internally to do this and simultaneously improve the documentation for all developers. Alternatively, if even just the documentation was updated, I would be happy to contribute that knowledge to this project in the form of updates to the models.

It's clear that Apple knows when fields are optional. For example, take this Apple Developer Forum post where it is confirmed by Apple that JWSTransactionDecodedPayload.purchaseDate will always appear but quantity might not.

As a developer building a product with App Store Server Notifications, it is unnecessarily difficult.

Support mock JWS generation/verification for unit tests

Feature request: Support creating and verifying mock JWS values, such as transactions and notifications.

I'd like the ability to create mock JWS data that will be accepted by a SignedDataVerifier which is specifically configured for testing.

This is very similar to #22, but instead of the JWS data coming from Xcode StoreKit testing I want it to be generated in my python unit tests.

I don’t think this would need to actually generate a certificate, sign the tokens, etc. But that would certainly work.

Use case

I would like to do unit testing for my server's use of the library. I want to test the following features:

  1. Processing signed transactions to deliver content
    • Technically I could do this with #22 by reproducing each case I want to test & copying out a signed JWS transaction, but this is pretty tedious.
  2. Handling App Store notifications V2
    • StoreKit testing just can't do this at all!

Example code

Here are some tests one could write, with example API:

from appstoreserverlibrary import testing as appstore

def make_transaction():
    # Ideally this would fill in dummy values for unspecified data like
    # transactionId and deviceVerificationNonce
    return appstore.mock_transaction(
        environment=Environment.PRODUCTION,
        product_id="com.example.yearly-subscription",
        bundle_id="com.example",
        expires_date=datetime.datetime.now() + datetime.timedelta(days=30),
        type=Type.AUTO_RENEWABLE_SUBSCRIPTION
    )

# Replace a normal SignedDataVerifier with a magic testing one:
replace_server_signed_data_verifier(appstore.TestingSignedDataVerifier(...))

def test_transaction_processing(client):
    "Test that the client can get the proper content when they have a signed transaction."
    response = client.post('/api/app-store/exchange-transaction-for-content', json.dumps({
        'transaction' : make_transaction()
    }))
    assert response.code == 200
    assert has_yearly_content(response.json)

def test_refund_notification(client):
    "Test that our server is properly revoking access for refunded transactions"
    tx = make_transaction()
    notif_jws = appstore.mock_notification_v2(
        type=NotificationTypeV2.REFUND,
        subtype=None,
        data=appstore.mock_notification_data(
            bundle_id="com.example",
            transaction=appstore.mock_revoked_transaction(tx),
            renewal_info=appstore.mock_renewal_info(...),
            ...
        )
    )
    response = client.post('/api/app-store/notifications-v2', notif_jws)
    assert response.code == 200

    response = client.post('/api/app-store/exchange-transaction-for-content', json.dumps({
        'transaction' : tx
    }))
    assert response.code == 403
    assert not has_yearly_content(response.json)

Raise maximum version of cryptography

Explanation

The cryptography dependency is limited to cryptographty >= 40.0.0, < 42 in your setup.py. I suggest allowing major version 42.

Motivation

The latest version of cryptography (42.0.4) fixes a CVE:

* Fixed a null-pointer-dereference and segfault that could occur when creating
  a PKCS#12 bundle. Credit to **Alexander-Programming** for reporting the
  issue. **CVE-2024-26130**

Since this library limits its version of cryptography, no one who depends on this library can use the updated version.

Version 42 Changes

The cryptography changelog only lists two ‘backwards incompatible changes’ in version 42. I don’t see any direct usage of the two mentioned functions using GitHub code search, but I’m not familiar with all the ways cryptography might be used in this library.

* **BACKWARDS INCOMPATIBLE:** Dropped support for LibreSSL < 3.7.
* **BACKWARDS INCOMPATIBLE:** Loading a PKCS7 with no content field using
    :func:`~cryptography.hazmat.primitives.serialization.pkcs7.load_pem_pkcs7_certificates`
  or
  :func:`~cryptography.hazmat.primitives.serialization.pkcs7.load_der_pkcs7_certificates`
  will now raise a ``ValueError`` rather than return an empty list.

Getting '0' as a result of extract_transaction_id_from_app_receipt

Hi,

We are trying to use this package for backend validation of the in-app purchase.
We followed all of your guidelines and request_test_notification and verify_and_decode_notification works fine.
However, when we try to call extract_transaction_id_from_app_receipt we get a response "0" which is an invalid transaction id.
The receipt looks like: app_receipt = "MIAGCSqGSIb3DQEHAqCAMIAC....." which seems valid..
We are using https://pub.dev/packages/in_app_purchase to handle the purchases and the app_receipt is purchaseDetails.verificationData.serverVerificationData

Any assistance would be appreciated.

app_apple_id for SignedDataVerifier should be `int` type, not `str`

class SignedDataVerifier:
    """
    A class providing utility methods for verifying and decoding App Store signed data.
    """
    def __init__(
        self,
        root_certificates: List[bytes],
        enable_online_checks: bool,
        environment: Environment,
        bundle_id: str,
        app_apple_id: str = None,
    )

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.