Unverified Commit 79c08f78 authored by Simon Esposito's avatar Simon Esposito Committed by GitHub
Browse files

Add persist field to iap validation APIs (#788)

Allow to skip persisting the IAP receipts in the db and only validate the receipt with the corresponding providers.
Resolves #784
parent 0f3e93c6
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ require (
	github.com/gorilla/mux v1.8.0
	github.com/gorilla/websocket v1.4.2
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0
	github.com/heroiclabs/nakama-common v1.21.1-0.20220225112712-3adfceaba805
	github.com/heroiclabs/nakama-common v1.21.1-0.20220302145851-74490eeebdda
	github.com/jackc/pgconn v1.10.0
	github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451
	github.com/jackc/pgtype v1.8.1
+2 −6
Original line number Diff line number Diff line
@@ -130,8 +130,6 @@ github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fp
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06 h1:XqC5eocqw7r3+HOhKYqaYH07XBiBDp9WE3NQK8XHSn4=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20220124171016-cfb079cdc7b4 h1:gUXabLfCUjaNl7kLxGdaZaw1c5x33SGL9PEo6p/hfuo=
github.com/dop251/goja v0.0.0-20220124171016-cfb079cdc7b4/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
@@ -257,10 +255,8 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/heroiclabs/nakama-common v1.21.1-0.20220117102903-bc0471af4fe1 h1:x/dBIoeLjzb8PnCbrOsVEda8AJy+kmWj/zRTVkO9mc8=
github.com/heroiclabs/nakama-common v1.21.1-0.20220117102903-bc0471af4fe1/go.mod h1:WF4YG46afwY3ibzsXnkt3zvhQ3tBY03IYeU7xSLr8HE=
github.com/heroiclabs/nakama-common v1.21.1-0.20220225112712-3adfceaba805 h1:GxmEr5fxdNEamOJmCPeUYdJvMQsJJJNZnBnkBmJxG9M=
github.com/heroiclabs/nakama-common v1.21.1-0.20220225112712-3adfceaba805/go.mod h1:WF4YG46afwY3ibzsXnkt3zvhQ3tBY03IYeU7xSLr8HE=
github.com/heroiclabs/nakama-common v1.21.1-0.20220302145851-74490eeebdda h1:3lN3FQw2Ud3QAznRfz0t6JE40aubnqYRtUTEu2wHh8g=
github.com/heroiclabs/nakama-common v1.21.1-0.20220302145851-74490eeebdda/go.mod h1:WF4YG46afwY3ibzsXnkt3zvhQ3tBY03IYeU7xSLr8HE=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+18 −3
Original line number Diff line number Diff line
@@ -58,7 +58,12 @@ func (s *ApiServer) ValidatePurchaseApple(ctx context.Context, in *api.ValidateP
		return nil, status.Error(codes.InvalidArgument, "Receipt cannot be empty.")
	}

	validation, err := ValidatePurchasesApple(ctx, s.logger, s.db, userID, s.config.GetIAP().Apple.SharedPassword, in.Receipt)
	persist := true
	if in.Persist != nil {
		persist = in.Persist.GetValue()
	}

	validation, err := ValidatePurchasesApple(ctx, s.logger, s.db, userID, s.config.GetIAP().Apple.SharedPassword, in.Receipt, persist)
	if err != nil {
		return nil, err
	}
@@ -110,7 +115,12 @@ func (s *ApiServer) ValidatePurchaseGoogle(ctx context.Context, in *api.Validate
		return nil, status.Error(codes.InvalidArgument, "Purchase cannot be empty.")
	}

	validation, err := ValidatePurchaseGoogle(ctx, s.logger, s.db, userID, s.config.GetIAP().Google, in.Purchase)
	persist := true
	if in.Persist != nil {
		persist = in.Persist.GetValue()
	}

	validation, err := ValidatePurchaseGoogle(ctx, s.logger, s.db, userID, s.config.GetIAP().Google, in.Purchase, persist)
	if err != nil {
		return nil, err
	}
@@ -168,7 +178,12 @@ func (s *ApiServer) ValidatePurchaseHuawei(ctx context.Context, in *api.Validate
		return nil, status.Error(codes.InvalidArgument, "Signature cannot be empty.")
	}

	validation, err := ValidatePurchaseHuawei(ctx, s.logger, s.db, userID, s.config.GetIAP().Huawei, in.Purchase, in.Signature)
	persist := true
	if in.Persist != nil {
		persist = in.Persist.GetValue()
	}

	validation, err := ValidatePurchaseHuawei(ctx, s.logger, s.db, userID, s.config.GetIAP().Huawei, in.Purchase, in.Signature, persist)
	if err != nil {
		return nil, err
	}
+75 −28
Original line number Diff line number Diff line
@@ -41,7 +41,7 @@ var ErrPurchasesListInvalidCursor = errors.New("purchases list cursor invalid")

var httpc = &http.Client{Timeout: 5 * time.Second}

func ValidatePurchasesApple(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, password, receipt string) (*api.ValidatePurchaseResponse, error) {
func ValidatePurchasesApple(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, password, receipt string, persist bool) (*api.ValidatePurchaseResponse, error) {
	validation, raw, err := iap.ValidateReceiptApple(ctx, httpc, receipt, password)
	if err != nil {
		var vErr *iap.ValidationError
@@ -86,6 +86,23 @@ func ValidatePurchasesApple(ctx context.Context, logger *zap.Logger, db *sql.DB,
		})
	}

	if !persist {
		// Skip storing the receipts
		validatedPurchases := make([]*api.ValidatedPurchase, 0, len(storagePurchases))
		for _, p := range storagePurchases {
			validatedPurchases = append(validatedPurchases, &api.ValidatedPurchase{
				ProductId:        p.productId,
				TransactionId:    p.transactionId,
				Store:            p.store,
				PurchaseTime:     &timestamppb.Timestamp{Seconds: p.purchaseTime.Unix()},
				ProviderResponse: string(raw),
				Environment:      p.environment,
			})
		}

		return &api.ValidatePurchaseResponse{ValidatedPurchases: validatedPurchases}, nil
	}

	purchases, err := storePurchases(ctx, db, storagePurchases)
	if err != nil {
		return nil, err
@@ -101,8 +118,8 @@ func ValidatePurchasesApple(ctx context.Context, logger *zap.Logger, db *sql.DB,
			CreateTime:       &timestamppb.Timestamp{Seconds: p.createTime.Unix()},
			UpdateTime:       &timestamppb.Timestamp{Seconds: p.updateTime.Unix()},
			ProviderResponse: string(raw),
			Environment:      p.environment,
			SeenBefore:       p.seenBefore,
			Environment:      p.environment,
		})
	}

@@ -111,7 +128,7 @@ func ValidatePurchasesApple(ctx context.Context, logger *zap.Logger, db *sql.DB,
	}, nil
}

func ValidatePurchaseGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, config *IAPGoogleConfig, receipt string) (*api.ValidatePurchaseResponse, error) {
func ValidatePurchaseGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, config *IAPGoogleConfig, receipt string, persist bool) (*api.ValidatePurchaseResponse, error) {
	gResponse, gReceipt, raw, err := iap.ValidateReceiptGoogle(ctx, httpc, config.ClientEmail, config.PrivateKey, receipt)
	if err != nil {
		var vErr *iap.ValidationError
@@ -132,8 +149,7 @@ func ValidatePurchaseGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB,
		purchaseEnv = api.ValidatedPurchase_SANDBOX
	}

	purchases, err := storePurchases(ctx, db, []*storagePurchase{
		{
	sPurchase := &storagePurchase{
		userID:        userID,
		store:         api.ValidatedPurchase_GOOGLE_PLAY_STORE,
		productId:     gReceipt.ProductID,
@@ -141,8 +157,24 @@ func ValidatePurchaseGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB,
		rawResponse:   string(raw),
		purchaseTime:  parseMillisecondUnixTimestamp(int(gReceipt.PurchaseTime)),
		environment:   purchaseEnv,
	}

	if !persist {
		validatedPurchases := []*api.ValidatedPurchase{
			{
				ProductId:        sPurchase.productId,
				TransactionId:    sPurchase.transactionId,
				Store:            sPurchase.store,
				PurchaseTime:     &timestamppb.Timestamp{Seconds: sPurchase.purchaseTime.Unix()},
				ProviderResponse: string(raw),
				Environment:      sPurchase.environment,
			},
	})
		}

		return &api.ValidatePurchaseResponse{ValidatedPurchases: validatedPurchases}, nil
	}

	purchases, err := storePurchases(ctx, db, []*storagePurchase{sPurchase})
	if err != nil {
		if err != context.Canceled {
			logger.Error("Error storing Google receipt", zap.Error(err))
@@ -160,8 +192,8 @@ func ValidatePurchaseGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB,
			CreateTime:       &timestamppb.Timestamp{Seconds: p.createTime.Unix()},
			UpdateTime:       &timestamppb.Timestamp{Seconds: p.updateTime.Unix()},
			ProviderResponse: string(raw),
			Environment:      p.environment,
			SeenBefore:       p.seenBefore,
			Environment:      p.environment,
		})
	}

@@ -170,7 +202,7 @@ func ValidatePurchaseGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB,
	}, nil
}

func ValidatePurchaseHuawei(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, config *IAPHuaweiConfig, inAppPurchaseData, signature string) (*api.ValidatePurchaseResponse, error) {
func ValidatePurchaseHuawei(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, config *IAPHuaweiConfig, inAppPurchaseData, signature string, persist bool) (*api.ValidatePurchaseResponse, error) {
	validation, data, raw, err := iap.ValidateReceiptHuawei(ctx, httpc, config.PublicKey, config.ClientID, config.ClientSecret, inAppPurchaseData, signature)
	if err != nil {
		var vErr *iap.ValidationError
@@ -194,8 +226,7 @@ func ValidatePurchaseHuawei(ctx context.Context, logger *zap.Logger, db *sql.DB,
		env = api.ValidatedPurchase_SANDBOX
	}

	purchases, err := storePurchases(ctx, db, []*storagePurchase{
		{
	sPurchase := &storagePurchase{
		userID:        userID,
		store:         api.ValidatedPurchase_HUAWEI_APP_GALLERY,
		productId:     validation.PurchaseTokenData.ProductId,
@@ -203,8 +234,24 @@ func ValidatePurchaseHuawei(ctx context.Context, logger *zap.Logger, db *sql.DB,
		rawResponse:   string(raw),
		purchaseTime:  parseMillisecondUnixTimestamp(int(data.PurchaseTime)),
		environment:   env,
	}

	if !persist {
		validatedPurchases := []*api.ValidatedPurchase{
			{
				ProductId:        sPurchase.productId,
				TransactionId:    sPurchase.transactionId,
				Store:            sPurchase.store,
				PurchaseTime:     &timestamppb.Timestamp{Seconds: sPurchase.purchaseTime.Unix()},
				ProviderResponse: string(raw),
				Environment:      sPurchase.environment,
			},
	})
		}

		return &api.ValidatePurchaseResponse{ValidatedPurchases: validatedPurchases}, nil
	}

	purchases, err := storePurchases(ctx, db, []*storagePurchase{sPurchase})
	if err != nil {
		if err != context.Canceled {
			logger.Error("Error storing Huawei receipt", zap.Error(err))
@@ -222,8 +269,8 @@ func ValidatePurchaseHuawei(ctx context.Context, logger *zap.Logger, db *sql.DB,
			CreateTime:       &timestamppb.Timestamp{Seconds: p.createTime.Unix()},
			UpdateTime:       &timestamppb.Timestamp{Seconds: p.updateTime.Unix()},
			ProviderResponse: string(raw),
			Environment:      p.environment,
			SeenBefore:       p.seenBefore,
			Environment:      p.environment,
		})
	}

+9 −6
Original line number Diff line number Diff line
@@ -2674,10 +2674,11 @@ func (n *RuntimeGoNakamaModule) TournamentRecordsHaystack(ctx context.Context, i
// @param ctx(type=context.Context) The context object represents information about the server and requester.
// @param userId(type=string) The user ID of the owner of the receipt.
// @param receipt(type=string) Base-64 encoded receipt data returned by the purchase operation itself.
// @param persist(type=bool) Persist the purchase so that seenBefore can be computed to protect against replay attacks.
// @param passwordOverride(type=string, optional=true) Override the iap.apple.shared_password provided in your configuration.
// @return validation(*api.ValidatePurchaseResponse) The resulting successfully validated purchases. Any previously validated purchases are returned with a seenBefore flag.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) PurchaseValidateApple(ctx context.Context, userID, receipt string, passwordOverride ...string) (*api.ValidatePurchaseResponse, error) {
func (n *RuntimeGoNakamaModule) PurchaseValidateApple(ctx context.Context, userID, receipt string, persist bool, passwordOverride ...string) (*api.ValidatePurchaseResponse, error) {
	if n.config.GetIAP().Apple.SharedPassword == "" && len(passwordOverride) == 0 {
		return nil, errors.New("Apple IAP is not configured.")
	}
@@ -2697,7 +2698,7 @@ func (n *RuntimeGoNakamaModule) PurchaseValidateApple(ctx context.Context, userI
		return nil, errors.New("receipt cannot be empty string")
	}

	validation, err := ValidatePurchasesApple(ctx, n.logger, n.db, uid, password, receipt)
	validation, err := ValidatePurchasesApple(ctx, n.logger, n.db, uid, password, receipt, persist)
	if err != nil {
		return nil, err
	}
@@ -2710,9 +2711,10 @@ func (n *RuntimeGoNakamaModule) PurchaseValidateApple(ctx context.Context, userI
// @param ctx(type=context.Context) The context object represents information about the server and requester.
// @param userId(type=string) The user ID of the owner of the receipt.
// @param receipt(type=string) JSON encoded Google receipt.
// @param persist(type=bool) Persist the purchase so that seenBefore can be computed to protect against replay attacks.
// @return validation(*api.ValidatePurchaseResponse) The resulting successfully validated purchases. Any previously validated purchases are returned with a seenBefore flag.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) PurchaseValidateGoogle(ctx context.Context, userID, receipt string) (*api.ValidatePurchaseResponse, error) {
func (n *RuntimeGoNakamaModule) PurchaseValidateGoogle(ctx context.Context, userID, receipt string, persist bool) (*api.ValidatePurchaseResponse, error) {
	if n.config.GetIAP().Google.ClientEmail == "" || n.config.GetIAP().Google.PrivateKey == "" {
		return nil, errors.New("Google IAP is not configured.")
	}
@@ -2726,7 +2728,7 @@ func (n *RuntimeGoNakamaModule) PurchaseValidateGoogle(ctx context.Context, user
		return nil, errors.New("receipt cannot be empty string")
	}

	validation, err := ValidatePurchaseGoogle(ctx, n.logger, n.db, uid, n.config.GetIAP().Google, receipt)
	validation, err := ValidatePurchaseGoogle(ctx, n.logger, n.db, uid, n.config.GetIAP().Google, receipt, persist)
	if err != nil {
		return nil, err
	}
@@ -2740,9 +2742,10 @@ func (n *RuntimeGoNakamaModule) PurchaseValidateGoogle(ctx context.Context, user
// @param userId(type=string) The user ID of the owner of the receipt.
// @param receipt(type=string) The Huawei receipt data.
// @param signature(type=string) The receipt signature.
// @param persist(type=bool) Persist the purchase so that seenBefore can be computed to protect against replay attacks.
// @return validation(*api.ValidatePurchaseResponse) The resulting successfully validated purchases. Any previously validated purchases are returned with a seenBefore flag.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) PurchaseValidateHuawei(ctx context.Context, userID, signature, inAppPurchaseData string) (*api.ValidatePurchaseResponse, error) {
func (n *RuntimeGoNakamaModule) PurchaseValidateHuawei(ctx context.Context, userID, signature, inAppPurchaseData string, persist bool) (*api.ValidatePurchaseResponse, error) {
	if n.config.GetIAP().Huawei.ClientID == "" ||
		n.config.GetIAP().Huawei.ClientSecret == "" ||
		n.config.GetIAP().Huawei.PublicKey == "" {
@@ -2762,7 +2765,7 @@ func (n *RuntimeGoNakamaModule) PurchaseValidateHuawei(ctx context.Context, user
		return nil, errors.New("inAppPurchaseData cannot be empty string")
	}

	validation, err := ValidatePurchaseHuawei(ctx, n.logger, n.db, uid, n.config.GetIAP().Huawei, inAppPurchaseData, signature)
	validation, err := ValidatePurchaseHuawei(ctx, n.logger, n.db, uid, n.config.GetIAP().Huawei, inAppPurchaseData, signature, persist)
	if err != nil {
		return nil, err
	}
Loading