Am I getting the steps right for verifying a user's Android in-app subscription?

21,648

Solution 1

As it turns out, my steps were not correct. It took me weeks to figure this out and it doesn't seem to be documented anywhere else. You're welcome:

  1. Create a Web Application account in the Google APIs Console. Put any website as a "redirect URI"; it doesn't matter since you will not really be using it. You will get a client id and client secret when you create the account.

  2. In a browser on your computer go to https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher&response_type=code&access_type=offline&redirect_uri=[YOUR REDIRECT URI]&client_id=[YOUR CLIENT ID] and allow access when prompted.

  3. Look in the address bar. At the end of the URI you entered originally will be your refresh token. It looks like 1/.... You will need this "code" in the next step. The refresh token never expires.

  4. Convert this "code" to a "refresh token" by going to https://accounts.google.com/o/oauth2/token?client_id=[YOUR CLIENT ID]&client_secret=[YOUR CLIENT SECRET]&code=[CODE FROM PREVIOUS STEP]&grant_type=authorization_code&redirect_uri=[YOUR REDIRECT URI]. You can save the resulting value right in your program; it never expires unless explicitly revoked. (this step inserted by @BrianWhite -- see comments) Make sure you are using POST.(inserted by Gintas)

  5. In your code, send an HttpPost request to https://accounts.google.com/o/oauth2/token with the BasicNameValuePairs "grant_type","refresh_token", "client_id",[YOUR CLIENT ID], "client_secret",[YOUR CLIENT SECRET], "refresh_token",[YOUR REFRESH TOKEN]. For an example look here. You will need to do this in a separate thread, probably using AsyncTask. This will return a JSONObject.

  6. Get the access token from the returned JSONObject. For an example look here. You will need to get the string "access_token". The access token expires in 1 hour.

  7. In your code, send an HttpGet request to https://www.googleapis.com/androidpublisher/v1/applications/[YOUR APP'S PACKAGE NAME]/subscriptions/[THE ID OF YOUR PUBLISHED SUBSCRIPTION FROM YOUR ANDROID DEVELOPER CONSOLE]/purchases/[THE PURCHASE TOKEN THE USER RECEIVES UPON PURCHASING THE SUBSCRIPTION]?accesstoken="[THE ACCESS TOKEN FROM STEP 4]". For an example look here.

Solution 2

.NET Users: I hope this answer saves someone a ton of grief.

As @Christophe Fondacci noted on 2015, the accepted solution worked great a few years ago.

Now it's 2017 2020 and the process is far easier and faster.

My use case is to validate in-app subscriptions, where my mobile app sends subscription purchase information to my RESTful server, which in turn contacts Google to validate a subscription purchase.

The strategy is to create a Service Account that will operate on your behalf.

  1. Sign into your Google Play Dev Console and click the app you're setting up.

  2. Visit Settings->API access

  3. Under Service Accounts, hit the Create Service Account button.

  4. As of Jan 2017 a dialog with directions on setting up a service account appears. The dialog takes you to the Google API Console; from there,

    A) Click Create Service Account

    B) Create the service account name that makes sense. Since we're interested in accessing Android Publisher Services, I chose "publisher".

C) For Role, just choose something - you can change this later.

D) Choose "Furnish New private key" and choose P12 for .Net implementations. Don't lose this file!

  1. Now you're done with #4, you'll see your new Service Account listed; click "Grant Access" to enable it.

  2. Tap on the link to "View permissions". You should modify permissions based on your needs and API.

To validate in-app purchases, visit the Cog->Change Permissions and enable the GLOBAL "Visibility" and "Manage Orders" permissions.

OK at this point you have configured everything on Google's end. Now to setup your server to server stuff. I recommend creating a .Net Console App to test out your implementation then offload it where needed.

  1. Add the Android Publisher Client Library from Nuget[1]
    PM> Install-Package Google.Apis.AndroidPublisher.v3
  1. Add the P12 file to your project root

  2. Change the P12 Properties so "Build Action" is "Content" and "Copy To Output Directory" to "Copy if newer".

  3. Implement something like this to test your access and fine tune [1] .

    using System.Threading.Tasks;
    using System.Security.Cryptography.X509Certificates;
    using Google.Apis.Services;
    using Google.Apis.Auth.OAuth2;
    using Google.Apis.AndroidPublisher.v3;
    ...
    public Task<SubscriptionPurchase> GetSubscriptionPurchase(string packageName, string productId, string purchaseToken)
    {
        var certificate = new X509Certificate2(
                        "{{your p12 file name}}",
                        "{{ your p12 secret }}",
                        X509KeyStorageFlags.Exportable
                    );
        var credentials = new ServiceAccountCredential(
                new ServiceAccountCredential.Initializer("{{ your service account email }}")
                {
                    Scopes = new[] { AndroidPublisherService.Scope.Androidpublisher }
                }.FromCertificate(certificate));
    
        var service = new AndroidPublisherService(new BaseClientService.Initializer()
        {
            HttpClientInitializer = credentials,
            ApplicationName = "my server app name",
        });
        return service.Purchases.Subscriptions.Get(packageName, productId, purchaseToken).ExecuteAsync();
    }

Good luck, hope this helps someone.

Sources:

Using OAuth 2.0 for Server to Server Applications

.Net Client Library for Google.Apis.AndroidPublisher.v3[1]


Updated 04/11/2020 - Google.Apis.AndroidPublisher.v2 EOL'd, use Google.Apis.AndroidPublisher.v3.

Solution 3

If you are like me, and want to do this in PHP, here is the procedure how to do it... Thanks to Kalina's answer it took me only three days to work out how it works :).

Here goes:

  1. go to google developers console https://console.developers.google.com/ and create a web app. Put 'developers.google.com/oauthplayground'as a "redirect URI"; You will use it in step 2. You will get a client id and client secret when you create the account. Make sure you have the Google Play Android Developer API added.

  2. go to the Google oauth2 playground https://developers.google.com/oauthplayground/. This great tool is your best friend for the next few days. Now go to settings : make sure Use your own OAuth credentials is set. Only then you can fill in your client ID and client secret in the form below.

  3. In Google oauth2 playground go to step 1 Select & authorize APIs fill in the scope in the input field https://www.googleapis.com/auth/androidpublisher. I couldnt find the Google Play Android Developer API in the list, maybe they will add some time later. Hit AUTORIZE APIS. Do the authorisation thing that follows.

  4. In Google oauth2 playground go to step 2 Exchange authorization code for tokens. If all went well you will see a authorization code starting with /4. If something didnt go well check the error message on the right. Now you hit 'refresh access token'. Copy the Refresh token... it will start with /1...

  5. Now you can always get an access token! here is how:

    $url ="https://accounts.google.com/o/oauth2/token";
    $fields = array(
       "client_id"=>"{your client id}",
       "client_secret"=>"{your client secret}",
       "refresh_token"=>"{your refresh token 1/.....}",
       "grant_type"=>"refresh_token"
    );
    
    $ch = curl_init($url);
    
    //set the url, number of POST vars, POST data
    curl_setopt($ch, CURLOPT_POST,count($fields));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    //execute post
    $lResponse_json = curl_exec($ch);
    
    //close connection
    curl_close($ch);
    

Now you have an ACCESS TOKEN hooray... the JSON will look like this:

"access_token" : "{the access token}",  "token_type" : "Bearer",  "expires_in" : 3600

Finally you're ready to ask google something! Here is how to do it:

$lAccessToken = "{The access token you got in}" ;
$lPackageNameStr = "{your apps package name com.something.something}";
$lURLStr =  "https://www.googleapis.com/androidpublisher/v1.1/applications/$lPackageNameStr/subscriptions/$pProductIdStr/purchases/$pReceiptStr";

$curl = curl_init($lURLStr);

curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 
$curlheader[0] = "Authorization: Bearer " . $lAccessToken;
curl_setopt($curl, CURLOPT_HTTPHEADER, $curlheader);

$json_response = curl_exec($curl);
curl_close($curl);

$responseObj = json_decode($json_response,true);

The JSON returned will contain two timestamps, the initiationTimestampMsec and validUntilTimestampMsec the time the subscription is valid. Both are the nr of millisecs to add to the date 1/1/1970!

Solution 4

I don't know in 2012, but in 2015 you should not do any of these steps manually. I had a very hard time to find the documentation so I am posting here in case it helps anyone.

  1. You should only query in-app purchases from your server for security reasons as otherwise you can trust none of the 2 ends of the purchase process.

Now on the server side (I think you could still use the same code from your app if you absolutely need to), include the google-api-services-androidpublisher client library to your project (see https://developers.google.com/api-client-library/java/apis/androidpublisher/v1)

As you mentioned, you need a service account with a P12 file (the client library only accept P12 file).

Then the following code will authenticate and get purchase information nicely:

    HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
    JsonFactory jsonFactory = new JacksonFactory();

    List<String> scopes = new ArrayList<String>();
    scopes.add(AndroidPublisherScopes.ANDROIDPUBLISHER);

    Credential credential = new GoogleCredential.Builder().setTransport(httpTransport).setJsonFactory(jsonFactory)
            .setServiceAccountId(googleServiceAccountId)
            .setServiceAccountPrivateKeyFromP12File(new File(googleServicePrivateKeyPath))
            .setServiceAccountScopes(scopes).build();
    AndroidPublisher publisher = new AndroidPublisher.Builder(httpTransport, jsonFactory, credential).build();
    AndroidPublisher.Purchases purchases = publisher.purchases();
    final Get request = purchases.get(packageName, productId, token);
    final SubscriptionPurchase purchase = request.execute();

    // Do whatever you want with the purchase bean

Information on Java client authentication can be found here: https://developers.google.com/identity/protocols/OAuth2ServiceAccount

Solution 5

I may misunderstand your question, but I don't see a reason for you to be using the links you're referencing to get In-App Billing for an Android app working. This page is much more helpful:

http://developer.android.com/guide/google/play/billing/index.html

You can try out the demo application they include (Dungeons -- http://developer.android.com/guide/google/play/billing/billing_integrate.html#billing-download). That uses products (one-time purchases) rather than subscriptions, but you should be able to modify to test for what you want.

I think the key, for you, would be the restoreTransactions method they provide in the sample to see if the Google Play account has any subscriptions for your app:

@Override
public void onRestoreTransactionsResponse(RestoreTransactions request, int responseCode) {
    if (responseCode == BillingVars.OK) {                        
        // Update the shared preferences so that we don't perform a RestoreTransactions again.
        // This is also where you could save any existing subscriptions/purchases the user may have.
        SharedPreferences prefs = getSharedPreferences(my_prefs_file, Context.MODE_PRIVATE);
        SharedPreferences.Editor edit = prefs.edit();
        edit.putBoolean(DB_INITIALIZED, true);
        edit.commit();
    } else {
        Log.e(TAG, "RestoreTransactions error: " + responseCode);
    }
}
Share:
21,648
Kalina
Author by

Kalina

Updated on October 14, 2021

Comments

  • Kalina
    Kalina over 2 years

    I am making an app that does not require a user account/login, and allows the user to purchase a subscription. I want to use the Google Play Developer API to verify whether or not a user has a purchased/active subscription. From all of the documentation, I've gathered the following steps.

    Are they correct, and could you answer the two questions in them?

    1. Create a Service Account in the Google APIs Console.
    2. Save the private key that is given to me (where? surely not in my code/on the device as this sample code suggests)
    3. Use Google APIs Client Library for Java to create and sign a JWT with the private key (how? the docs give me this, but that is not Java code... What do I do with it?)
    4. Construct an access token request, and get access to the API
    5. Application can now send a GET request to the API to find out whether or not the user has a subscription
    6. When the access token expires, go back to step 3.

    Also, I have a web service, though I know nothing about web services or web service programming... I only know enough to be aware that it is probably necessary to use here.

    EDIT: These steps were not correct. See my answer below for the correct steps. However, note that this only applies to using a service account (because I did not want to require a user to have to explicitly allow API access)

  • Kalina
    Kalina almost 12 years
    Going to the "Subscriptions" page from the link you posted, there is this: developer.android.com/guide/google/play/billing/… Using this API seems to be the most secure way to verify a user's subscription, and to use this API you need to authorize the account with OAuth2, which requires the steps I listed... Perhaps what you say is a better way?
  • Kalina
    Kalina almost 12 years
    Thank you! However the previous answer to my question worries me. Those 6 steps are right, correct? Or should I just follow the Dungeons example?
  • John J Smith
    John J Smith almost 12 years
    From what I've read (though not used so far) I think your 6 steps are correct. The Dungeons example was, I believe, created before some of this new API functionality and the Dungeons example is not secure. There are some useful tips on Google I/O e.g. youtube.com/watch?v=TnSNCXR9fbY
  • Kalina
    Kalina almost 12 years
    Whew, great. One last question -- my web service is SOAP-based, and I know the Google Play API is REST... Is that going to be a problem?
  • John J Smith
    John J Smith almost 12 years
    Yes. Your SOAP-based service will expect a call to a base url plus op=MethodName and will expect the payload to be in XML format, whereas this REST-based service will expect a call to a url like baseurl/method and the payload is JSON, I believe. You'll probably have to parse the responses and reformat them when going between your service and Google's.
  • Dean Wild
    Dean Wild over 11 years
    BTW - You should not be doing this from your application directly, you should be doing this from your server. Your app should be talking only to your server directly.
  • Kalina
    Kalina over 11 years
    @DeanWild I am aware of the risks, but I do not have a server.
  • Dean Wild
    Dean Wild over 11 years
    Fair enough, just thought i'd point this out in case others read the post.
  • OferR
    OferR about 11 years
    @TheBeatlemaniac When I compare your answer to this document developers.google.com/android-publisher/authorization there seems to be a step missing between step 3 and step 4, which is getting a refresh token based on a code returned in the browser's address line. Can you please elaborate on this? Also, can you please elaborate on the statement saying "The refresh token never expires", since others seem to think that it does expire under some circumstances. (I am hoping that you are correct on both counts)
  • Kalina
    Kalina about 11 years
    @OferR, step 3 is where you're getting a refresh token based on a code returned in the browser's address line. That's exactly what that step is saying. Please read it again; I can try to rephrase it if it would help, but I think we're talking about the same thing here. As far as the refresh token expiring under some circumstances... I haven't heard about that but I suppose it could be true. It hasn't happened in my experience, but I don't know what those circumstances are.
  • OferR
    OferR about 11 years
    @TheBeatlemaniac Thank you for your prompt answer. We are doing the exact same procedure until step 2. The result that I get in step 2 is a URL in the address line that has code=xxxxx at the end. I then use this code with an extra HttpPost request which has grant_type=authorization_code, and I get a JSON response from which I get the refresh_token that I use in step 4. BTW, the code I get (in code=xxx) starts with 4/... Are we seeing the same thing? Thanks again..
  • Brian White
    Brian White almost 10 years
    @OferR, I believe you are correct. I had to also go through another step after #3 which then gave me a "refresh_token" usable in step #4: client_id=..., client_secret=..., code=<code from step #3>, grant_type=authorization_code, redirect_uri=...
  • Rudolfwm
    Rudolfwm almost 10 years
    Youre welcome, dont forget that the second accesstoken is valid only for a few hours. After that just ask for another.
  • OferR
    OferR almost 10 years
    Thanks @Brian White, Its been working well for me for the past year+.
  • Brian White
    Brian White almost 10 years
    I've edited the answer to add the missing step. Not sure if it needs approval from another editor before it goes live.
  • Rat-a-tat-a-tat Ratatouille
    Rat-a-tat-a-tat Ratatouille over 9 years
    @Kalina - i get an invalid request json string Step#4. Is there something i am missing?
  • Rat-a-tat-a-tat Ratatouille
    Rat-a-tat-a-tat Ratatouille over 9 years
    @Kalina - i figured it out.. i was making a get request instead of post. and then i used postman to encode the data.. (form-url-encode)
  • Rat-a-tat-a-tat Ratatouille
    Rat-a-tat-a-tat Ratatouille over 9 years
    i get an ssl error while trying to connect to google api.. how do i solve that ? And also an http 400 request when i try to support calling https
  • Paresh Gami
    Paresh Gami almost 9 years
    i got error like Array ( [error] => Array ( [errors] => Array ( [0] => Array ( [domain] => androidpublisher [reason] => permissionDenied [message] => The current user has insufficient permissions to perform the requested operation. ) ) [code] => 401 [message] => The current user has insufficient permissions to perform the requested operation. ) ). can you help me please
  • er.irfankhan11
    er.irfankhan11 over 8 years
    In this code, What is $pProductIdStr and $pReceiptStr
  • er.irfankhan11
    er.irfankhan11 over 8 years
    In the step 3 I have got 4/........ and next step return access_token but the 7th step not return subscribers information.
  • Rudolfwm
    Rudolfwm over 8 years
    @lrfan $pProductIdStr is your play store product ID and $pReceiptStr is the receipt you got when the purchase was made.
  • Helio Soares Junior
    Helio Soares Junior almost 8 years
    I need a help please, in my case, and a purchase and do not subscription, I already realized exchanges in the url address, however the result that always returns me is "Not Found" Url: https://www.googleapis.com/androidpublisher/v2/applications/‌​$lPackageNameStr/pur‌​chases/$pProductIdSt‌​r/purchases/$pReceip‌​tStr
  • Rudolfwm
    Rudolfwm almost 8 years
    do you have your $lPackageNameStr, $pProductIdStr and $pReceiptStr set correctly?
  • MiguelSlv
    MiguelSlv over 7 years
    Great help. Just need 1 extra step: link the Project at Google 'Play Developer Console' as explained here stackoverflow.com/a/25655897/2700303.
  • FetFrumos
    FetFrumos about 7 years
    Thank you. It is great. You save my time. I use "notasecret" - as p12 secret.
  • nadafafif
    nadafafif almost 7 years
    I am facing issue and getting error Id = 28, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}". Do you know why this could be ?
  • RamblinRose
    RamblinRose almost 7 years
    @nadafafif That sounds like you're have trouble with async/await? stackoverflow.com/questions/9255187/…
  • Selcuk Sasoglu
    Selcuk Sasoglu almost 7 years
    If you will deploy this code to Azure, you need to use X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet on X509Certificate2 constructor. Otherwise the code will try to use user certificate store which can be denied.
  • Admin
    Admin over 6 years
    i am getting this error. { "error" : "unauthorized_client" } . any idea what i am doing wrong here? thanks
  • Danjoa
    Danjoa over 6 years
    This works great, but now I am only getting 400 Invalid Value as response from Google for all my tokens.
  • Etienne Juneau
    Etienne Juneau almost 6 years
    I'm sorry; I'm stuck at this step: "To validate in-app purchases, visit the Cog->Change Permissions". Where is the "Cog->Change Permissions"?
  • Etienne Juneau
    Etienne Juneau almost 6 years
    Found it. Back under play.google.com, not at console.developers.google.com. It also looks like the UI was updated. Under "Global", I checked 2 boxes: "View app information" and "Manage Orders".
  • JPBlanc
    JPBlanc about 4 years
    Still working in 2020, if you meet the "This version of the Play Developer API is no longer available", use the "Google.Apis.AndroidPublisher.v3" NuGet package.
  • RamblinRose
    RamblinRose about 4 years
    @JPBlanc Thanks for the comment regarding V3! I've updated the post accordingly.
  • Meekohi
    Meekohi about 3 years
    This is so insane... why can't we just use the API key?
  • Meekohi
    Meekohi about 3 years
    This helped me a lot but I am still stuck getting: Google.Apis.Requests.RequestError The current user has insufficient permissions to perform the requested operation. [401] despite repeatedly confirming that this service account has the correct permissions. Hard to make progress when Google doesn't provide any insight into what is going on. Others say that updating permissions can take up to 48 hours. Who can get any work done in this environment?
  • Meekohi
    Meekohi about 3 years
    For other frustrated devs: stackoverflow.com/questions/54724015/… seems to confirm that you may have to wait up to 7 days for a service account to actually work properly.