Skip to content

Commit f2e2378

Browse files
author
Chris Ballinger
committed
Implement decoding of RealtimeRequestBody
Add serializer for RealtimeResponseBody Remove response model files Revert changes
1 parent 6f73956 commit f2e2378

File tree

5 files changed

+134
-0
lines changed

5 files changed

+134
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
2+
3+
from typing import Optional
4+
from attr import define
5+
import attr
6+
from .Environment import Environment
7+
from .LibraryUtility import AttrsRawValueAware
8+
9+
@define
10+
class DecodedRealtimeRequestBody(AttrsRawValueAware):
11+
"""
12+
The decoded request body the App Store sends to your server to request a real-time retention message.
13+
14+
https://developer.apple.com/documentation/retentionmessaging/decodedrealtimerequestbody
15+
"""
16+
17+
originalTransactionId: Optional[str] = attr.ib(default=None)
18+
"""
19+
The original transaction identifier of the customer's subscription.
20+
21+
https://developer.apple.com/documentation/retentionmessaging/originaltransactionid
22+
"""
23+
24+
appAppleId: Optional[int] = attr.ib(default=None)
25+
"""
26+
The unique identifier of the app in the App Store.
27+
28+
https://developer.apple.com/documentation/retentionmessaging/appappleid
29+
"""
30+
31+
productId: Optional[str] = attr.ib(default=None)
32+
"""
33+
The unique identifier of the auto-renewable subscription.
34+
35+
https://developer.apple.com/documentation/retentionmessaging/productid
36+
"""
37+
38+
locale: Optional[str] = attr.ib(default=None)
39+
"""
40+
The device's locale.
41+
42+
https://developer.apple.com/documentation/retentionmessaging/locale
43+
"""
44+
45+
requestIdentifier: Optional[str] = attr.ib(default=None)
46+
"""
47+
A UUID the App Store server creates to uniquely identify each request.
48+
49+
https://developer.apple.com/documentation/retentionmessaging/requestidentifier
50+
"""
51+
52+
environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment')
53+
"""
54+
The server environment, either sandbox or production.
55+
56+
https://developer.apple.com/documentation/retentionmessaging/environment
57+
"""
58+
59+
rawEnvironment: Optional[str] = Environment.create_raw_attr('environment')
60+
"""
61+
See environment
62+
"""
63+
64+
signedDate: Optional[int] = attr.ib(default=None)
65+
"""
66+
The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data.
67+
68+
https://developer.apple.com/documentation/retentionmessaging/signeddate
69+
"""
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
2+
3+
from typing import Optional
4+
from attr import define
5+
import attr
6+
7+
@define
8+
class RealtimeRequestBody:
9+
"""
10+
The request body the App Store server sends to your Get Retention Message endpoint.
11+
12+
https://developer.apple.com/documentation/retentionmessaging/realtimerequestbody
13+
"""
14+
15+
signedPayload: Optional[str] = attr.ib(default=None)
16+
"""
17+
The payload in JSON Web Signature (JWS) format, signed by the App Store.
18+
19+
https://developer.apple.com/documentation/retentionmessaging/signedpayload
20+
"""

appstoreserverlibrary/signed_data_verifier.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,30 @@ def verify_and_decode_app_transaction(self, signed_app_transaction: str) -> AppT
131131
raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT)
132132
return decoded_app_transaction
133133

134+
def verify_and_decode_retention_request(self, signed_payload: str):
135+
"""
136+
Verifies and decodes a retention messaging request from the App Store.
137+
Uses the same x509 certificate chain verification as other Apple notifications.
138+
139+
:param signed_payload: The signedPayload field from the retention request
140+
:return: The decoded retention request after verification
141+
:throws VerificationException: Thrown if the data could not be verified
142+
"""
143+
from .models.DecodedRealtimeRequestBody import DecodedRealtimeRequestBody
144+
145+
decoded_dict = self._decode_signed_object(signed_payload)
146+
decoded_request = _get_cattrs_converter(DecodedRealtimeRequestBody).structure(
147+
decoded_dict, DecodedRealtimeRequestBody
148+
)
149+
150+
if self._app_apple_id is not None and decoded_request.appAppleId != self._app_apple_id:
151+
raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)
152+
153+
if decoded_request.environment != self._environment:
154+
raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT)
155+
156+
return decoded_request
157+
134158
def _decode_signed_object(self, signed_obj: str) -> dict:
135159
try:
136160
decoded_jwt = jwt.decode(signed_obj, options={"verify_signature": False})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiIxMDAwMDAwMDAwMDAwMDAxIiwiYXBwQXBwbGVJZCI6MTIzNCwicHJvZHVjdElkIjoiY29tLmV4YW1wbGUuc3Vic2NyaXB0aW9uIiwibG9jYWxlIjoiZW5fVVMiLCJyZXF1ZXN0SWRlbnRpZmllciI6IjU1MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMCIsImVudmlyb25tZW50IjoiTG9jYWxUZXN0aW5nIiwic2lnbmVkRGF0ZSI6MTY4MTMxNDMyNDAwMH0.RvRAX6xYcozUaNIHLlL7saZX_Lk8Qlg3-INbBVPpwTVVRWmDCUlzlrTHBGKrbfVI73ppQPMXko5ChiwsDfOguQ

tests/test_payload_verification.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,25 @@ def test_malformed_jwt_with_malformed_data(self):
6868
verifier.verify_and_decode_notification("a.b.c")
6969
self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE)
7070

71+
def test_retention_request_decoding(self):
72+
verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example")
73+
retention_request = read_data_from_file('tests/resources/mock_signed_data/retentionRequest')
74+
decoded_request = verifier.verify_and_decode_retention_request(retention_request)
75+
self.assertEqual(decoded_request.originalTransactionId, "1000000000000001")
76+
self.assertEqual(decoded_request.appAppleId, 1234)
77+
self.assertEqual(decoded_request.productId, "com.example.subscription")
78+
self.assertEqual(decoded_request.locale, "en_US")
79+
self.assertEqual(decoded_request.requestIdentifier, "550e8400-e29b-41d4-a716-446655440000")
80+
self.assertEqual(decoded_request.environment, Environment.LOCAL_TESTING)
81+
self.assertEqual(decoded_request.signedDate, 1681314324000)
82+
83+
84+
def test_retention_request_wrong_app_id(self):
85+
verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example", 5678)
86+
retention_request = read_data_from_file('tests/resources/mock_signed_data/retentionRequest')
87+
with self.assertRaises(VerificationException) as context:
88+
verifier.verify_and_decode_retention_request(retention_request)
89+
self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER)
90+
7191
if __name__ == '__main__':
7292
unittest.main()

0 commit comments

Comments
 (0)