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.
- 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?
- 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#creatingjwtThen, 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.