app_store_server_library/
jws_signature_creator.rs

1use base64::engine::general_purpose::STANDARD as BASE64;
2use base64::Engine;
3use chrono::Utc;
4use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7use uuid::Uuid;
8use crate::primitives::advanced_commerce::in_app_request::AdvancedCommerceInAppRequest;
9
10#[derive(Error, Debug)]
11pub enum JWSSignatureCreatorError {
12    #[error("InvalidPrivateKey")]
13    InvalidPrivateKey,
14
15    #[error("JWTEncodingError: [{0}]")]
16    JWTEncodingError(#[from] jsonwebtoken::errors::Error),
17
18    #[error("SerializationError: [{0}]")]
19    SerializationError(#[from] serde_json::Error),
20}
21
22#[derive(Debug, Serialize, Deserialize)]
23struct BasePayload {
24    nonce: String,
25    iss: String,
26    bid: String,
27    aud: String,
28    iat: i64,
29}
30
31#[derive(Debug, Serialize, Deserialize)]
32struct PromotionalOfferV2Payload {
33    #[serde(flatten)]
34    base: BasePayload,
35    #[serde(rename = "productId")]
36    product_id: String,
37    #[serde(rename = "offerIdentifier")]
38    offer_identifier: String,
39    #[serde(rename = "transactionId", skip_serializing_if = "Option::is_none")]
40    transaction_id: Option<String>,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44struct IntroductoryOfferEligibilityPayload {
45    #[serde(flatten)]
46    base: BasePayload,
47    #[serde(rename = "productId")]
48    product_id: String,
49    #[serde(rename = "allowIntroductoryOffer")]
50    allow_introductory_offer: bool,
51    #[serde(rename = "transactionId")]
52    transaction_id: String,
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56struct AdvancedCommerceInAppPayload {
57    #[serde(flatten)]
58    base: BasePayload,
59    request: String,
60}
61
62/// Base struct for creating JWS signatures for App Store requests
63struct JWSSignatureCreator {
64    audience: String,
65    signing_key: EncodingKey,
66    key_id: String,
67    issuer_id: String,
68    bundle_id: String,
69}
70
71impl JWSSignatureCreator {
72    fn new(
73        audience: String,
74        signing_key: &str,
75        key_id: String,
76        issuer_id: String,
77        bundle_id: String,
78    ) -> Result<Self, JWSSignatureCreatorError> {
79        let key = EncodingKey::from_ec_pem(signing_key.as_bytes())
80            .map_err(|_| JWSSignatureCreatorError::InvalidPrivateKey)?;
81
82        Ok(Self {
83            audience,
84            signing_key: key,
85            key_id,
86            issuer_id,
87            bundle_id,
88        })
89    }
90
91    fn get_base_payload(&self) -> BasePayload {
92        BasePayload {
93            nonce: Uuid::new_v4().to_string(),
94            iss: self.issuer_id.clone(),
95            bid: self.bundle_id.clone(),
96            aud: self.audience.clone(),
97            iat: Utc::now().timestamp(),
98        }
99    }
100
101    fn create_signature<T: Serialize>(&self, payload: &T) -> Result<String, JWSSignatureCreatorError> {
102        let mut header = Header::new(Algorithm::ES256);
103        header.kid = Some(self.key_id.clone());
104        header.typ = Some("JWT".to_string());
105
106        let token = encode(&header, payload, &self.signing_key)?;
107        Ok(token)
108    }
109}
110
111/// Creator for Promotional Offer V2 signatures
112pub struct PromotionalOfferV2SignatureCreator {
113    base: JWSSignatureCreator,
114}
115
116impl PromotionalOfferV2SignatureCreator {
117    /// Creates a new `PromotionalOfferV2SignatureCreator` instance.
118    ///
119    /// # Arguments
120    ///
121    /// * `signing_key` - Your private key downloaded from App Store Connect (in PEM format)
122    /// * `key_id` - Your key ID from the Keys page in App Store Connect
123    /// * `issuer_id` - Your issuer ID from the Keys page in App Store Connect
124    /// * `bundle_id` - Your app's bundle ID
125    ///
126    /// # Returns
127    ///
128    /// A `Result` containing the `PromotionalOfferV2SignatureCreator` instance or an error.
129    pub fn new(
130        signing_key: &str,
131        key_id: String,
132        issuer_id: String,
133        bundle_id: String,
134    ) -> Result<Self, JWSSignatureCreatorError> {
135        let base = JWSSignatureCreator::new(
136            "promotional-offer".to_string(),
137            signing_key,
138            key_id,
139            issuer_id,
140            bundle_id,
141        )?;
142
143        Ok(Self { base })
144    }
145
146    /// Creates a promotional offer V2 signature.
147    ///
148    /// # Arguments
149    ///
150    /// * `product_id` - The unique identifier of the product
151    /// * `offer_identifier` - The promotional offer identifier that you set up in App Store Connect
152    /// * `transaction_id` - The unique identifier of any transaction that belongs to the customer.
153    ///   You can use the customer's appTransactionId, even for customers who haven't made any
154    ///   In-App Purchases in your app. This field is optional, but recommended.
155    ///
156    /// # Returns
157    ///
158    /// A `Result` containing the signed JWS string or an error.
159    ///
160    /// # References
161    ///
162    /// [Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
163    pub fn create_signature(
164        &self,
165        product_id: &str,
166        offer_identifier: &str,
167        transaction_id: Option<String>,
168    ) -> Result<String, JWSSignatureCreatorError> {
169        let base_payload = self.base.get_base_payload();
170        let payload = PromotionalOfferV2Payload {
171            base: base_payload,
172            product_id: product_id.to_string(),
173            offer_identifier: offer_identifier.to_string(),
174            transaction_id,
175        };
176
177        self.base.create_signature(&payload)
178    }
179}
180
181/// Creator for Introductory Offer Eligibility signatures
182pub struct IntroductoryOfferEligibilitySignatureCreator {
183    base: JWSSignatureCreator,
184}
185
186impl IntroductoryOfferEligibilitySignatureCreator {
187    /// Creates a new `IntroductoryOfferEligibilitySignatureCreator` instance.
188    ///
189    /// # Arguments
190    ///
191    /// * `signing_key` - Your private key downloaded from App Store Connect (in PEM format)
192    /// * `key_id` - Your key ID from the Keys page in App Store Connect
193    /// * `issuer_id` - Your issuer ID from the Keys page in App Store Connect
194    /// * `bundle_id` - Your app's bundle ID
195    ///
196    /// # Returns
197    ///
198    /// A `Result` containing the `IntroductoryOfferEligibilitySignatureCreator` instance or an error.
199    pub fn new(
200        signing_key: &str,
201        key_id: String,
202        issuer_id: String,
203        bundle_id: String,
204    ) -> Result<Self, JWSSignatureCreatorError> {
205        let base = JWSSignatureCreator::new(
206            "introductory-offer-eligibility".to_string(),
207            signing_key,
208            key_id,
209            issuer_id,
210            bundle_id,
211        )?;
212
213        Ok(Self { base })
214    }
215
216    /// Creates an introductory offer eligibility signature.
217    ///
218    /// # Arguments
219    ///
220    /// * `product_id` - The unique identifier of the product
221    /// * `allow_introductory_offer` - A boolean value that determines whether the customer
222    ///   is eligible for an introductory offer
223    /// * `transaction_id` - The unique identifier of any transaction that belongs to the customer.
224    ///   You can use the customer's appTransactionId, even for customers who haven't made any
225    ///   In-App Purchases in your app.
226    ///
227    /// # Returns
228    ///
229    /// A `Result` containing the signed JWS string or an error.
230    ///
231    /// # References
232    ///
233    /// [Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
234    pub fn create_signature(
235        &self,
236        product_id: &str,
237        allow_introductory_offer: bool,
238        transaction_id: &str,
239    ) -> Result<String, JWSSignatureCreatorError> {
240        let base_payload = self.base.get_base_payload();
241        let payload = IntroductoryOfferEligibilityPayload {
242            base: base_payload,
243            product_id: product_id.to_string(),
244            allow_introductory_offer,
245            transaction_id: transaction_id.to_string(),
246        };
247
248        self.base.create_signature(&payload)
249    }
250}
251
252/// Creator for Advanced Commerce In-App signatures
253pub struct AdvancedCommerceInAppSignatureCreator {
254    base: JWSSignatureCreator,
255}
256
257impl AdvancedCommerceInAppSignatureCreator {
258    /// Creates a new `AdvancedCommerceInAppSignatureCreator` instance.
259    ///
260    /// # Arguments
261    ///
262    /// * `signing_key` - Your private key downloaded from App Store Connect (in PEM format)
263    /// * `key_id` - Your key ID from the Keys page in App Store Connect
264    /// * `issuer_id` - Your issuer ID from the Keys page in App Store Connect
265    /// * `bundle_id` - Your app's bundle ID
266    ///
267    /// # Returns
268    ///
269    /// A `Result` containing the `AdvancedCommerceInAppSignatureCreator` instance or an error.
270    pub fn new(
271        signing_key: &str,
272        key_id: String,
273        issuer_id: String,
274        bundle_id: String,
275    ) -> Result<Self, JWSSignatureCreatorError> {
276        let base = JWSSignatureCreator::new(
277            "advanced-commerce-api".to_string(),
278            signing_key,
279            key_id,
280            issuer_id,
281            bundle_id,
282        )?;
283
284        Ok(Self { base })
285    }
286
287    /// Creates an Advanced Commerce in-app signed request.
288    ///
289    /// # Arguments
290    ///
291    /// * `advanced_commerce_in_app_request` - The request to be signed.
292    ///
293    /// # Returns
294    ///
295    /// A `Result` containing the signed JWS string or an error.
296    ///
297    /// # References
298    ///
299    /// [Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
300    pub fn create_signature<T: AdvancedCommerceInAppRequest>(
301        &self,
302        advanced_commerce_in_app_request: &T,
303    ) -> Result<String, JWSSignatureCreatorError> {
304        let json_data = serde_json::to_vec(advanced_commerce_in_app_request)?;
305        let base64_encoded_body = BASE64.encode(&json_data);
306
307        let base_payload = self.base.get_base_payload();
308        let payload = AdvancedCommerceInAppPayload {
309            base: base_payload,
310            request: base64_encoded_body,
311        };
312
313        self.base.create_signature(&payload)
314    }
315}