Mandatory security rules for developing payment and in-app purchase (IAP) endpoints.
Authentication Required#
All payment-related endpoints must set requireLogin => true.
// â
CORRECT
class IapPurchaseEndpoint extends Endpoint {
@override
bool get requireLogin => true;
Future<void> verifyPurchase(Session session, ...) async { ... }
}
// â WRONG: Processing payments without authentication
class IapPurchaseEndpoint extends Endpoint {
// requireLogin not set = anyone can call!
Future<void> verifyPurchase(Session session, ...) async { ... }
}
Server-Side User ID Verification#
Never trust the userId sent by the client. Always verify server-side with getCurrentUserInfo(session).
// â
CORRECT: Server-side user verification
Future<void> verifyPurchase(
Session session,
IapPurchaseRequest request,
) async {
final userInfo = await getCurrentUserInfo(session);
if (userInfo == null) throw AuthenticationException();
// Use userInfo.id (ignore client userId)
await _processPayment(session, userInfo.id!, request);
}
// â WRONG: Trusting client userId
Future<void> verifyPurchase(
Session session,
IapPurchaseRequest request,
) async {
// Using request.userId directly â allows payment manipulation for other users!
await _processPayment(session, request.userId, request);
}
Reference: See serverpod-auth.md for getCurrentUserInfo()
usage
Duplicate Payment Prevention#
Use equals() (No LIKE Queries)#
Use equals() for DB lookups. like() carries SQL injection risk and potential false positives from wildcard matching.
// â
CORRECT: Exact matching
final existing = await WalletTransaction.db.findFirstRow(
session,
where: (table) => table.description.equals(purchaseToken),
);
// â WRONG: LIKE query (SQL injection and false positive risk)
final existing = await WalletTransaction.db.findFirstRow(
session,
where: (table) => table.description.like('%$purchaseToken%'),
);
Dedicated Column Recommended#
Using a dedicated UNIQUE column for duplicate checks is the safest approach.
// â
BEST: Dedicated purchaseId UNIQUE column
final existing = await WalletTransaction.db.findFirstRow(
session,
where: (table) => table.purchaseId.equals(purchaseToken),
);
Unimplemented Feature Handling#
When platform-specific verification is not yet implemented, return a clear error instead of hard-coded tokens/responses.
// â
CORRECT: Return a clear error
Future<bool> verifyIosPurchase(String receiptData) async {
throw UnimplementedError(
'iOS App Store receipt verification not implemented. '
'App Store Server API JWT token integration required.',
);
}
// â WRONG: Hard-coded success response
Future<bool> verifyIosPurchase(String receiptData) async {
return true; // Always succeeds without verification!
}
// â WRONG: Hard-coded token
const _accessToken = 'ya29.hardcoded-token-here';
TossPayments Web Payment Security#
Security rules for when users are redirected from the iOS app to a web browser for direct payment via TossPayments, per Apple External Link policy.
Web Auth Token (WebAuthToken)#
Web payment pages authenticate using a temporary token generated from the app.
// Token characteristics
// - 10-minute validity (Duration(minutes: 10))
// - Single use (tracked via used flag)
// - UUID v4 format (Random.secure() based)
// - DB UNIQUE index prevents duplicates
| Step | Token Handling | Method |
|---|---|---|
| Book detail page | Read-only validation (not consumed) | validateTokenReadOnly() |
| Payment success callback | Validate + consume (used=true) | validateAndConsumeToken() |
Triple Amount Verification#
1. Client amount vs server-calculated amount â reject on mismatch
2. TossPayments API payment approval request
3. TossPayments response totalAmount vs server-calculated amount â reject on mismatch
userId Included in orderId#
// orderId format: WEBBOOK_{bookId}_{userId}_{timestamp}_{orderType}
// Cross-user attack prevention: verify userId extracted from orderId matches token userId
Store tossPaymentKey#
After payment approval, store tossPaymentKey in BookOrder for refund/audit tracking.
Platform-Specific Verification Strategy#
| Platform | Payment Method | Verification Method | Status |
|---|---|---|---|
| Android | TossPayments Flutter Widget SDK (in-app) | TossPayments API payment approval + amount cross-verification | ð§ Planned (Epic #2619) |
| iOS | Apple External Link â web browser payment | TossPayments API payment approval + amount cross-verification | â Implemented |
| Web | dart:js_interop + TossPayments JS SDK v2 |
TossPayments API payment approval + amount cross-verification | â Implemented |
| Google Play IAP | Google Play Billing | Google Play Developer API (purchases.products.get) |
â Implemented |
TossPayments Flutter Widget SDK Limitations#
tosspayments_widget_sdk_flutter v2.1.2 supports Android/iOS only (no Flutter Web support).
iOS cannot use the in-app payment widget due to Apple External Link policy.
Therefore, only Android uses the Widget SDK, while iOS/Web maintains the existing web payment approach as a hybrid strategy.
Android: Flutter Widget SDK (PaymentMethodWidget + AgreementWidget) â TossPayments API approval
iOS: Apple External Link â web browser â TossPayments JS SDK â server callback
Web: dart:js_interop â TossPayments JS SDK v2 â server callback
Google Play Verification Flow#
Client â purchaseToken â Server â Google API verification â credit top-up
iOS App Store Verification Flow (TODO)#
Client â receiptData â Server â App Store API (JWT) â credit top-up
Checklist#
When writing payment endpoints:
- Set
requireLogin => true - Verify user server-side with
getCurrentUserInfo(session) - Ignore client-sent userId
- Duplicate payment check: use
equals()(no LIKE) - Recommend dedicated purchaseId column
-
Unimplemented features: return
UnimplementedError(no hard-coding) - Amount/product verification: re-confirm price server-side
- Web payments: single-use WebAuthToken consumption
- Web payments: store tossPaymentKey (for refund/audit tracking)
Related Files#
backend/kobic_server/lib/src/feature/payments/endpoint/iap_purchase_endpoint.dartbackend/kobic_server/lib/src/feature/payments/service/iap_purchase_service.dartbackend/kobic_server/lib/src/feature/payments/helper/iap_verification_helper.dartbackend/kobic_server/lib/src/feature/payments/endpoint/web_payment_endpoint.dartbackend/kobic_server/lib/src/feature/payments/service/web_book_payment_service.dartbackend/kobic_server/lib/src/web/routes/store_book_route.dartbackend/kobic_server/lib/src/web/routes/store_payment_success_route.dartbackend/kobic_server/lib/src/web/routes/store_payment_fail_route.dart- serverpod-auth.md -
getCurrentUserInfo()usage