16

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 = '[email protected]';
    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;
}
}
4

4 Answers 4

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.

10
  • 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
    Commented Jun 28, 2013 at 18:55
  • No problem, I've been scratching my head with it for a while myself. Commented Jun 28, 2013 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
    Commented May 22, 2014 at 2:03
  • Any idea what the algorithm name is @identigral? Commented May 22, 2014 at 10:22
  • @PhilHawthorn rsa-sha256
    – identigral
    Commented May 23, 2014 at 7:00
22

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 "[email protected]"
    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": "[email protected]",
      "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]

8
  • Hey Andy, did you have any issues with an "invalid_grant" type when using this?
    – Chris
    Commented Oct 30, 2015 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
    Commented Nov 3, 2015 at 16:09
  • 1
    hey @Chris - let me know if there's a update I need to make to the above instructions.
    – Andy H.
    Commented Nov 3, 2015 at 23:05
  • Hugely helpful for us today! Commented Dec 3, 2015 at 18:34
  • I will be thankful all my life to Andy H. for this comment. You saved my soul. Commented Apr 26, 2016 at 14:02
13

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);
}
10
  • Did you use a self signed certificate? Commented Feb 26, 2018 at 12:31
  • 1
    Nice, this will definitely be the preferred way going forward! Thanks for the help, this should be marked as the best answer now. Commented Feb 27, 2018 at 4:07
  • 1
    Hi, thank you for your answer, that's exactly what I was looking for. Google has updated the format of Service Account keys from P12 to JSON; do you know if keytool can be used to convert Google's JSON format to JKS? Thank you! Commented Jul 9, 2019 at 12:43
  • 1
    Can someone shed more light on how to create the jks file? I am trying to authenticate Goole service from Salesforce. I have the JSON file from Google service account with attributes like type, project_id, private_key, client_email, auth_provider_x509_cert_url, client_x509_cert_url etc. Commented Apr 9, 2020 at 22:17
  • 1
    @AlexFragotsis Just export a P12 key. When you add a key in the console, it asks you what format you want. Also see webapps.stackexchange.com/questions/58411/….
    – jon_wu
    Commented Nov 27, 2020 at 22:36
5

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.

1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .