14

I have been trying to connect Salesforce and Google API using server to server application.

Intent :- To communicate data between Salesforce and Google Spreadsheet which is hosted in Google Sites and using some Google Forms, Google scripts and triggers to update data.

So, for doing this I need a server to server application model from Google as it wouldnt require a consent from user while its communicating data. for doing this I need to create a JWT and pass it to Google to get the token and proceed with the next steps.

JWT for Google requires a RSA with SHA256 signing of the msg using the private key that google has provided in the certificate when I created the server to server application in Google.

I havent found this info anywhere till now :(

In salesforce, we have a crypto class in which we can sign with RSA SHA1 (or) generate digest with SHA256, I tried generating a digest using SHA256 and signing that digest usng RSA and the private key given by google. Even thats not working.

Google isnt accepting my assertion values (JWT) and its returning an invalid Grant message.

I have seen that Jeff douglas has posted some information about this here :- http://blog.jeffdouglas.com/2010/07/06/using-rsa-sha1-with-salesforce-crypto-class/

But unfortunately he has mentioned about domain model and asking us to upload a certificate to google, which i dont want to do or which is not my scenario.

Also Google requires a UT8 base64 URL encoded value at all places as mentioned in this link :- https://developers.google.com/accounts/docs/OAuth2ServiceAccount#libraries But salesforce has a separate URL encoding and a separate base64encoding method available in the EncodingUtil class.

I have a C# dotnet application which is generating the same value and its able to hit Google and get the value properly. But uses the certificate file and gets the privatekey directly. I used openssl to retrieve the privatekey from the .p12 certificate file provided by Google and have pasted it in my code. I am sure there is some problem in the signing part, because when I compare the values generated by my .NET application and Salesforce Apex code, its returning correct values, but when it comes to the signature part, the length is also same for the returned data (signature) from both .NET and SF but Google returns an invalid grant while calling from SF but returns a bearer token when I call it from my .NET application.

Here is my code :-

    public class TestRestAPICall
{
    public class JWTClaimSet
    {
       public string iss {get;set;}
       public string scope {get;set;}
       public string aud {get;set;}
       public Long exp {get;set;}
       public Long iat {get;set;}
       //public string prn {get;set;}
    }

//@future (callout=true)
public static void LoginToGoogle()
{
    //Set your username and password here        
    String clientId = '851234545868.apps.googleusercontent.com';

    //Construct HTTP request and response
    Http http = new Http();
    HttpRequest req = new HttpRequest();
    HttpResponse res = new HttpResponse();


    String JWTHeader =  '{"typ":"JWT","alg":"RS256"}';
    //String Base64EncodedJWTHeader = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9'; //To encode later using code

    //Taken from .net application
// Since the information is going to be same, I have encoded it already in .NET and using it here
    String Base64EncodedJWTHeader = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9';
// Salesforce returns time in milliseconds, so we are dividing it by 1000 to set the seconds value instead of milliseconds value
    Long expires_at = math.roundToLong(DateTime.now().addMinutes(40).getTime() / 1000);
    Long issued_at = math.roundToLong(DateTime.now().addSeconds(-2).getTime() / 1000);
    //Long issued_at = 1372276504;
    //Long Expires_at = 1372279804;

    JWTClaimSet JWT = new JWTClaimSet();
    //JWT.prn = username;
    JWT.iss = '851234545868@developer.gserviceaccount.com';
    JWT.scope = 'https://www.googleapis.com/auth/drive.file';
    JWT.aud = 'https://accounts.google.com/o/oauth2/token';
    JWT.iat = issued_at;
    JWT.exp = expires_at;

    String strJWTJSON = JSON.Serialize(JWT);
    system.debug('Unencoded claimset::'+strJWTJSON);


    Blob ClaimsetBlob = Blob.valueOf(strJWTJSON);
    String Base64EncodedClaimset = EncodingUtil.base64Encode(ClaimsetBlob);
    //Base64EncodedClaimset = PerformPostBase64Encode(Base64EncodedClaimset);
    system.debug('Base64 Encoded Claimset::'+Base64EncodedClaimset);

    // constructing the base64 encoded string to sign it
    string Base64EncodedString = Base64EncodedJWTHeader + '.' + Base64EncodedClaimset;


    // Steps to sign the base64Encoded string
    String algorithmName = 'RSA';
    String key = 'MIICXAIBAAKBgQCi16h+5TeQU5Fo0DlR6+YmrzYXZ7DLxz+dBEnB8Hj0gznqlz8p7nQ7I4AV/SfiJQ6JbU16sKS5IW7Hob5ieW1DfwxYZeCSBPbEkt7eASrI8xqGU5RVewaQivY3vO+avgSSUT+ZU243XsDvZJQlkE3e46FhBvFedDQvuk2iEfgdxQIDAQABAoGAFaO882f4c0h3qUsKYvWLNxbPhFq2Js5KiM4aEximqi+KEb+ZmDPk5Dr6eXGTzDyKav7IbgZtTWDA/OxkhWeHelsMB9LqBq50L6hLHTK4hHecPrT3oN4GviUXh5y5Vt479A1TopjLKdt7V4AnAs0HEWJzar/euUa+T9eODPWPFP0CQQDWQeK2cqzWSVHUpkufp7a0Sc2RvfcIpOf8kRFBLnKiFGr7BscLz6qsaG1M8TyUNMrCquwLSNSDMvPjl6sCjgRPAkEAwpFx8+lspjN0yV5M5XHPmnoddTHwV/8QHoccMBBq0ZMFs2m1E/rFhwMHRBiFN6flbze8src7YnOmmtYqsGizqwJAfqoEtYel1ikST3zgSEqGIJ9hAEAlwt56pz27zaT/8AHSHQUstzbV14cE1u/muFddZyhU03cC62078djAKIp80QJAKMMT3ofOrVsmYnGRJpibZ7+hoEXgFm9nTx37N86YsmNc1GOW/iKRc2GdChUhA7H3DT/eForwtAWKp/Gqa97jlQJBAKjKOIKvdYS9fTpCzs1nUHg9rvVartRR5xxwLH57bBincuSJSBGjwd1FInAh2tgiUGPWGPsplShP87wao9+n9VQ=';
    Blob privateKey = EncodingUtil.base64Decode(key);

    Blob input = Blob.valueOf(Base64EncodedString);
    //Blob SHA256InputBlob = Crypto.generateDigest('SHA-256',input);

    Blob Blobsign = Crypto.sign(algorithmName, input , privateKey);


    // The following line is just for debugging and viewing the blob data in signature as string and its not used anywhere
    String signature = EncodingUtil.urlEncode(EncodingUtil.convertToHex(Blobsign),'UTF-8');

    system.debug('Unencoded signature ::'+signature);
    String base64EncodedSignature = EncodingUtil.base64Encode(Blobsign); 
    //base64EncodedSignature = PerformPostBase64Encode(base64EncodedSignature);
    system.debug('Base 64 encoded signature ::'+base64EncodedSignature );

    system.debug('Encoded assertion : ' + Base64EncodedString+'.'+base64EncodedSignature);

    string URLEncodedUTF8GrantType = encodingUtil.urlEncode('urn:ietf:params:oauth:grant-type:jwt-bearer','UTF-8');
    string URLEncodedUTF8Assertion = encodingUtil.urlEncode(Base64EncodedString+'.'+base64EncodedSignature,'UTF-8');        

    system.debug('URLEncodedUTF8GrantType : ' + URLEncodedUTF8GrantType);
    system.debug('URLEncodedUTF8Assertion : ' + URLEncodedUTF8Assertion);

    //Making the call out
    req.setEndpoint('https://accounts.google.com/o/oauth2/token');
    req.setMethod('POST');
    //req.setHeader('Content-Length', '-1');
    req.setHeader('Content-Type','application/x-www-form-urlencoded');
    //req.setHeader('grant_type',URLEncodedUTF8GrantType);
    //req.setHeader('assertion',URLEncodedUTF8Assertion);
    req.setBody('grant_type='+URLEncodedUTF8GrantType+'&assertion='+URLEncodedUTF8Assertion);
    res = http.send(req);
    system.debug('Response : '+res.getBody());
}

public static String PerformPostBase64Encode(String s)
{
    s = s.Replace('+', '-');
    s = s.Replace('/', '_');
    s = s.Split('=')[0]; // Remove any trailing '='s
    return s;
}
}
1

Unfortunately, I'm not sure that this is going to be possible using Apex. I created a test Java program (which works against the Google API) to observe the differences between what Java produces vs Apex. I noted the signatures generated from the two were different which narrowed it down to the output of the Crypto.sign() method.

I found this link which gives the following info:

The Apex Crypto class provides support for Digital Signatures with the sign() method. The following considerations apply:

  • The two algorithms are RSA and RSA-SHA1, which are functionally equivalent.
  • A PKCS8 formatted private key in base64 decoded form is required. This private key should not be hardcoded in the Apex script but should be stored in a protected custom setting or a encrypted fields in a custom table.
  • It is equivalent to the Java Signature.sign() class method using "SHA1withRSA".
  • In C#, it is the equivalent of (1) signing the clear text using SHA1Managed.ComputeHash() and (2) Signing using RSACryptoServiceProvider.ComputeHash() against the resulting hash.
  • Functionally, it will compute a SHA1 digest from clear text and encrypt the digest using RSA with the provided private key.

I have highlighted the key issue here, I believe you need the equivalent of SHA256withRSA which does not seem to be an option with the Crypto class (at least not that I can figure out).

So, in summary I think your code is correct but the signature being generated is not.

|improve this answer|||||
  • yes @Phil this is exactly what am asking as well. Salesforce doesnt provide an RSA with SHA256. Even if I try to create an SHA256 message digest and pass that digest as input to crypto.sign() by passing Algorithm name as RSA, it does an RSA-SHA1 on it again, so my purpose is defeated here. So I dont think its possible unless salesforce comes up with some solution for this. I guess thats the reason while implementing SAML and Federated authentication, salesforce asks us to choose encryption mechanism as SHA1 in Windows ADFS certificate issuer. Thanks for your research and explanations though!! – Sathya Jun 28 '13 at 18:55
  • No problem, I've been scratching my head with it for a while myself. – Phil Hawthorn Jun 28 '13 at 18:58
  • It will work in Summer '14, Crypto class will do RSA-256. If you sign up for a pre-release org, you can try it. salesforce.com/form/signup/prerelease-summer14.jsp – identigral May 22 '14 at 2:03
  • Any idea what the algorithm name is @identigral? – Phil Hawthorn May 22 '14 at 10:22
  • @PhilHawthorn rsa-sha256 – identigral May 23 '14 at 7:00
16

Just want to update this answer with the latest process, this is built referencing the Google Service Account oAuth instructions (look at REST code) (credit to Jai-Singh for original Salesforce code):

  1. Set up an app in Google's Developer Console. Make sure the API you want to use is turned on for this App.
  2. You will need the "scope" URL for the specific Google API you are going to access. e.g. https://www.googleapis.com/auth/analytics.readonly
  3. On Credentials page, create a new oAuth Client ID, use Service Account type
  4. The credentials you will need are the:

    1. EMAIL ADDRESS ".......@developer.gserviceaccount.com"
    2. KEY: Click "Generate New JSON Key" and in the downloaded file extract the private_key value being sure to remove any "\n" instances from key.

      {
      "private_key_id": ".....",
      "private_key": "-----BEGIN PRIVATE KEY-----[KEY HERE, REMOVE "\n" LINEBREAKS]-----END PRIVATE KEY-----\n",
      "client_email": ".....@developer.gserviceaccount.com",
      "client_id": "....apps.googleusercontent.com",
      "type": "service_account"
      }
      
  5. Then, in Salesforce here is the Apex class code you need to get the

access token:

public String get_access_token(){

Http h = new Http();
HttpRequest req = new HttpRequest();
HttpResponse res = new HttpResponse();
req.setEndpoint('https://accounts.google.com/o/oauth2/token');
req.setMethod('POST');

req.setHeader('ContentType','application/x-www-form-urlencoded');

String header = '{"alg":"RS256","typ":"JWT"}';
String header_encoded = EncodingUtil.base64Encode(blob.valueof(header));

String claim_set = '{"iss":"[EMAIL ADDRESS GOES HERE]"';
claim_set += ',"scope":"[URL SCOPE OF GOOGLE API GOES HERE]"';
claim_set += ',"aud":"https://accounts.google.com/o/oauth2/token"';
claim_set += ',"exp":"' + datetime.now().addHours(1).getTime()/1000;
claim_set += '","iat":"' + datetime.now().getTime()/1000 + '"}';

String claim_set_encoded = EncodingUtil.base64Encode(blob.valueof(claim_set));

String signature_encoded = header_encoded + '.' + claim_set_encoded;

String key = '[KEY GOES HERE]';

blob private_key = EncodingUtil.base64Decode(key);
signature_encoded = signature_encoded.replaceAll('=','');
String signature_encoded_url = EncodingUtil.urlEncode(signature_encoded,'UTF-8');
blob signature_blob =   blob.valueof(signature_encoded_url);

String signature_blob_string = EncodingUtil.base64Encode(Crypto.sign('RSA-SHA256', signature_blob, private_key));

String JWT = signature_encoded + '.' + signature_blob_string;

JWT = JWT.replaceAll('=','');

String grant_string= 'urn:ietf:params:oauth:grant-type:jwt-bearer';
req.setBody('grant_type=' + EncodingUtil.urlEncode(grant_string, 'UTF-8') + '&assertion=' + EncodingUtil.urlEncode(JWT, 'UTF-8'));
res = h.send(req);
String response_debug = res.getBody() +' '+ res.getStatusCode();
System.debug('Response =' + response_debug );
if(res.getStatusCode() == 200) {
    JSONParser parser = JSON.createParser(res.getBody());
    while (parser.nextToken() != null) {
        if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'access_token')) {
            // Move to the value.
            parser.nextToken();
            // Return the access_token
            return parser.getText();
        }
    }
}
return 'error';

This access token is valid for the next hour and can be used to sign API requests (easiest to just add as GET variable like this: http://googleapiurl/?params=whatever&acccess_token=[ACCESS_TOKEN_HERE]

|improve this answer|||||
  • Hey Andy, did you have any issues with an "invalid_grant" type when using this? – Chris Oct 30 '15 at 17:44
  • In case anyone comes back to this - I want to point out that the grant_string and the JWT string both need to be URL encoded in order for this to be successful. The message returned is not very helpful either - it returns a generic catch all message. Also be sure to remove brackets [] in all user input areas. – Chris Nov 3 '15 at 16:09
  • 1
    hey @Chris - let me know if there's a update I need to make to the above instructions. – Andy H. Nov 3 '15 at 23:05
  • Hugely helpful for us today! – Troy Harvey Dec 3 '15 at 18:34
  • I will be thankful all my life to Andy H. for this comment. You saved my soul. – Alexander Aeons Torn Apr 26 '16 at 14:02
11

As of Winter '17 there are new JWT methods baked in to Apex.

One hoop you have to jump through is getting your key from Google into a Java keystore (JKS) file. Some general info on getting a JKS file is in the Salesforce docs.

The following creates keystorefile.jks with a certificate named google_cloud, and password notasecret (what Google exports), which must match the P12 store password.

keytool -importkeystore -srckeystore private-key-from-google.p12 -destkeystore keystorefile.jks -srcstoretype pkcs12 -srcstorepass notasecret -deststorepass notasecret -deststoretype jks -destalias google_cloud -srcalias privatekey

Here's a similar example that shows getting an access token for Google Pub/Sub using a certificate called google_cloud that has been uploaded to Certificate Management as a Java KeyStore. I'm using the Org Cache to cache the credentials so that the token can be reused. Compared to using Custom Settings, you can update and clear the cache without DML, so there's no problem with updating the token between callouts.

I store the Google Cloud Project ID and Service Account ID in Custom Settings to keep them out of code.

private final static String AUTH_ENDPOINT = 'https://www.googleapis.com/oauth2/v4/token';
private final static String SCOPE = 'https://www.googleapis.com/auth/pubsub';
private final static String CACHE_ACCESS_TOKEN_KEY = 'googleCloudAccessToken';

public static String getAccessToken() {
    // Credentials are stored in Custom Settings
    GoogleCloud__c credentials = GoogleCloud__c.getOrgDefaults();
    String cachedAccessToken = (String) Cache.Org.get(CACHE_ACCESS_TOKEN_KEY);

    if (cachedAccessToken != null) {
        // Return the valid cached token from Custom Settings
        System.debug('Returning cached access token');
        return cachedAccessToken;
    } else {
        System.debug('No cached access token exists, fetching a new one');
        Auth.JWT jwt = new Auth.JWT();
        jwt.setAud(AUTH_ENDPOINT);
        jwt.setIss(credentials.ServiceAccountId__c);
        jwt.setAdditionalClaims(new Map<String, Object>{'scope' => SCOPE});

        // Create the object that signs the JWT bearer token with the certificate from Certificate Management
        Auth.JWS jws = new Auth.JWS(jwt, 'google_cloud');

        // POST the JWT bearer token.
        // Will throw a Auth.JWTBearerTokenExchange.​JWTBearerTokenExchangeException​ with the error in the message
        // the API fails to return the access token, the response is not in JSON format, or if the API
        // returns a non-200 response code.
        Auth.JWTBearerTokenExchange bearer = new Auth.JWTBearerTokenExchange(AUTH_ENDPOINT, jws);

        // Get the access token
        String token = bearer.getAccessToken();

        cacheAccessToken(token);

        return token;
    }
}

/**
 * Caches an access token in org cache so future API requests don't need to get a new token.
 * Compared to using Custom Settings, updating the org cache doesn't require a DML operation
 * and can be interleaved between callouts. The cache also handles expiration for us.
 */
private static void cacheAccessToken(String accessToken) {
    // Cache the access token in the default org cache with a TTL of 30 seconds less than its expiration
    Cache.Org.put(CACHE_ACCESS_TOKEN_KEY, accessToken, 3600 - 30);
}

/**
 * Proactively clear the access token from the Custom Settings cache.
 * This won't invalidate the previous access token on Google's side, but it will remove it from
 * our cache so that future requests will be forced to fetch a new token.
 */
public static Boolean clearAccessToken() {
    return Cache.Org.remove(CACHE_ACCESS_TOKEN_KEY);
}
|improve this answer|||||
  • Did you use a self signed certificate? – Scott Morrison Feb 26 '18 at 12:31
  • For which part? IIRC there was nothing custom, just using what Google gives you and getting it into a JKS format. – jon_wu Feb 26 '18 at 17:15
  • for the 'google_cloud' certificate, what did you use to get it into JKS format? – Scott Morrison Feb 26 '18 at 17:22
  • I've updated the answer with a few more things. – jon_wu Feb 26 '18 at 22:55
  • 1
    Nice, this will definitely be the preferred way going forward! Thanks for the help, this should be marked as the best answer now. – Scott Morrison Feb 27 '18 at 4:07
3

For a properly formatted JWT to be generated, it's important to note that this line of code:

JWT = JWT.replaceAll('=','');

Needs to be changed to this:

JWT = JWT.replaceAll('=',''); JWT = JWT.replaceAll('\\+','-'); JWT = JWT.replaceAll('\\/','_');

The reason is base64 can also produce a + or /. They need to be replaced with dash and underscore respectively. I have validated this with both PHP and NodeJS libraries.

|improve this answer|||||

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.