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
61pub trait AdvancedCommerceInAppRequest: Serialize {}
63
64struct 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
113pub struct PromotionalOfferV2SignatureCreator {
115 base: JWSSignatureCreator,
116}
117
118impl PromotionalOfferV2SignatureCreator {
119 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 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
183pub struct IntroductoryOfferEligibilitySignatureCreator {
185 base: JWSSignatureCreator,
186}
187
188impl IntroductoryOfferEligibilitySignatureCreator {
189 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 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
254pub struct AdvancedCommerceInAppSignatureCreator {
256 base: JWSSignatureCreator,
257}
258
259impl AdvancedCommerceInAppSignatureCreator {
260 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 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}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use serde_json::Value;
323
324 #[test]
325 fn test_promotional_offer_v2_signature_creator() {
326 let test_signing_key = include_str!("../resources/certs/testSigningKey.p8");
327 let creator = PromotionalOfferV2SignatureCreator::new(
328 test_signing_key,
329 "keyId".to_string(),
330 "issuerId".to_string(),
331 "bundleId".to_string(),
332 ).unwrap();
333
334 let signature = creator.create_signature(
335 "productId",
336 "offerIdentifier",
337 Some("transactionId".to_string()),
338 ).unwrap();
339
340 let parts: Vec<&str> = signature.split('.').collect();
341 assert_eq!(parts.len(), 3);
342
343 let header_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
345 .decode(parts[0])
346 .unwrap();
347 let header: Value = serde_json::from_slice(&header_bytes).unwrap();
348
349 assert_eq!(header["typ"], "JWT");
350 assert_eq!(header["alg"], "ES256");
351 assert_eq!(header["kid"], "keyId");
352
353 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
355 .decode(parts[1])
356 .unwrap();
357 let payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
358
359 assert_eq!(payload["iss"], "issuerId");
360 assert!(payload["iat"].is_number());
361 assert!(payload["exp"].is_null());
362 assert_eq!(payload["aud"], "promotional-offer");
363 assert_eq!(payload["bid"], "bundleId");
364 assert!(payload["nonce"].is_string());
365 assert_eq!(payload["productId"], "productId");
366 assert_eq!(payload["offerIdentifier"], "offerIdentifier");
367 assert_eq!(payload["transactionId"], "transactionId");
368 }
369
370 #[test]
371 fn test_promotional_offer_v2_signature_creator_without_transaction_id() {
372 let test_signing_key = include_str!("../resources/certs/testSigningKey.p8");
373 let creator = PromotionalOfferV2SignatureCreator::new(
374 test_signing_key,
375 "keyId".to_string(),
376 "issuerId".to_string(),
377 "bundleId".to_string(),
378 ).unwrap();
379
380 let signature = creator.create_signature(
381 "productId",
382 "offerIdentifier",
383 None,
384 ).unwrap();
385
386 let parts: Vec<&str> = signature.split('.').collect();
387 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
388 .decode(parts[1])
389 .unwrap();
390 let payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
391
392 assert!(payload["transactionId"].is_null());
393 }
394
395 #[test]
396 fn test_introductory_offer_eligibility_signature_creator() {
397 let test_signing_key = include_str!("../resources/certs/testSigningKey.p8");
398 let creator = IntroductoryOfferEligibilitySignatureCreator::new(
399 test_signing_key,
400 "keyId".to_string(),
401 "issuerId".to_string(),
402 "bundleId".to_string(),
403 ).unwrap();
404
405 let signature = creator.create_signature(
406 "productId",
407 true,
408 "transactionId",
409 ).unwrap();
410
411 let parts: Vec<&str> = signature.split('.').collect();
412 assert_eq!(parts.len(), 3);
413
414 let header_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
416 .decode(parts[0])
417 .unwrap();
418 let header: Value = serde_json::from_slice(&header_bytes).unwrap();
419
420 assert_eq!(header["typ"], "JWT");
421 assert_eq!(header["alg"], "ES256");
422 assert_eq!(header["kid"], "keyId");
423
424 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
426 .decode(parts[1])
427 .unwrap();
428 let payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
429
430 assert_eq!(payload["iss"], "issuerId");
431 assert!(payload["iat"].is_number());
432 assert!(payload["exp"].is_null());
433 assert_eq!(payload["aud"], "introductory-offer-eligibility");
434 assert_eq!(payload["bid"], "bundleId");
435 assert!(payload["nonce"].is_string());
436 assert_eq!(payload["productId"], "productId");
437 assert_eq!(payload["allowIntroductoryOffer"], true);
438 assert_eq!(payload["transactionId"], "transactionId");
439 }
440
441 #[derive(Debug, Serialize)]
442 struct TestInAppRequest {
443 #[serde(rename = "testData")]
444 test_data: String,
445 }
446
447 impl AdvancedCommerceInAppRequest for TestInAppRequest {}
448
449 #[test]
450 fn test_advanced_commerce_in_app_signature_creator() {
451 let test_signing_key = include_str!("../resources/certs/testSigningKey.p8");
452 let creator = AdvancedCommerceInAppSignatureCreator::new(
453 test_signing_key,
454 "keyId".to_string(),
455 "issuerId".to_string(),
456 "bundleId".to_string(),
457 ).unwrap();
458
459 let in_app_request = TestInAppRequest {
460 test_data: "testData".to_string(),
461 };
462
463 let signature = creator.create_signature(&in_app_request).unwrap();
464
465 let parts: Vec<&str> = signature.split('.').collect();
466 assert_eq!(parts.len(), 3);
467
468 let header_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
470 .decode(parts[0])
471 .unwrap();
472 let header: Value = serde_json::from_slice(&header_bytes).unwrap();
473
474 assert_eq!(header["typ"], "JWT");
475 assert_eq!(header["alg"], "ES256");
476 assert_eq!(header["kid"], "keyId");
477
478 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
480 .decode(parts[1])
481 .unwrap();
482 let payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
483
484 assert_eq!(payload["iss"], "issuerId");
485 assert!(payload["iat"].is_number());
486 assert!(payload["exp"].is_null());
487 assert_eq!(payload["aud"], "advanced-commerce-api");
488 assert_eq!(payload["bid"], "bundleId");
489 assert!(payload["nonce"].is_string());
490
491 let base64_encoded_request = payload["request"].as_str().unwrap();
493 let request_data = BASE64.decode(base64_encoded_request).unwrap();
494 let decoded_request: Value = serde_json::from_slice(&request_data).unwrap();
495 assert_eq!(decoded_request["testData"], "testData");
496 }
497
498}