Connect apex and Google API using JWT to retrieve Oauth 2.0 token
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):
- Set up an app in Google's Developer Console. Make sure the API you want to use is turned on for this App.
- 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
- On Credentials page, create a new oAuth Client ID, use Service Account type
The credentials you will need are the:
- EMAIL ADDRESS "[email protected]"
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" }
Then, in Salesforce here is the Apex class code you need to get the
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]
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);
}
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.