Tüm Yazılar
KategoriBackend
Okuma Süresi
18 dk okuma
Yayın Tarihi
...
Kelime Sayısı
1.381kelime

Kahveni hazırla - bu içerikli bir makale!

Server-side receipt validation iOS (JWS verification) + Android (Google Play Developer API) cross-platform implementation. Security, fraud prevention, grant entitlement.

Server-Side Receipt Validation: StoreKit 2 + Play Billing Cross-Platform

# Server-Side Receipt Validation: StoreKit 2 + Play Billing Cross-Platform

Client-side IAP validation güvensiz. Jailbreak + rooted device'larda fake purchase trivial. Server-side receipt validation zorunlu — ya RevenueCat gibi 3rd-party ya da kendi backend. Bu yazı iOS StoreKit 2 JWS (JSON Web Signature) verification, Android Google Play Developer API, unified abstraction layer ve fraud prevention pattern'leri ile production-ready setup'ı anlatır.

💡 Pro Tip: Client'tan gelen "bu user pro" iddiasına asla güvenme — her zaman server validation yap. Jailbreak'li iPhone'da fake JWS üretmek 5 dakikalık iş.

İçindekiler


Neden Server-Side Validation

Client-Only Risks

  1. Jailbreak/root bypass: Hex edit ile "pro:true" inject
  2. Receipt replay: Aynı receipt farklı device'larda
  3. Dev environment contamination: Sandbox receipt production'a geçiş
  4. Fake receipt: Scripted receipt üretme (StoreKit 1'de yaygındı)

Server-Side Benefits

  • Cryptographic verification (signature valid mi)
  • Apple/Google'dan real-time status
  • Refund detection (app offline'da bile server biliyor)
  • Cross-device sync (user A device'ta purchase, B device'ta login)

iOS StoreKit 2: JWS Verification

StoreKit 2'de receipt = JWS (JSON Web Signature) — ECDSA signed.

JWS Verification (Node.js)

typescript
1import jwt from 'jsonwebtoken';
2import { X509Certificate } from 'node:crypto';
3 
4const APPLE_ROOT_CA_G3 = `-----BEGIN CERTIFICATE-----
5...Apple Root CA G3 cert...
6-----END CERTIFICATE-----`;
7 
8async function verifyJWS(signedPayload: string): Promise {
9 // 1. Header decode
10 const [headerB64] = signedPayload.split('.');
11 const header = JSON.parse(Buffer.from(headerB64, 'base64url').toString());
12 
13 // 2. Cert chain (x5c header)
14 const certChain = header.x5c.map(
15 (c: string) => new X509Certificate(Buffer.from(c, 'base64'))
16 );
17 
18 // 3. Verify cert chain against Apple Root CA
19 await verifyCertificateChain(certChain, APPLE_ROOT_CA_G3);
20 
21 // 4. Extract public key from leaf cert
22 const leafCert = certChain[0];
23 const publicKey = leafCert.publicKey;
24 
25 // 5. Verify signature
26 const decoded = jwt.verify(signedPayload, publicKey, {
27 algorithms: ['ES256']
28 });
29 
30 return decoded as TransactionInfo;
31}

App Store Server API Usage

Apple'ın REST API ile real-time status:

typescript
1import axios from 'axios';
2 
3async function getTransactionStatus(
4 originalTransactionId: string
5): Promise {
6 const token = generateAppStoreServerAPIToken(); // JWT with private key
7 
8 const response = await axios.get(
9 `https://api.storekit.itunes.apple.com/inApps/v1/transactions/${originalTransactionId}`,
10 {
11 headers: { 'Authorization': `Bearer ${token}` }
12 }
13 );
14 
15 const { signedTransactionInfo } = response.data;
16 const transaction = await verifyJWS(signedTransactionInfo);
17 
18 return {
19 isActive: transaction.expiresDate! > Date.now(),
20 productId: transaction.productId,
21 expiresAt: new Date(transaction.expiresDate!),
22 isFamilyShared: transaction.inAppOwnershipType === 'FAMILY_SHARED'
23 };
24}

JWT Token Generation

typescript
1import fs from 'fs';
2import jwt from 'jsonwebtoken';
3 
4function generateAppStoreServerAPIToken(): string {
5 const privateKey = fs.readFileSync('SubscriptionKey_XXXX.p8');
6 
7 return jwt.sign(
8 {
9 iss: APPLE_ISSUER_ID,
10 iat: Math.floor(Date.now() / 1000),
11 exp: Math.floor(Date.now() / 1000) + 3600,
12 aud: 'appstoreconnect-v1',
13 bid: APPLE_BUNDLE_ID
14 },
15 privateKey,
16 {
17 algorithm: 'ES256',
18 keyid: APPLE_KEY_ID,
19 header: { typ: 'JWT' }
20 }
21 );
22}

Android Play Billing: Google Play API

Google Play Developer API ile subscription validation:

typescript
1import { google } from 'googleapis';
2 
3async function validateAndroidPurchase(
4 packageName: string,
5 productId: string,
6 purchaseToken: string
7): Promise {
8 const auth = new google.auth.GoogleAuth({
9 keyFile: 'service-account.json',
10 scopes: ['https://www.googleapis.com/auth/androidpublisher']
11 });
12 
13 const androidpublisher = google.androidpublisher({
14 version: 'v3',
15 auth: await auth.getClient()
16 });
17 
18 const result = await androidpublisher.purchases.subscriptionsv2.get({
19 packageName,
20 token: purchaseToken
21 });
22 
23 const subscription = result.data;
24 const lineItem = subscription.lineItems![0];
25 
26 return {
27 isActive: subscription.subscriptionState === 'SUBSCRIPTION_STATE_ACTIVE',
28 productId: lineItem.productId!,
29 expiresAt: new Date(lineItem.expiryTime!),
30 isFamilyShared: false, // Android doesn't have family sharing yet
31 autoRenewing: lineItem.autoRenewingPlan?.autoRenewEnabled ?? false
32 };
33}

Service Account Setup

  1. Google Cloud Console → IAM → Create Service Account
  2. Android Publisher API enable
  3. Play Console → Users & Permissions → grant access
  4. Service account key (JSON) download

Unified Abstraction

Cross-platform single interface:

typescript
1interface SubscriptionStatus {
2 isActive: boolean;
3 productId: string;
4 expiresAt: Date;
5 isFamilyShared: boolean;
6 autoRenewing: boolean;
7 platform: 'ios' | 'android';
8}
9 
10class PurchaseValidator {
11 async validate(
12 platform: 'ios' | 'android',
13 token: string
14 ): Promise {
15 if (platform === 'ios') {
16 return { ...await validateiOS(token), platform: 'ios' };
17 } else {
18 return { ...await validateAndroid(token), platform: 'android' };
19 }
20 }
21 
22 async grantEntitlement(userId: string, status: SubscriptionStatus) {
23 if (status.isActive) {
24 await db.users.update(userId, {
25 isPro: true,
26 proExpiresAt: status.expiresAt,
27 proPlatform: status.platform
28 });
29 }
30 }
31}
32 
33// Usage
34const validator = new PurchaseValidator();
35const status = await validator.validate('ios', jwsToken);
36await validator.grantEntitlement(userId, status);

Fraud Prevention

  1. Rate limiting: Max 10 validation requests per IP per minute
  2. Duplicate detection: Same transaction ID flag
  3. Environment check: Sandbox transaction production'da reject
  4. Geographic anomaly: User Türkiye'de, transaction US'den — flag
  5. Device binding: Transaction + user agent + device ID combo
  6. Webhook signature: Apple webhook JWS + Google Pub/Sub auth

Duplicate Transaction Table

sql
1CREATE TABLE validated_transactions (
2 transaction_id VARCHAR(256) PRIMARY KEY,
3 user_id UUID,
4 platform VARCHAR(10),
5 validated_at TIMESTAMP DEFAULT NOW(),
6 subscription_expires_at TIMESTAMP
7);
8 
9-- On validation:
10INSERT INTO validated_transactions (...)
11ON CONFLICT (transaction_id) DO NOTHING;
12 
13-- If count(inserted_rows) == 0 → duplicate, reject

Webhook Handling

Apple + Google webhook'larını dinle:

typescript
1app.post('/apple/webhook', async (req, res) => {
2 const { signedPayload } = req.body;
3 const notification = await verifyJWS(signedPayload);
4 
5 switch (notification.notificationType) {
6 case 'SUBSCRIBED':
7 await grantProAccess(notification.data.transactionId);
8 break;
9 case 'DID_RENEW':
10 await extendProAccess(notification.data.transactionId);
11 break;
12 case 'EXPIRED':
13 case 'REFUND':
14 await revokeProAccess(notification.data.transactionId);
15 break;
16 }
17 
18 res.status(200).send('OK');
19});

Google Play Pub/Sub similar ama Pub/Sub message wrapped.


Testing

iOS Sandbox

  1. Sandbox Apple ID oluştur (App Store Connect → Sandbox Testers)
  2. Settings → Apple ID → Sandbox Account olarak giriş
  3. App'te purchase → sandbox environment'a gider
  4. Receipt'te environment: "Sandbox" field

Production'a deploy edince bu field'ı check et — "Sandbox" → reject validation.

Android Testing

  1. Play Console → License testers → Gmail ekle
  2. Signed APK install
  3. Real transaction olur ama 5-dakikada refund
  4. Subscription period 5 dakika (yearly 5 dakika)

Monitoring + Alerts

Metrics:

  • Validation success rate (>%99)
  • Validation latency p99 (<500ms)
  • Webhook delivery success
  • Failed validations (flag fraud)

Alert:

  • Validation latency > 2s
  • Webhook delivery < %95
  • Fraud flag spike (10x normal)
  • Expired/revoked transactions discrepancy

Use: DataDog, Sentry, CloudWatch — business-critical monitoring.


ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

typescript
1// receipt-validator.ts — Cross-platform helper
2export class ReceiptValidator {
3 private appleCA: string;
4 private googleAuth: GoogleAuth;
5 
6 constructor(config: ValidatorConfig) {
7 this.appleCA = config.appleCA;
8 this.googleAuth = config.googleAuth;
9 }
10 
11 async validate(params: ValidateParams): Promise {
12 const { platform, token, transactionId } = params;
13 
14 // Duplicate check
15 if (await this.isDuplicate(transactionId)) {
16 return { valid: false, reason: 'duplicate' };
17 }
18 
19 // Environment check
20 const env = this.detectEnvironment(token);
21 if (env === 'sandbox' && process.env.NODE_ENV === 'production') {
22 return { valid: false, reason: 'sandbox_in_production' };
23 }
24 
25 // Platform-specific validation
26 try {
27 const status = platform === 'ios'
28 ? await this.validateiOS(token)
29 : await this.validateAndroid(token, params.productId);
30 
31 await this.recordValidation(transactionId, status);
32 return { valid: true, status };
33 } catch (error) {
34 return { valid: false, reason: error.message };
35 }
36 }
37}
38 
39// Usage
40const validator = new ReceiptValidator({ appleCA, googleAuth });
41const result = await validator.validate({
42 platform: 'ios',
43 token: jwsToken,
44 transactionId: txnId,
45 productId: 'pro_monthly'
46});

Okuyucu Ödülü

Production'da bu pattern'i bir npm package olarak wrap et. **External Resources:** - [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi) - [JWS verification guide](https://developer.apple.com/documentation/appstoreserverapi/jwstransaction) - [Google Play Developer API](https://developers.google.com/android-publisher) - [Apple Root CA](https://www.apple.com/certificateauthority/) - [Server notifications](https://developer.apple.com/documentation/appstoreservernotifications)

Sonuç

Server-side receipt validation 2026'da production IAP için non-optional. JWS verification (iOS) + Google Play API (Android) ile cross-platform. Unified abstraction + fraud prevention + webhook handling + monitoring = production-ready. Self-build 1-2 hafta iş, RevenueCat alternative'i 2 gün. Fraud prevention + regulatory compliance için her app'te zorunlu.

*İlgili yazılar: StoreKit 2, Play Billing v7, RevenueCat.*

Etiketler

#Backend#Security#IAP#Receipt Validation#StoreKit#Play Billing#JWS
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz