0 votes
1 view
in Salesforce by (11.9k points)

What I am trying to accomplish is to be able to upload data from Force.com into a Google Fusion Table held under the service account that has already been set up so that a network graph visualization can be generated based on that data and shown in a VF page within Force.com.

I have written an Apex class that consists of the following:

public class NetworkGraphTestController {

    public static String base64URLEncode(Blob input) {

        String output = encodingUtil.base64Encode(input);

        output = output.replace('+', '-');

        output = output.replace('/', '_');

        while (output.endsWith('=')) {

            output = output.subString(0, output.length()-1);

        }

        return output;

    }

    public static Long findSecondsBetween2DateTimes(DateTime dt1, DateTime dt2)

    {

        Integer intDays = dt1.Date().daysBetween(dt2.Date());

        Long seconds = dt2.getTime() - dt1.getTime();

        Long daysToSeconds = intDays * 24 * 60 * 60;

        return daysToSeconds + seconds;

    }

    public static void generateJWT() {

        String pkCS8PrivateKey = 'PRIVATE KEY OBTAINED VIA OPENSSL';

        DateTime utc0 = DateTime.newInstance(1970, 1, 1, 0, 0, 0);

        DateTime issueTime = DateTime.now();

        JSONGenerator gen = JSON.createGenerator(false);

        gen.writeStartObject();

        gen.writeStringField('iss', '[email protected]account.com');

        gen.writeStringField('scope', 'https:\\/\\/www.googleapis.com\\/auth\\/fusiontables');

        gen.writeStringField('aud', 'https:\\/\\/accounts.google.com\\/o\\/oauth2\\/token');

        Long now = findSecondsBetween2DateTimes(utc0, issueTime);

        gen.writeNumberField('exp', now + 3500);

        gen.writeNumberField('iat', now);

        String claimSet = gen.getAsString().trim();

        System.debug(gen.getAsString());

        System.debug('Gen trimmed: ' + gen.getAsString().trim());

        String header = '{"alg":"RS256", "typ":"JWT"}';

        String signatureInput = base64URLEncode(blob.valueOf(header)) + '.' + base64URLEncode(blob.valueOf(claimSet));

        Blob signature = Crypto.sign('RSA', blob.valueOf(signatureInput), encodingUtil.base64decode(pkCS8PrivateKey));

        String jwt = signatureInput+'.'+base64URLEncode(signature);

        //System.debug(jwt);

        Http h = new Http();

        HttpRequest req = new HttpRequest();

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

        req.setMethod('POST');

        req.setBody('grant_type=' + encodingUtil.urlEncode('urn:ietf:params:oauth:grant-type:jwt-bearer', 'UTF-8')+'&assertion=' + encodingUtil.urlEncode(jwt, 'UTF-8'));

        req.setEndpoint('https://accounts.google.com/o/oauth2/token');

        System.debug('Request: ' + req + ' BODY: ' + req.getBody());

        //System.debug(req.toString());

        HttpResponse res = h.send(req);

        System.debug(res + ' BODY: ' + res.getBody() + ' STATUS: ' + res.getStatus());

    }

}

The service account has already been set up and the Fusion Tables API has been enabled in the Google API Console. I extracted the PKCS8 key from the PKCS12 p12 private key file that Google provided via OpenSSL, though I am not familiar with this process, so it is possible a mistake was made. What I did notice was that through my many tries to extract the value in that private key, the Crypto class didn't accept the key-value until I got the value that is currently stored in pkCS8PrivateKey. If I remember the process correctly, it first converted the PKCS12 file to an intermediate pem file and then it extracted the private key in the PKCS8 format which was outputted in another pem file.

The body of the 400 'Bad Request' error I am encountering is as follows:

{

     "error" : "invalid_grant"

}

I have tried not escaping the forward slashes in the iss and scope parameters as well but that makes no difference. I have also allowed access to 'https://accounts.google.com' via the Remote Site Security settings.

My biggest source for writing this code is this question: Force.com Apex Code to generate Google API oAuth 2.0 JWT

The creator of that question ended up in the same boat I am now, in that he is only able to retrieve an invalid_grant error upon trying to authenticate with Google. Unfortunately, his question was left unanswered and so, here I am.

I appreciate any help that can be given in regards to the problem I am facing.

1 Answer

0 votes
by (30.1k points)

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.

Related questions

Welcome to Intellipaat Community. Get your technical queries answered by top developers !


Categories

...