# 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
- iOS StoreKit 2: JWS Verification
- Android Play Billing: Google Play API
- Unified Abstraction
- Fraud Prevention
- Webhook Handling
- Testing
- Monitoring + Alerts
Neden Server-Side Validation
Client-Only Risks
- Jailbreak/root bypass: Hex edit ile "pro:true" inject
- Receipt replay: Aynı receipt farklı device'larda
- Dev environment contamination: Sandbox receipt production'a geçiş
- 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 decode10 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 CA19 await verifyCertificateChain(certChain, APPLE_ROOT_CA_G3);20 21 // 4. Extract public key from leaf cert22 const leafCert = certChain[0];23 const publicKey = leafCert.publicKey;24 25 // 5. Verify signature26 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: string5): Promise { 6 const token = generateAppStoreServerAPIToken(); // JWT with private key7 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_ID14 },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: string7): 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: purchaseToken21 });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 yet31 autoRenewing: lineItem.autoRenewingPlan?.autoRenewEnabled ?? false32 };33}Service Account Setup
- Google Cloud Console → IAM → Create Service Account
- Android Publisher API enable
- Play Console → Users & Permissions → grant access
- 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: string14 ): 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.platform28 });29 }30 }31}32 33// Usage34const validator = new PurchaseValidator();35const status = await validator.validate('ios', jwsToken);36await validator.grantEntitlement(userId, status);Fraud Prevention
- Rate limiting: Max 10 validation requests per IP per minute
- Duplicate detection: Same transaction ID flag
- Environment check: Sandbox transaction production'da reject
- Geographic anomaly: User Türkiye'de, transaction US'den — flag
- Device binding: Transaction + user agent + device ID combo
- 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 TIMESTAMP7);8 9-- On validation:10INSERT INTO validated_transactions (...)11ON CONFLICT (transaction_id) DO NOTHING;12 13-- If count(inserted_rows) == 0 → duplicate, rejectWebhook 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
- Sandbox Apple ID oluştur (App Store Connect → Sandbox Testers)
- Settings → Apple ID → Sandbox Account olarak giriş
- App'te purchase → sandbox environment'a gider
- Receipt'te
environment: "Sandbox"field
Production'a deploy edince bu field'ı check et — "Sandbox" → reject validation.
Android Testing
- Play Console → License testers → Gmail ekle
- Signed APK install
- Real transaction olur ama 5-dakikada refund
- 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 helper2export 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 check15 if (await this.isDuplicate(transactionId)) {16 return { valid: false, reason: 'duplicate' };17 }18 19 // Environment check20 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 validation26 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// Usage40const 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.*

