I'm working with Google Cloud Storage in AppEngine and I'm attempting to use a POST form to upload a file to GCS. The problem I'm having is with the steps needed to sign the policy document. I can easily fetch the client_secret, which is a String from the client_secrets.json that the API Console gave me. however, in order to create a signature, I need to convert that string into a PrivateKey object. Here's what my code looks like:
//create the policy document and encode it
String policyDocument = ... //omitted for brevity
String encodedPolicy = Base64.encodeString(policyDocument);
//sign using SHA256 with RSA
String secretKey = ... //I fetch this from client_secrets.json
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(secretKey); //THIS IS THE PROBLEM!
sig.update(encodedPolicy.getBytes("UTF-8"));
String signature = new String(Base64.encode(sig.sign()));
//put the values in the request attributes so we can fetch them from a JSP
req.setAttribute("policy", encodedPolicy);
req.setAttribute("signature", signature);
As noted above, my problem is in the line
sig.initSign(secretKey); //THIS IS THE PROBLEM!
secretKey is a String. Signature.initSign() expects a PrivateKey, or one of its descendant objects. How do I convert the string in the client_secrets.json into a PrivateKey (or derived) object that I can pass Signature.initSign?
Any help would be greatly appreciated. Thanks
OK, here's where I am right now. I tried the suggestions below, and all of the documentation is urging me to use the client_secret in the client_secrets.json file downloaded from the Google API console, not the service account. And besides, I'm trying to construct an example of a user's upload, not a service account.
I found the following code on another page:
public static String signPolicyDocument(String policyDocument, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
byte[] secretBytes = secret.getBytes();
SecretKeySpec signingKey = new SecretKeySpec(secretBytes, "HmacSHA256");
mac.init(signingKey);
byte[] signedSecretBytes = mac.doFinal(policyDocument.getBytes());
return new String(Base64.encode(signedSecretBytes));
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
And it gets me all the way through the process...until I submit the resulting form. Then I get the following response:
The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.
What signing method is it looking for?
Here's what I think you need to do:
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
sig.initSign(privateKey);
The keyBytes variable should contain a byte[] array with your service account key file in it.
The final answer to this problem is like the conclusion of Wargames. As WOPR said, "A strange game...the only way to win is not to play." Avoid signing and policy document and all that crap and use the blobstore.
(See this: https://developers.google.com/appengine/docs/java/blobstore/overview#using-blobstore-with-gcs)
It's very easy to implement; when you create your temporary blobstore upload URL like so:
//open the blobstore service and create the upload url
BlobstoreService bs = BlobstoreServiceFactory.getBlobstoreService();
String uploadUrl = bs.createUploadUrl("/display",
UploadOptions.Builder.withGoogleStorageBucketName(bucket));
The downside to this approach is the object name will be a string of characters you don't recognize. You can open the blobstore viewer and see your object by file name in the blobstore, but in GCS its object name will be gobbledygook. (A hash, maybe? A randomly assigned ID)?
To upload a file to GCS from Appengine you can use the blobstore Api. Follow the steps described in Using Blobstore with GCS. The advantage is that you don't have to worry about keys and the code is much simpler.
Related
I need to update the permissions of a Drive document from Salesforce.
I wanted to use Named Credentials, but I didn't find any way of building a call like this one:
https://www.googleapis.com/drive/v3/files/{documentId}/permissions
where {documentId} is a dynamic value.
I've seen that it is possible to add a prefix, but actually even if I create a Named Credential with only https://www.googleapis.com/drive/v3/files, when I call it from my Apex class I get a permission error.
Is there a way to achieve what I would like or I need to change approach?
Thank you
What exactly error are you getting? Salesforce security about not having access to class X? Something about callouts not allowed from triggers? You're sure it works with hardcoded document id?
Should be possible to make the named credential point to https://www.googleapis.com or https://www.googleapis.com/drive/v3/files/ and then add the rest of the endpoint in Apex. If it throws errors - maybe Drive's API is special, you'd need to read up.
String endpoint = 'callout:MyNamedCredential' + '/abc123/permissions';
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('GET');
I have something like that:
Named credential pointing to https://maps.googleapis.com/maps/api/geocode/json
and then
static final String ENDPOINT = 'callout:GoogleMaps?key={0}&latlng={1}&result_type=premise%7Cstreet_address';
String apiKey = SomeCustomSetting__c.getInstance().GoogleApiKey__c;
String latLng = '60.23,11.17';
req.setEndpoint(String.format(ENDPOINT, new List<String>{apiKey, latLng}));
HttpResponse res = h.send(req);
You could also look into "Files Connect" API I guess.
I am actually trying to implement text-to-speech conversion in Salesforce by hitting a third-party api. When i send the request through Postman, i get back the proper response in .wav format. However, I'm not being able to handle this reponse programatically in salesforce end, as I am not able to store the response in any audio object.
Any assistance would be greatly appreciated.
Thanks in advance.
Abhishek.
Not 100% sure of what your trying to do but it would look something like this assuming you built your object correctly
ResponseObject result = new ResponseObject();
result = (InnerClasses.ResponseObject)JSON.deserialize(json, InnerClasses.ResponseObject.class);
This is supported by IBM's Watson Salesforce SDK, available here
The functional tests for Watson's text-to-speech services can be found here refer to the method
testSynthesize(String username, String password, String customizationId)
The audio file returned as part of the response could be saved as an attachment in Salesforce by simply creating it from the IBMWatsonFile like on this example,
IBMWatsonFile resp = textToSpeech.synthesize(options);
Attachment attachment = new Attachment();
attachment.Body = resp.body();
attachment.Name = resp.name();
attachment.ParentId = '<your salesforce parent id>';
insert attachment;
This code uses basically the method getBodyAsBlob() available from the HttpResponse class
before using this approach, please consider the governor limits enforced by Salesforce on any APEX callout, refer to the Maximum size of callout request or response documented here
I have an unity app and use the google-play-games plugin with google *.aar versions 9.4.0. I lately changed my Backend (Google App Engine) from php to java. My problem is the following: in php the serverauthcode is used to get the users data (in JWT format) - it was working fine. So I changed to a Java servlet and I am failing since 2 days to get a valid idtoken. I am able to recieve the server auth code from my app and a valid token response is made by GoogleAuthorizationCodeTokenRequest (see code snippet). Unfortunately it does not contain any idtoken content but a valid auth_token. So I can not get the user id to identifiy the user. When I call tokenResponse.parseIdToken(); it is failing with a NullPointerException.
servlet code (authCode is the serverAuthCode I send from the play-games-plugin inside Unity to my GAE):
// (Receive authCode via HTTPS POST)
// Set path to the Web application client_secret_*.json file you downloaded from the
// Google Developers Console: https://console.developers.google.com/apis/credentials?project=_
// You can also find your Web application client ID and client secret from the
// console and specify them directly when you create the GoogleAuthorizationCodeTokenRequest
// object.
String CLIENT_SECRET_FILE = "/mypath/client_secret.json";
// Exchange auth code for access token
GoogleClientSecrets clientSecrets =
GoogleClientSecrets.load(
JacksonFactory.getDefaultInstance(), new FileReader(CLIENT_SECRET_FILE));
GoogleTokenResponse tokenResponse =
new GoogleAuthorizationCodeTokenRequest(
new NetHttpTransport(),
JacksonFactory.getDefaultInstance(),
clientSecrets.getDetails().getTokenUri(),
clientSecrets.getDetails().getClientId(),
clientSecrets.getDetails().getClientSecret(),
authCode,
REDIRECT_URI) // Specify the same redirect URI that you use with your web
// app. If you don't have a web version of your app, you can
// specify an empty string.
.execute();
String accessToken = tokenResponse.getAccessToken();
// Get profile info from ID token -> HERE IT THROWS AN EXCEPTION.
GoogleIdToken idToken = tokenResponse.parseIdToken();
GoogleIdToken.Payload payload = idToken.getPayload();
String userId = payload.getSubject(); // Use this value as a key to identify a user.
String email = payload.getEmail();
boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
String locale = (String) payload.get("locale");
String familyName = (String) payload.get("family_name");
String givenName = (String) payload.get("given_name");
the token response looks like (its invalid now):
{
"access_token" : "ya29.CjA8A7O96w-vX4OCSPm-GMEPGVIEuRTeOxKy_75z6fbYVSXsdi9Ot3NmxlE-j_t-BI",
"expires_in" : 3596,
"token_type" : "Bearer"
}
In my PHP GAE I always had a idToken inside this constuct which contained my encrypted data. But it is missing now?! So I asssume I do somthing differently in Java or I made a mistake creating the new OAuth 2.0 Client on the google console.
I checked the accessToken manually via:
https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=ya29.CjA8A7O96w-vX4OCSPm-GMEPGVIEu-RTeOxKy_75z6fbYVSXsdi9Ot3NmxlE-j_t-BI
{
"issued_to": "48168146---------.apps.googleusercontent.com",
"audience": "48168146---------.apps.googleusercontent.com",
"scope": "https://www.googleapis.com/auth/games_lite",
"expires_in": 879,
"access_type": "offline"
}
Is there something I do not see? Help is very much appreciated...
I found a root cause discussion inside the unity plugin "play-games-services" on github:
https://github.com/playgameservices/play-games-plugin-for-unity/issues/1293
and
https://github.com/playgameservices/play-games-plugin-for-unity/issues/1309
It seems that google switching their authentication flow. In the given links they are talking about adding the email scope inside the plugin to get the idtoken again. I'll try that in the next days and share my experience.
Here is a good explaination about what happens:
http://android-developers.blogspot.de/2016/01/play-games-permissions-are-changing-in.html
If you do what paulsalameh said here (Link to Github) it will work again:
paulsalameh: Sure. After you import the unitypackage, download NativeClient.cs and
PlayGamesClientConfig.cs from my commits (#1295 & #1296), and replace
them in the correct locations.
Afte that "unity play-services-plugin" code update you will be able to add AddOauthScope("email") to PlayGamesClientConfiguration, which allows your server to get the idtoken with the serverAuthCode again...
Code snippet from Unity:
PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
.AddOauthScope("email")
.AddOauthScope("profile")
.Build();
Now I am back in business:
{
"access_token" : "ya29.Ci8..7kBR-eBdPw1-P7Pe8QUC7e_Zv7qxCHA",
"expires_in" : 3600,
"id_token" : "eyJhbGciOi......I1NiE0v6kqw",
"refresh_token" : "1/HlSZOo......dQV1y4E",
"token_type" : "Bearer"
}
I am trying to authenticate via a service account from Salesforce.com to Google's DFP. I had the integration working under a previous user/credential pair, but am required to update to a new user.
I created the project/user/key pair in the Google Developer Console and added the new service account to the network in DFP. I then changed the "iss" value to be the new user's email and the private key to be the new private key from the keypair.
I am now receiving an 'Invalid Signature' error.
In SFDC, I am using Crypto.sign method with RSA-SHA256.
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_crypto.htm#apex_System_Crypto_sign
I have validated the key format to be PKCS#8 with header and new line characters removed per the documentation (I went so far as to decode the ASN.1 format and inspect the nodes for conformity).
Have I missed a step in the connection between the user and the correct credential? Is there a way for me to validate the signature that I am producing locally to see where I am going wrong? The only difference I have seen is that the old private key was shorter than the current private key.
Below is the code I am using to generate the JWT (again, this code functioned properly with a different username and credential key).
JWTHeader head = new JWTHeader();
head.alg = 'RS256';
head.typ = 'JWT';
JWTClaimSet claim = new JWTClaimSet();
claim.iss = '<username>#*.iam.gserviceaccount.com';
claim.scope = 'https://www.googleapis.com/auth/dfp';
claim.aud = 'https://accounts.google.com/o/oauth2/token';
claim.iat = DateTime.now().getTime() / 1000;
claim.exp = claim.iat + 3600;
System.debug(JSON.serialize(head));
System.debug(JSON.serialize(claim));
String key = '<privatekey>’;
String base = EncodingUtil.urlEncode(EncodingUtil.base64Encode(Blob.valueOf(JSON.serialize(head))), 'UTF-8') + '.' + EncodingUtil.urlEncode(EncodingUtil.base64Encode(Blob.valueOf(JSON.serialize(claim))), 'UTF-8');
String sig = EncodingUtil.urlEncode(EncodingUtil.base64Encode(Crypto.sign('RSA-SHA256', Blob.valueOf(base), EncodingUtil.base64Decode(key))), 'UTF-8');
String body = base + '.' + sig;
System.debug(body);
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('https://accounts.google.com/o/oauth2/token');
req.setBody('grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=' + body);
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
req.setMethod('POST');
HttpResponse resp = http.send(req);
Days later I found another solution that solved the issue. The problem was the base64urlsafe encoding. This encoding is not natively done in SFDC and perscribes the removal of trailing padding characters from the base64 string. Luckily, my original username encoded with no padding characters in the claim set. With the new username, the padding characters are present and must be removed before signing.
It all comes down to just a few characters.
I am using official .net api client to send emails with attachments by messages.send method. When I attach a file of size more than approximately 5mb, I've come to
[JsonReaderException: Unexpected character encountered while parsing value: <. Path '', line 0, position 0.]
Newtonsoft.Json.JsonTextReader.ParseValue() +1187
Newtonsoft.Json.JsonTextReader.ReadInternal() +65
Newtonsoft.Json.JsonTextReader.Read() +28
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter) +237
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) +783
Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) +293
Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings) +274
Newtonsoft.Json.JsonConvert.DeserializeObject(String value, JsonSerializerSettings settings) +57
Google.Apis.Json.NewtonsoftJsonSerializer.Deserialize(String input) in c:\code\google.com\google-api-dotnet-client\default\Tools\Google.Apis.Release\bin\Debug\test\default\Src\GoogleApis.Core\Apis\Json\NewtonsoftJsonSerializer.cs:120
Google.Apis.Services.<DeserializeError>d__8.MoveNext() in c:\code\google.com\google-api-dotnet-client\default\Tools\Google.Apis.Release\bin\Debug\test\default\Src\GoogleApis\Apis\Services\BaseClientService.cs:286
[GoogleApiException: An Error occurred, but the error response could not be deserialized]
Google.Apis.Requests.ClientServiceRequest`1.Execute() in c:\code\google.com\google-api-dotnet-client\default\Tools\Google.Apis.Release\bin\Debug\test\default\Src\GoogleApis\Apis\Requests\ClientServiceRequest.cs:102
I think client use Metadata URI, for metadata-only requests. A am going to try another option: Upload URI, for media upload requests.
It looks like there is a limit on email size that leads to exception of parsing error response in the client library.
So, the first question: is there a size limit?
Second, there is no info about how to use different upload methods via client, do you know any client library documentation?
Update: I hacked that request produced by
var request = gmailService.Users.Messages.Send(message, AccountUserId);
is going to https://www.googleapis.com/gmail/v1/users/me/messages/send. As I supposed it didn't use media upload request.
I ended up with limit on attachments total size. Here is code snippet.
Class level:
public const int MaxAttachmentsSize = 5 * 1024 * 1024;
Method level:
var attachmentsSize = 0;
foreach (var attachment in attachments)
{
attachmentsSize += attachment.Item1;
if (attachmentsSize > MaxAttachmentsSize) break;
mailMessage.Attachments.Add(attachment.Item2);
}
There is limit in MB of whole message. Google API allows you for quite big email but it may get timeout when sending if your service because of connection speed will be doing it too long.
According to this google docs it is 35MB:
google api send docs
For anything (uploading) over a few MB you should definitely use the /upload version of the method, otherwise yes you may run into those size limitations.
In response to the second part of your question...
Second, there is no info about how to use different upload methods via client, do you know any client library documentation?
I did a little poking around in the API, and I see that there is also a method that takes a stream as the 3rd parameter.
services.Users.Messages.Send( body, userId, stream, contentType)
Digging into the source code of that, I see that it seems to use a URL that looks like:
upload/....
I haven't tried it yet, and I don't know (yet) what it wants for a "stream", but this looks like a good possibility for getting a resumable upload with a bigger limit.