I want to verify an Android IAP via Google's API on my central game server.
There is a lot of partial information about this and it is blowing my mind. I have not paid €25 to become a Google Developer, because I am not sure if I will be able to get it to work.
When an IAP is made, a JSON object is returned. This object contains several fields, like the purchaseToken
and the productId
(source).
I found that you can request information about a bought product via the following GET request: GET https://www.googleapis.com/androidpublisher/v2/applications/packageName/purchases/products/productId/tokens/token
.
I could program this no problem, but you need to authorize yourself: "This request requires authorization with the following scope" (source). This is where I started getting confused.
There are several huge code snippets to be found on the internet that may or may not work, but they are all partial and not very well documented.
I found Googles API library for Java: link. This API seems to be made to fix all these problems with OAuth and tokens for you. However, I am unable to figure out how to get this API to work.
It is probably not that hard, but there are a lot of different ways to do it, and I can't find any clear examples.
TL;DR: I need to verify a Google Play IAP serverside. To do this, I want to use Googles Java API.
EDIT: THIS MIGHT BE A WAY SIMPLER SOLUTION. Passing the original JSON plus the JSON to the server might be way easier, because I could just verify the asymmetric signature server side.
I have done that in Scala, but using the Java standard Library. it should be simple to convert that code to Java I believe. The main advantage of this implementation is that it contains zero dependencies on Google's libraries.
First of all, you need a service account. You can create that via the Google Dev console. It basically gives you back a generated email account that you will use to authenticate your backend service and generate the tokens.
With that account created, you are prompt to download your private key. You need that in order to sign the JWT.
You have to generate a JWT in the format Google specifies (I show you how in the code below). See: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatingjwt
then, with the JWT, you can request the access token
With the access token, you can make requests to validate your purchases
/** 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 the JWT generated, you can ask for the access token
like that:
/** 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 the access token on your hands, you are free to validate your purchases.
I Hope that helps.