Java server-side authentication for Android IAP

side authentication for Android IAP… here is a solution to the problem.

side authentication for Android IAP

I want to verify Android IAP via Google’s API on my central game server.

There was a lot of partial information about this, which blew my mind.
I haven’t paid 25 euros to become a Google developer yet because I’m not sure if I’ll be able to make it work.

When an IAP is created, a JSON object is returned. The object contains several fields, such as purchaseToken and productId ( source )。

I found that you can request information about a purchased product with the following GET request: GET https://www.googleapis.com/androidpublisher/v2/applications/packageName/purchases/products/productId/tokens/ token.

I can program this fine, but you need to authorize yourself: “This request requires authorization from the following scope” ( source )。
This is where I started to get confused.

  1. You need to create some kind of login token through the development console (Link). I don’t know what type it is. OAuth or service account?
  2. This token is short-lived. You need to refresh it

Several huge code fragments can be found on the Internet that may or may not work, but they are all partial and not well documented.

I found the Google API library for Java: link. However, I can’t figure out how to make this API work.

This may not be difficult, but there are so many different ways to do it, I can’t find any clear examples.

TL;DR: I need to verify the Google Play IAP server side. To do this, I want to use Google’s Java API.

EDIT: This may be a simpler solution.
It might be easier to pass the raw JSON plus JSON to the server because I can only verify the asymmetric signature server-side.

Solution

I’ve done it in Scala, but using the Java Standard Library. I believe converting that code to Java should be simple. The main advantage of this implementation is that it includes zero dependency on the Google library.

  • First, you need a service account. You can create it through the Google Dev console. It basically returns you a generated email account that you will use to authenticate your backend service and generate tokens.

  • After you create the account, you are prompted to download the private key. You need it to sign the JWT.

  • You must generate the JWT in the format specified by Google (I showed you how in the code below).
    See: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatingjwt

  • Then, using JWT, you can request access to the token

  • With Access Token, you can make a request to validate your purchase

/** Generate JWT(JSON Web Token) to request access token
* How to generate JWT: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatingjwt
*
* If we need to generate a new Service Account in the Google Developer Console,
* we are going to receive a .p12 file as the private key. We need to convert it to .der.
* That way the standard Java library can handle that.
*
* Covert the .p12 file to .pem with the following command:
* openssl pkcs12 -in <FILENAME>.p12 -out <FILENAME>.pem -nodes
*
* Convert the .pem file to .der with the following command:
* openssl pkcs8 -topk8 -inform PEM -outform DER -in <FILENAME>.pem -out <FILENAME>.der -nocrypt
*
* */
private def generateJWT(): String = {

 Generating the Header
  val header = Json.obj("alg" -> "RS256", "typ" -> "JWT").toString()

 Generating the Claim Set
  val currentDate = DateTime.now(DateTimeZone.UTC)
  val claimSet =Json.obj(
    "iss" -> "<YOUR_SERVICE_ACCOUNT_EMAIL>",
    "scope" -> "https://www.googleapis.com/auth/androidpublisher",
    "aud" -> "https://www.googleapis.com/oauth2/v4/token",
    "exp" -> currentDate.plusMinutes(5).getMillis / 1000,
    "iat" -> currentDate.getMillis / 1000
  ).toString()

 Base64URL encoded body
  val encodedHeader = Base64.getEncoder.encodeToString(header.getBytes(StandardCharsets.UTF_8))
  val encodedClaimSet = Base64.getEncoder.encodeToString(claimSet.getBytes(StandardCharsets.UTF_8))

 use header and claim set as input for signature in the following format:
   {Base64url encoded JSON header}. {Base64url encoded JSON claim set}
  val jwtSignatureInput = s"$encodedHeader.$encodedClaimSet"
   use the private key generated by Google Developer console to sign the content. 
   Maybe cache this content to avoid unnecessary round-trips to the disk.
  val keyFile = Paths.get("<path_to_google_play_store_api.der>");
  val keyBytes = Files.readAllBytes(keyFile);

val keyFactory = KeyFactory.getInstance("RSA")
  val keySpec = new PKCS8EncodedKeySpec(keyBytes)
  val privateKey = keyFactory.generatePrivate(keySpec)

 Sign payload using the private key
  val sign = Signature.getInstance("SHA256withRSA")
  sign.initSign(privateKey)
  sign.update(jwtSignatureInput.getBytes(StandardCharsets.UTF_8))
  val signatureByteArray = sign.sign()
  val signature = Base64.getEncoder.encodeToString(signatureByteArray)

 Generate the JWT in the following format:
   {Base64url encoded JSON header}. {Base64url encoded JSON claim set}. {Base64url encoded signature}
  s"$encodedHeader.$encodedClaimSet.$signature"
}

Now that you have generated the JWT, you can request the access token: like this

/** Request the Google Play access token */
private def getAccessToken(): Future[String] = {

ws.url("https://www.googleapis.com/oauth2/v4/token")
    .withHeaders("Content-Type" -> "application/x-www-form-urlencoded")
    .post(
      Map(
        "grant_type" -> Seq("urn:ietf:params:oauth:grant-type:jwt-bearer"),
        "assertion" -> Seq(generateJWT()))
    ).map {
    response =>
      try {
        (response.json \ "access_token").as[String]
      } catch {
        case ex: Exception => throw new IllegalArgumentException("GooglePlayAPI - Invalid response: ", ex)
      }
  }

}

With Access Token, you have the freedom to verify your purchases.

Hope this helps.

Related Problems and Solutions