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.
You need to create some sort of login token via the Dev Console (Link). I don't know what type. OAuth or service account?
This token is short lived. You need to refresh it
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.
Following on from my comment above, the Google API manager has changed in design but the procedure is still the same. You want to create a Service Account, once created you can download the JSON Web token, it's a simple JSON file with the credentials you need to authenticate. The service account should have all access you need to access the Google Play Developer API. You will need to grant access to Finance in the Google Play Developer Console > Settings > API Access page.
You can use the Google APIs Client Library for Java to authenticate and make requests to the Google Play Developer API. Follow the OAuth2 Service accounts docs to get you set up. There is a note on Data store that describes how the refresh token is handled.
There are also docs on how to use the Google Play Developer API Client Library for Java.
As for validating the signature of the purchase JSON, there is a INAPP_DATA_SIGNATURE in the response payload of the purchase intent. See the docs on Purchasing an Item for more information on how to get it. You can verify the INAPP_PURCHASE_DATA by base64 decoding the signature and verifying it with your licence key, found in the Google Play Developer Console > All Applications > [App name] > Services & APIs. Ensure the INAPP_PURCHASE_DATA is intact otherwise you will end up with this problem.
Hope this helps.
Related
I'm trying to test the most basic use cases around encryption/decryption with AWS S3 and AWS java SDK (trying both v1 and v2).
So this is what I'm doing :
I upload a small json file using aws console, and then I check that in Properties > crypt, "AWS-KMS" is selected and my key alias is selected. I assume this tells me the file is encrypted with my key, but I have no way to check this, since if I try to open the file using aws console, it's in clear text.
I try to download the file using various methods, and I expect to get an encrypted file when I use the most basic method.
So by using this client (sdk v2) :
#Bean
public S3Client s3Clientv2(AppProperties appProperties, CustomAwsCredentialsProvider customAwsCredentialsProvider) {
return S3Client.builder()
.httpClientBuilder(httpClientBuilder)
.credentialsProvider(customAwsCredentialsProvider)
.region(Region.EU_WEST_3)
.build();
}
and this download method :
public void downloadFile(String bucket, String key) {
s3Client.getObject(GetObjectRequest.builder().bucket(bucket).key(key).build(), ResponseTransformer.toFile(Paths.get("test_aws.json")));
}
I expected to get an encrypted file, but it was not.
Then I tried to use a client able to "encrypt/decrypt" by itself "Any objects you get from Amazon S3 using this client are automatically decrypted" source : https://docs.aws.amazon.com/en_pv/sdk-for-java/v1/developer-guide/examples-crypto-kms.html
AmazonS3Encryption s3Encryption = AmazonS3EncryptionClientBuilder
.standard()
.withRegion(Regions.US_WEST_2)
.withCryptoConfiguration(new CryptoConfiguration(CryptoMode.EncryptionOnly).withAwsKmsRegion(Region.getRegion(Regions.US_WEST_2)))
// Can either be Key ID or alias (prefixed with 'alias/')
.withEncryptionMaterials(new KMSEncryptionMaterialsProvider("alias/s3-kms-key"))
.build();
But using :
S3Object file = s3Encryption.getObject(new GetObjectRequest(bucket, key));
with this client call gets me a warning : "Unable to detect encryption information for object '%s' in bucket '%s'. Returning object without decryption."
So what am I doing wrong here ?
How can I check my file is really encrypted ?
What would be the right config to download it and decrypt it without a "file not encrypted" warning ?
Update : of course I've checked the object metadata, which do contain the KMS information and the KMS key id, but the encryption client is expecting some other informations about this :
/** Initialization vector (IV) header that is used in the symmetric and envelope encryption mechanisms */
public static final String CRYPTO_IV = "x-amz-iv";
and this :
/**
* Encrypted symmetric key header that is used in the Encryption Only (EO) envelope
* encryption mechanism.
*/
public static final String CRYPTO_KEY = "x-amz-key";
Ok, I think I get it : I got confused between server-side encryption and client-side encryption.
So AmazonS3Encryption client is only about client-side encryption. When you are using server-side encryption, any access to the file will decrypt it.
So the answer to my question would be : disable server-side encryption and use client-side encryption if you need fine-grained control about encryption, and to be able to download still encrypted content.
I had written a library in PHP using the AWS to communicate with the ECS server. Now I am migrating my code to java. In java I am using the same key and secret which I used in PHP.
In php I used the following method:
$s3 = Aws\S3\S3Client::factory(array( 'base_url' => $this->base_url, 'command.params' => array('PathStyle' => true), 'key' => $this->key, 'secret' => $this->secret ));
In java I am using following method
BasicAWSCredentials(String accessKey, String secretKey);
I am getting the following exception:
Exception in thread "main" com.amazonaws.services.s3.model.AmazonS3Exception: The AWS Access Key Id you provided does not exist in our records. (Service: Amazon S3; Status Code: 403; Error Code: InvalidAccessKeyId; Request ID: 3A3000708C8D7883), S3 Extended Request ID: U2rG9KLBBQrAqa6M2rZj65uhaHhOpZpY2VK1rXzqoGrKVd4R/JdR8aeih/skG4fIrecokE4FY3w=
at com.amazonaws.http.AmazonHttpClient.handleErrorResponse(AmazonHttpClient.java:1401)
at com.amazonaws.http.AmazonHttpClient.executeOneRequest(AmazonHttpClient.java:945)
at com.amazonaws.http.AmazonHttpClient.executeHelper(AmazonHttpClient.java:723)
at com.amazonaws.http.AmazonHttpClient.doExecute(AmazonHttpClient.java:475)
at com.amazonaws.http.AmazonHttpClient.executeWithTimer(AmazonHttpClient.java:437)
at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:386)
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:3996)
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:3933)
at com.amazonaws.services.s3.AmazonS3Client.listBuckets(AmazonS3Client.java:851)
at com.amazonaws.services.s3.AmazonS3Client.listBuckets(AmazonS3Client.java:857)
at com.justdial.DocUpload.DocUpload.main(DocUpload.java:22)
Do I need a new key and secret for Java or the previous one can be used.
My questions are:
1 - Is then any prerequisite that we need to follow before using AWS's BasicAwsCredentials() method.
2 - Do we need to create the IAM roles.
3 - If we do then how will it come to know which IP to hit as in case of PHP in Aws\S3\S3Client::factory method I was specifying the base url.
Following code worked for me.
String accesskey = "objuser1";
String secret = "xxxxxxxxxxxxxxxx";
ClientConfiguration config = new ClientConfiguration();
config.setProtocol(Protocol.HTTP);
AmazonS3 s3 = new AmazonS3Client(new BasicAWSCredentials(accesskey, secret), config);
S3ClientOptions options = new S3ClientOptions();
options.setPathStyleAccess(true);
s3.setS3ClientOptions(options);
s3.setEndpoint("1.2.3.4:9020"); //ECS IP Address
System.out.println("Listing buckets");
for (Bucket bucket : s3.listBuckets()) {
System.out.println(" - " + bucket.getName());
}
System.out.println();
AWS access key and secret access keys are independent of SDKs you use (doesn't matter whether you use PHP, Java or Ruby etc), it's more about granting and getting access to services you run/use on AWS.
Root cause of the error is I think you've not set AWS region though you've given access key and secret access key. Please check this to set AWS credentials for Java.
To answer your questions.
As far as I know BasicAwsCredentials() itself acts as prerequisite step before you want to do anything with Amazon services. Only prerequisite for BasicAwsCredentials() is to have the authentication keys.
No, it's not mandatory step. It's an alternative. Please check the definition of IAM role from this link.
You can use the 'endpoint' to do the same job in Java. Check 'endpoint' details from this link.
Please take a look at the examples from here.
Using the same method I'm able to successfully do the 'BasicAwsCredentials()'. And also cross check whether 'access key' and 'secret access key' have necessary permissions to access Amazon services as per your need. Check IAM user, group and ensure they've necessary AWS permissions.
I recently got to know about Json Web Token (JWT). Since I liked how it works I have started to implement it on my project. My project involves two apps to communicate. One is an android app and the other is Laravel web application.
The mobile app logs in after the user credential is authenticated from the server side.
I have sent the username and password to server from the mobile app and I have got the JWT in string format. But from this point onward I couldn't find a way to collect the JWT content.
I have gone through almost all possible shown (googled results) but I couldn't manage to get the contents, signature and header.
One of the method I have got a little bit further with, was using the following code, notice I have removed the setSigningKey():
try {
Claims claims = Jwts.parser().parseClaimsJwt(jwtHeaderAndClaim).getBody();
System.out.println("ID of the claims: " + claims.getId().toString());
}catch (Exception e){
Log.e("Exception: ", e.toString());
}
The above code generates the following error:
Exception: io.jsonwebtoken.PrematureJwtException: JWT must not be accepted before 2016-06-14T10:20:09+0300. Current time: 2016-06-14T10:19:37+0300´
the jwtHeaderAndClaim is the JWT String after removing the signature part only (i.e: "xxxxxx.yyyyyyyy."). if i put the jwtString (xxxxxxx.yyyyyyyy.ccccccc) instead of jwtHeaderAndClaim the following error will occur:
Exception: io.jsonwebtoken.UnsupportedJwtException: Signed JWSs are not supported
If I put the setSigningKey as shown in stormpath example:
Claims claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(apiKey.getSecret())).parseClaimsJwt(jwtString).getBody();.
The above code will not work for two reasons:
1. I don't have the library
import javax.xml.bind.DatatypeConverter;
2. I don't know how to get the key.
But know that I don't need the key since this time I am trying to login and collect the user information's (like firstname, lastname, phone, etc), and the signature (token) so that the next time I send data to be stored to the server side I have the token to get access to the backend.
Can anyone please help me?
You have many questions. I try to answer some of them
io.jsonwebtoken.PrematureJwtException: JWT must not be accepted before
2016-06-14T10:20:09+0300. Current time: 2016-06-14T10:19:37+0300´
You are using nbf (not before) attribute in JWT. Do not use it (it is optional) or sets a range of validity given that the clocks of the devices will not be synchronized
From RFC 7519
The "nbf" (not before) claim identifies the time before which the JWT
MUST NOT be accepted for processing. The processing of the "nbf" claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the "nbf" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.
Signed JWS
Exception: io.jsonwebtoken.UnsupportedJwtException: Signed JWSs are
not supported
Do you want to validate the signing key at client side or at server side?
If you use the JWT for authentication replacing user & password, and you are sending token in each request, you can validate the signature at server side.
If you want to validate the key on the app, do not use a symmetric key, because it could be a big vulnerability if it fell into the wrong hands. See. You can use and asymmetric key pair. Sign the JWT in server with the private key and validate on device with public key.
I don't have the library import javax.xml.bind.DatatypeConverter
String base64 = Base64.encodeToString(data, Base64.DEFAULT);
byte[] data = Base64.decode(base64, Base64.DEFAULT);
I don't know how to get the key.
Your key probably was generated on server side in this way
Key key = MacProvider.generateKey(SignatureAlgorithm.HS256);
byte data[] = key.getEncoded();
Make available the key data[] to client in the way you prefer. Using assymetric keys, you only need to make available the public key.
KeyPair keyPair = RsaProvider.generateKeyPair();
byte data[] = keyPair.getPublic().getEncoded();
I found the follow note, which describes exactly what I'd like to do:
Note: If your users are only uploading resources (writing) to an access-controlled bucket, you can use the resumable uploads functionality of Google Cloud Storage, and avoid signing URLs or requiring a Google account. In a resumable upload scenario, your (server-side) code authenticates and initiates an upload to Google Cloud Storage without actually uploading any data. The initiation request returns an upload ID, which can then be used in a client request to upload the data. The client request does not need to be signed because the upload ID, in effect, acts as an authentication token. If you choose this path, be sure to transmit the upload ID over HTTPS.
https://cloud.google.com/storage/docs/access-control#Signed-URLs
However, I cannot figure out how to do this with the Google Cloud Storage Library for Java.
https://developers.google.com/resources/api-libraries/documentation/storage/v1/java/latest/
I can't find any reference to resumable files, or getting the URL for a file anywhere in this API. How can I do this?
That library does not expose the URLs that it creates to its caller, which means you can't use it to accomplish this. If you want to use either signed URLs or the trick you mention above, you'll need to implement it manually.
I would advise going with the signed URL solution over the solution where the server initializes the resumable upload, if possible. It's more flexible and easier to get right, and there are some odd edge cases with the latter method that you could run into.
Someone wrote a up a quick example of signing a URL from App Engine a while back in another question: Cloud storage and secure download strategy on app engine. GCS acl or blobstore
You can build the url yourself. Here is an example :
OkHttpClient client = new OkHttpClient();
AppIdentityService appIdentityService = credential.getAppIdentityService();
Collection<String> scopes = credential.getScopes();
String accessToken = appIdentityService.getAccessToken(scopes).getAccessToken();
Request request = new Request.Builder()
.url("https://www.googleapis.com/upload/storage/v1/b/" + bucket + "/o?name=" + fileName + "&uploadType=resumable")
.post(RequestBody.create(MediaType.parse(mimeType), new byte[0]))
.addHeader("X-Upload-Content-Type", mimeType)
.addHeader("X-Upload-Content-Length", "" + length)
.addHeader("Origin", "http://localhost:8080")
.addHeader("Origin", "*")
.addHeader("authorization", "Bearer "+accessToken)
.build();
Response response = client.newCall(request).execute();
return response.header("location");
It took some digging, but I came up with the following which does the right thing. Some official documentation on how to do this would have been nice, especially because the endpoint for actually triggering the resumable upload is different from what the docs call out. What is here came from using the gsutil tool to sign requests and then working out what was being done. The under-documented additional thing is that the code which POSTs to this URL to get a resumable session URL must include the "x-goog-resumable: start" header to trigger the upload. From there, everything is the same as the docs for performing a resumable upload to GCS.
import base64
import datetime
import time
import urllib
from google.appengine.api import app_identity
SIGNED_URL_EXPIRATION = datetime.timedelta(days=7)
def SignResumableUploadUrl(gcs_resource_path):
"""Generates a signed resumable upload URL.
Note that documentation on this ability is sketchy. The canonical source
is derived from running the gsutil program to generate a RESUMABLE URL
with the "-m RESUMABLE" argument. Run "gsutil help signurl" for info and
the following for an example:
gsutil -m RESUMABLE -d 10m keyfile gs://bucket/file/name
Note that this generates a URL different from the standard mechanism for
deriving a resumable start URL and the initiator needs to add the header:
x-goog-resumable:start
Args:
gcs_resource_path: The path of the GCS resource, including bucket name.
Returns:
A full signed URL.
"""
method = "POST"
expiration = datetime.datetime.utcnow() + SIGNED_URL_EXPIRATION
expiration = int(time.mktime(expiration.timetuple()))
signature_string = "\n".join([
method,
"", # content md5
"", # content type
str(expiration),
"x-goog-resumable:start",
gcs_resource_path
])
_, signature_bytes = app_identity.sign_blob(signature_string)
signature = base64.b64encode(signature_bytes)
query_params = {
"GoogleAccessId": app_identity.get_service_account_name(),
"Expires": str(expiration),
"Signature": signature,
}
return "{endpoint}{resource}?{querystring}".format(
endpoint="https://storage.googleapis.com",
resource=gcs_resource_path,
querystring=urllib.urlencode(query_params))
I am trying to generate an Oauth signature in order to authenticate an user in flickr.com from my android app.
According to the article in flickr.com, I have to send a signing request in order to get the signature. The hyperlink to the flickr.com guide page is:
http://www.flickr.com/services/api/auth.oauth.html#request_token
According to the post, I have to send a request like this to the flickr server in order to receive the signature key:
http://www.flickr.com/services/oauth/request_token
?oauth_nonce=89601180
&oauth_timestamp=1305583298
&oauth_consumer_key=653e7a6ecc1d528c516cc8f92cf98611
&oauth_signature_method=HMAC-SHA1
&oauth_version=1.0
&oauth_callback=http%3A%2F%2Fwww.example.com
I have send a request from my app, in the above mentioned format, but all I received is an error saying oauth_problem=parameter_absent&oauth_parameter_absent=oauth_signature.
My request code is:
HttpGet get = new HttpGet("http://www.flickr.com/services/oauth/request_token?oauth_nonce="+nonce+"&oauth_timestamp="+ts+"&oauth_consumer_key=****&oauth_signature_method=HMAC-SHA1&oauth_version=1.0");
Actually the problem is that, the url through which I am requesting for the signature is responding in a wrong way. Where it should return the signature, its asking for the signature.
The signing step is no request. You take the URI you have so far and transform it into the base string as seen in the documentation (URL encoding its parts and the like). The you use the HMAC-SHA1 algorithm witch takes the 2 parameters key and data. Use the base string as data and key
is the concatenated values of the Consumer Secret and Token Secret, separated by an '&'.
The value you get back from the algorithm (the signature) is then appended to your URI with
&oauth_signature={ALGORITHM_OUTPUT}
Using this new URI you can then request tokens.
If you think this is too much work, check out some Java OAuth library, e.g. scribe-java.