libgdx / gdx-pay

A libGDX cross-platform API for InApp purchasing.
Apache License 2.0
224 stars 83 forks source link

Receipt validation #141

Closed MrCharli3 closed 7 years ago

MrCharli3 commented 7 years ago

I want to validate purchases made in my backend, and save the value there. Is there a way to do this using Gdx-pay?

Tom-Ski commented 7 years ago

Not really related to gdx-pay specifically, but weighing in here on the topic. You can locally verify and/or submit the receipt to the app store verification end point.

If you are just interested in locally verifying, I suggest you write a binding that wraps a unique verification strategy for you app. Having this strategy as part of an open source library makes it very easy to bypass verification on any app that uses the library. The reason I suggest a native binding is the ease of accessing openssl, which makes your life a lot easier when verifying the payloads and containers.

You can also verify using the app store end point, by sending off the receipt to the app store api, and parsing the result. This should only really be done on a server you are running, as you can't guarantee the connection between the app>appstore is trusted. Although its possible and trivial to do it from the app, its not recommended you do so, but its up to you what level of protection you are willing to go to. Everything is crackable.

MrCharli3 commented 7 years ago

@Tom-Ski Thanks :) Is there a way to get the receipt-object using gdx-pay? So that I can use it. Or do I need to look into platform specific stuff?

Tom-Ski commented 7 years ago

I'm not familiar with this project, but you can see the robovm gdx-pay backend accessing the receipt here: https://github.com/libgdx/gdx-pay/blob/master/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java#L369 where validation isn't implemented yet, as it states in the comments.

You can grab the receipt outside of gdx-pay by doing the same, just as these two lines show https://github.com/libgdx/gdx-pay/blob/master/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java#L380-L381

As the receipt is ios only, I doubt there is a utility method to obtain it in a cross-platform friendly way, but I would advise you write your own platform specific code for this stuff anyway.

noblemaster commented 7 years ago

There is a sample for the purchase verification in gdx-pay: https://github.com/libgdx/gdx-pay/blob/master/gdx-pay-server/src/com/badlogic/gdx/pay/server/impl/PurchaseVerifieriOSApple.java

It needs some cleanup & testing. However, that's the code that should go onto your server for purchase verification.

MrCharli3 commented 7 years ago

@noblemaster thanks man! Is there something similar for Android that you know of?

noblemaster commented 7 years ago

Yes, it's described here: http://stackoverflow.com/questions/11115381/unable-to-get-the-subscription-information-from-google-play-android-developer-ap

Here is some code that I wrote. I should have integrated it into gdx-pay, just simply haven't found the time. Do you have time to adapt it to gdx-pay & submit a pull request?

/**
 * Server-side purchase verification for Google Play.
 * <p>
 * Required JARs:
 * <ul>
 *   <li>google-api-services-androidpublisher-v2-rev20141111-1.19.0.jar
 *   <li>google-api-client-1.19.0.jar
 *   <li>google-oauth-client-1.19.0.jar
 *   <li>google-http-client-1.19.0.jar
 *   <li>jsr305-1.3.9.jar
 *   <li>google-http-client-jackson2-1.19.0.jar (when using Jackson 2)
 *   <li>jackson-core-$2.1.3.jar
 * </ul>
 * Information:
 * </ul>
 *   <li>Our credentials are here: https://console.developers.google.com/apis/credentials
 *   <li>We use project "Google Play Android Developer" (stay the same for multiple projects)
 * </ul>
 * To obtain OAuth, refer to the following pages:
 * <ul>
 *   <li>https://developers.google.com/android-publisher/authorization
 *   <li>http://stackoverflow.com/questions/11115381/unable-to-get-the-subscription-information-from-google-play-android-developer-ap
 * </ul>
 *
 * @author noblemaster
 */
public class CashierVerifyGoogle implements Cashier.Verify {

  private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
  private static final JsonFactory JSON_FACTORY = new com.google.api.client.json.jackson2.JacksonFactory();

  /** The application name. Suggested format is "MyCompany-Application/1.0". */
  private String productName;
  /** The application package, e.g. "com.ageofconquest.app.user.aoc". */
  private String productPack;

  /** Client ID from Google Developer Console. */
  private String clientID;
  /** Client Secret from Google Developer Console. */
  private String clientSecret;
  /** The refresh-token: generate via our "OAuth20Printer.java"!. */
  private String refreshToken;

  public CashierVerifyGoogle(String productName, String productPack, String clientID, String clientSecret, String refreshToken) {
    this.productName = productName;
    this.productPack = productPack;

    // store connection-information
    this.clientID = clientID;
    this.clientSecret = clientSecret;
    this.refreshToken = refreshToken;
  }

  @Override
  public Market exchange() {
    return Market.GOOGLE;
  }

  @Override
  public void valid(Purchase purchase, String location) throws Failure {
    try {
      // obtain access-token (only temporary, so we need to refresh every time!)
      String accessToken = OAuth20.getAccessToken(OAuth20.PATH_GOOGLE, clientID, clientSecret, refreshToken);

      // purchase parameters
      String productId = purchase.getResourceExchange();
      String purchaseToken = purchase.getPurchaseDataSignature();

      // use AndroidPublisher to access our purchase-data!
      TokenResponse tokenResponse = new TokenResponse();
      tokenResponse.setAccessToken(accessToken);
      tokenResponse.setRefreshToken(refreshToken);
      tokenResponse.setExpiresInSeconds(3600L);
      tokenResponse.setScope("https://www.googleapis.com/auth/androidpublisher");
      tokenResponse.setTokenType("Bearer");

      HttpRequestInitializer credential =  new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
                                                                         .setJsonFactory(JSON_FACTORY)
                                                                         .setClientSecrets(clientID, clientSecret)
                                                                         .build()
                                                                         .setFromTokenResponse(tokenResponse);
      AndroidPublisher publisher = new AndroidPublisher.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
                                                       .setApplicationName(productName)
                                                       .build();

      // we use the publisher to obtain the purchase data (if valid, we should receive it!)
      AndroidPublisher.Purchases purchases = publisher.purchases();
      AndroidPublisher.Purchases.Products.Get purchasesGet = purchases.products().get(productPack, productId, purchaseToken);
      ProductPurchase productPurchase = purchasesGet.execute();

      // verify purchase
      if (productPurchase != null) {
        // log debug-info as needed!
        Log.d("GooglePlay purchase information successfully located/verified for purchase " + purchase + ".");
      }
      else {
        // error: not purchase found!
        throw new Failure("No purchase information located (i.e. 'null') originating from \"" + location + "\".");
      }
    }
    catch (Exception e) {
      // output & throw error
      boolean minimal = true;  // <-- mostly hackers: we just log the purchase info & IP and consider it done.
      if (minimal) {
        // no stacktrace
        Log.i("CashierVerifyGoogle: Error verifying purchase originating from \"" + location + "\" (" + purchase + ", purchaseText = \"" + purchase.getPurchaseText() + "\").");
      }
      else {
        // with stacktrace...
        Log.e("CashierVerifyGoogle: Error verifying purchase originating from \"" + location + "\" (" + purchase + ", purchaseText = \"" + purchase.getPurchaseText() + "\").", e);
      }
      throw Failure.e(e);
    }
  }
}
MrCharli3 commented 7 years ago

@noblemaster I'll look at it and see if I can get it to work. I'm not sure how to add it to Gdx-Pay, but would be great if it was in there for sure.

MrCharli3 commented 7 years ago

@noblemaster Might be a silly question, but the code you sent for iOS (i.e the code from Gdx-pay goes into my client yes? But the AndroidGooglePlay-code has to go in my server?

Or do I need to import gdx-pay to my server and use the iOS version there too? Unsure if what you sent for Gdx-pay is a sample I need to reproduce, or something I can use through gdx-pay.

Can't find a way to get "Transaction" using gdx-pay. To build a Transaction I need a receipt, not sure how to get to that using gdx-pay. Hope I'm making sense. These are some very silly questions I'm aware. Just having a hard time wrapping my head around it.

noblemaster commented 7 years ago

Both samples go onto the server! The Transaction object contains all the data needed for verification.

MrCharli3 commented 7 years ago

@noblemaster Thanks, then one last question. I need to send the Transaction-object from my client right? But how do I get/create that object (or the receipt)?

noblemaster commented 7 years ago

You could send the Transaction object as JSON data to the server for example. It all depends how you implemented your server. Then simply re-create the Transaction object from the JSON data on the server.

The relevant parts for verification are the orderID and transactionDataSignature from the Transaction object.

MrCharli3 commented 7 years ago

@noblemaster Thanks, but I meant how do I get the Transaction object in my client? so that I can send it to my server :) I can't find any gdx-pay function to do this, only "getInformation(identifier)", but that returns info aobut a product from what I can tell.

noblemaster commented 7 years ago

You receive it via PurchaseObserver.