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