jsondjangorestapp-storejson-web-signature

Deserializer JSON objects containing JSON Web Signatures in Django. App Store Server Notifications responseBodyV2


I have a python django rest application that I need it to be able to handle post request for App Store Server Notifications.

Thing is that v2 of the App Store Server Notifications payload is in JSON Web Signature (JWS) format, signed by the App Store. Which contains fields that in turn are also in JSON Web Signature (JWS) format, signed by the App Store. I know how to handle that using python-jose procedurally but I can't figure out how to fit the whole process within Django serializers in a graceful manner with as minimal hacking as possible.

The data could be something like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub3RpZmljYXRpb25UeXBlIjoidHlwZSIsInN1YnR5cGUiOiJzdWJfVHlwZSIsIm5vdGlmaWNhdGlvblVVSUQiOiJzdHJpbmcgbm90aWZpY2F0aW9uVVVJRCIsImRhdGEiOnsiYXBwQXBwbGVJZCI6MTIzNCwiYnVuZGxlSWQiOiJhZmRzYXNkIiwiYnVuZGxlVmVyc2lvbiI6ImJ1bmRsZVZlcnNpb24iLCJlbnZpcm9ubWVudCI6ImVudmlyb25tZW50Iiwic2lnbmVkUmVuZXdhbEluZm8iOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKaGRYUnZVbVZ1WlhkUWNtOWtkV04wU1dRaU9pSjBaWE4wSUhSdmJHVnRJaXdpWVhWMGIxSmxibVYzVTNSaGRIVnpJam94TENKbGVIQnBjbUYwYVc5dVNXNTBaVzUwSWpvMExDSm5jbUZqWlZCbGNtbHZaRVY0Y0dseVpYTkVZWFJsSWpveE5qTTJOVE0xTVRReExDSnBjMGx1UW1sc2JHbHVaMUpsZEhKNVVHVnlhVzlrSWpwMGNuVmxMQ0p2Wm1abGNrbGtaVzUwYVdacFpYSWlPaUowWlhOMElIUnZiR1Z0SWl3aWIyWm1aWEpVZVhCbElqb3hMQ0p2Y21sbmFXNWhiRlJ5WVc1ellXTjBhVzl1U1dRaU9pSjBaWE4wSUhSdmJHVnRJaXdpY0hKcFkyVkpibU55WldGelpWTjBZWFIxY3lJNk1Td2ljSEp2WkhWamRFbGtJam9pZEdWemRDQjBiMnhsYlNJc0luTnBaMjVsWkVSaGRHVWlPakUyTXpZMU16VXhOREY5LnYwWW9YQUd0MTFPeVBXUk8zV2xTZDRiSWVtcVV6Q0ZJbFdjd0ZwcEI5TmMiLCJzaWduZWRUcmFuc2FjdGlvbkluZm8iOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKaGNIQkJZMk52ZFc1MFZHOXJaVzRpT2lKMFpYTjBJSFJ2YkdWdElpd2lZblZ1Wkd4bFNXUWlPaUp6WkdaellYTmtaaUlzSW1WNGNHbHlaWE5FWVhSbElqb3hOak0yTlRNMU1UUXhMQ0pwYmtGd2NFOTNibVZ5YzJocGNGUjVjR1VpT2lKMFpYTjBJSFJ2YkdWdElpd2lhWE5WY0dkeVlXUmxaQ0k2ZEhKMVpTd2liMlptWlhKSlpHVnVkR2xtYVdWeUlqb2lkR1Z6ZENCMGIyeGxiU0lzSW05bVptVnlWSGx3WlNJNk1UUTFMQ0p2Y21sbmFXNWhiRkIxY21Ob1lYTmxSR0YwWlNJNk1UWXpOalV6TlRFME1Td2liM0pwWjJsdVlXeFVjbUZ1YzJGamRHbHZia2xrSWpvaWRHVnpkQ0IwYjJ4bGJTSXNJbkJ5YjJSMVkzUkpaQ0k2SW5SbGMzUWdkRzlzWlcwaUxDSndkWEpqYUdGelpVUmhkR1VpT2pFMk16WTFNelV4TkRFc0luRjFZVzUwYVhSNUlqb3hORFVzSW5KbGRtOWpZWFJwYjI1RVlYUmxJam94TmpNMk5UTTFNVFF4TENKeVpYWnZZMkYwYVc5dVVtVmhjMjl1SWpveE5EVXNJbk5wWjI1bFpFUmhkR1VpT2pFMk16WTFNelV4TkRFc0luTjFZbk5qY21sd2RHbHZia2R5YjNWd1NXUmxiblJwWm1sbGNpSTZJblJsYzNRZ2RHOXNaVzBpTENKMGNtRnVjMkZqZEdsdmJrbGtJam9pZEdWemRDQjBiMnhsYlNJc0luUjVjR1VpT2lKMFpYTjBJSFJ2YkdWdElpd2lkMlZpVDNKa1pYSk1hVzVsU1hSbGJVbGtJam9pZEdWemRDQjBiMnhsYlNKOS5lbnlkTnVwd2txOTNYQ2dfeG5yYzNXTmtNNjM4NXpITnpoa0tqa3cyb3VrIn19.OgSJ4xE3r2Tw0Q4KcwPSD4YFo21uCLDgrKOtKOomijo

and then the part inbetween the dots decoded could look like

b'{"notificationType":"type","subtype":"sub_Type","notificationUUID":"string notificationUUID","data":{"appAppleId":1234,"bundleId":"afdsasd","bundleVersion":"bundleVersion","environment":"environment","signedRenewalInfo":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRvUmVuZXdQcm9kdWN0SWQiOiJ0ZXN0IHRvbGVtIiwiYXV0b1JlbmV3U3RhdHVzIjoxLCJleHBpcmF0aW9uSW50ZW50Ijo0LCJncmFjZVBlcmlvZEV4cGlyZXNEYXRlIjoxNjM2NTM1MTQxLCJpc0luQmlsbGluZ1JldHJ5UGVyaW9kIjp0cnVlLCJvZmZlcklkZW50aWZpZXIiOiJ0ZXN0IHRvbGVtIiwib2ZmZXJUeXBlIjoxLCJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiJ0ZXN0IHRvbGVtIiwicHJpY2VJbmNyZWFzZVN0YXR1cyI6MSwicHJvZHVjdElkIjoidGVzdCB0b2xlbSIsInNpZ25lZERhdGUiOjE2MzY1MzUxNDF9.v0YoXAGt11OyPWRO3WlSd4bIemqUzCFIlWcwFppB9Nc","signedTransactionInfo":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBBY2NvdW50VG9rZW4iOiJ0ZXN0IHRvbGVtIiwiYnVuZGxlSWQiOiJzZGZzYXNkZiIsImV4cGlyZXNEYXRlIjoxNjM2NTM1MTQxLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJ0ZXN0IHRvbGVtIiwiaXNVcGdyYWRlZCI6dHJ1ZSwib2ZmZXJJZGVudGlmaWVyIjoidGVzdCB0b2xlbSIsIm9mZmVyVHlwZSI6MTQ1LCJvcmlnaW5hbFB1cmNoYXNlRGF0ZSI6MTYzNjUzNTE0MSwib3JpZ2luYWxUcmFuc2FjdGlvbklkIjoidGVzdCB0b2xlbSIsInByb2R1Y3RJZCI6InRlc3QgdG9sZW0iLCJwdXJjaGFzZURhdGUiOjE2MzY1MzUxNDEsInF1YW50aXR5IjoxNDUsInJldm9jYXRpb25EYXRlIjoxNjM2NTM1MTQxLCJyZXZvY2F0aW9uUmVhc29uIjoxNDUsInNpZ25lZERhdGUiOjE2MzY1MzUxNDEsInN1YnNjcmlwdGlvbkdyb3VwSWRlbnRpZmllciI6InRlc3QgdG9sZW0iLCJ0cmFuc2FjdGlvbklkIjoidGVzdCB0b2xlbSIsInR5cGUiOiJ0ZXN0IHRvbGVtIiwid2ViT3JkZXJMaW5lSXRlbUlkIjoidGVzdCB0b2xlbSJ9.enydNupwkq93XCg_xnrc3WNkM6385zHNzhkKjkw2ouk"}}'

and then if the fields encoded in jws format are also decoded the same way mentioned aboce it is going to ultimately look like this:

{
"notificationType":"type",
"subtype":"sub_Type",
"notificationUUID":"string notificationUUID",
"data":
    {"appAppleId":1234,
     "bundleId":"afdsasd",
     "bundleVersion":"bundleVersion",
     "environment":"environment",
     "signedRenewalInfo":{
        "autoRenewProductId": "test tolem", 
        "autoRenewStatus": 1,
        "expirationIntent" : 4,
        "gracePeriodExpiresDate":  1636535141, 
        "isInBillingRetryPeriod": true,
        "offerIdentifier":  "test tolem", 
        "offerType": 1,
        "originalTransactionId": "test tolem", 
        "priceIncreaseStatus":  1,
        "productId": "test tolem",
        "signedDate": 1636535141,
        },
      "signedTransactionInfo":{
        "appAccountToken": "test tolem", 
        "bundleId": "sdfsasdf",
        "expiresDate" : 1636535141,
        "inAppOwnershipType":  "test tolem", 
        "isUpgraded": true,
        "offerIdentifier":  "test tolem", 
        "offerType": 145,
        "originalPurchaseDate": 1636535141,
        "originalTransactionId": "test tolem", 
        "productId":  "test tolem",
        "purchaseDate": 1636535141,
        "quantity": 145,
        "revocationDate": 1636535141,
        "revocationReason": 145,
        "signedDate": 1636535141,
        "subscriptionGroupIdentifier":  "test tolem", 
        "transactionId": "test tolem",
        "type":  "test tolem",
        "webOrderLineItemId": "test tolem" 
        }}}

Which is what I want to store into my database tables

Any help or idea is going to be greatly appriciated


Solution

  • For anybody possibly dealing with the same issues this is how I handled the serialization it looked like it's working fine

     from .models import TransactionV2, RenewalInfoV2, 
        AppStoreDecodedPayloadV2, AppStoreDataV2
     from rest_framework import serializers
     from jose import jwt
     import base64
     import io
     from rest_framework.parsers import JSONParser
     import json
        
    class PayloadField(serializers.Field):
    
        def to_internal_value(self, obj):
            content = obj.split('.')[1]
            jws_payload =  base64.b64decode(content)
            data = json.loads(jws_payload.decode())
            serializer = AppStoreDecodedPayloadSerializerV2(data = data)
            if serializer.is_valid():
                return serializer.validated_data
    
    class SignedTransactionInfo(serializers.Field):
    
        def to_internal_value(self, obj):
            content = obj.split('.')[1]
            jws_payload =  base64.b64decode(content)
            data = json.loads(jws_payload.decode())
            serializer = TransactionV2Serializer(data = data)
            if serializer.is_valid():
                return serializer.validated_data
    
    
    class SignedRenewalInfoField(serializers.Field):
    
        def to_internal_value(self, obj):
            content = obj.split('.')[1]
            algorithm = obj.split('.')[0]
            secret = obj.split('.')[2]
    
            jws_payload =  base64.b64decode(content)
            jws_payload_algo = base64.b64decode(algorithm)
           
            algo = json.loads(jws_payload_algo.decode())
            data = json.loads(jws_payload.decode())
            serializer = RenewSerializer(data = data)
            if serializer.is_valid():
                return serializer.validated_data
    
    
    class AppStoreNotificationSerializerReuturnV2(serializers.Serializer):
        signedPayload =  PayloadField()
    
        def create(self, validated_data):
            app_store_decoded_payload_data = validated_data.pop('signedPayload')
            app_store_sub = AppStoreDecodedPayloadSerializerV2.create(app_store_decoded_payload_data)
            return app_store_sub
    
    class AppStoreDecodedPayloadDataSerializer(serializers.Serializer):
        appAppleId = serializers.IntegerField()
        bundleId = serializers.CharField()
        bundleVersion = serializers.CharField()
        environment = serializers.CharField()
        signedRenewalInfo = SignedRenewalInfoField()
        signedTransactionInfo = SignedTransactionInfo()
        
        def create(**decoded_payload_data):
            renewall_data = decoded_payload_data.pop('signedRenewalInfo')
            transaction_data = decoded_payload_data.pop('signedTransactionInfo')
            app_store_data = AppStoreDataV2.objects.create(**decoded_payload_data)
            TransactionV2.objects.create(**transaction_data ,app_store_data = app_store_data)
            RenewalInfoV2.objects.create(**renewall_data, app_store_data = app_store_data)
            return app_store_data
    
    
    class AppStoreDecodedPayloadSerializerV2(serializers.Serializer):
        notificationType = serializers.CharField()
        subtype = serializers.CharField()
        notificationUUID = serializers.CharField()
        data = AppStoreDecodedPayloadDataSerializer()
        
        def create(app_store_decoded_payload):
            app_store_decoded_payload_data = app_store_decoded_payload.pop('data')
            decoded_payload = AppStoreDecodedPayloadV2.objects.create(**app_store_decoded_payload)
            app_store_decoded_payload =AppStoreDecodedPayloadDataSerializer.create(**app_store_decoded_payload_data, app_store_decoded_payload = decoded_payload)
            return app_store_decoded_payload
    
    
    class TransactionV2Serializer(serializers.ModelSerializer):
    
        class Meta:
            fields = '__all__'
            model = TransactionV2
    
    
    class RenewSerializer(serializers.ModelSerializer):
    
        class Meta:
            fields = '__all__'
            model = RenewalInfoV2