See all Insights

Connecting Salesforce and QuickBooks with oAuth

We were recently challenged with implementing a connection between Salesforce and the QuickBooks Online API. Specifically, we were interested in connecting to QuickBooks’ Accounting API in order to integrate the invoicing creation and tracking process with some of our internal systems. Since there was little in the way of documentation for doing this, I’m sharing my experiences here in hopes of helping others.

If you’re looking for a working solution (in both PHP and Apex), feel free to skip to the code. Otherwise, read on for some notes on the process.

oAuth

The hardest part of getting this all working, for me, was implementing the three-legged oAuth 1.0 authentication process that the QuickBooks API uses. Without getting into the full spec, an oAuth process is a series of secure tokens passed back and forth between two applications, with the end result being a signature specific to the current user session. This key then allows the user (and only that user) to make API requests. A three-legged authorization is the most complex of way of doing this. If you’ve ever seen a popup reading “Please sign into your cat-rank.com account to allow this service to access your data,” you’ve likely experienced this process.

The general workflow looks something like the following. (I use the term “client application” to refer to whatever it is you’re building, and “host application” to mean the service you’re trying to authenticate with, such as Quickbooks.)

  1. Using a secret, unique key, the client application requests a one-time temporary token from the host application.
  2. This temporary token is returned to the client, which is then combined with the first key. This new, combined token is redirected along with the user to a login screen on the host application.
  3. If this combined token checks out, the user is shown a screen to log in to the host application if they are not logged in.
  4. The user is then prompted by the host application to confirm the connection, usually by clicking a button.
  5. The host application then redirects the user back to the client application, with a new temporary one-time token.
  6. The client application receives the redirected user back, and then combines this new one-time token with another secret unique key, and makes a request back to the host application for the final session token.
  7. This final, authenticated session token is then stored somewhere in the client application, associated with the current user. Every API request that particular user makes is signed with that unique token.

The tricky part, at least for me, was the fact that every request must be encoded, combined and serialized in a very specific order, otherwise the host application will reject it. Most languages have built-in tools or third-party libraries that can be used to simplify this workflow during development. To my knowledge, however, Salesforce does not and instead requires manually constructing each request and parsing each response.

To make sure I was getting all of this encoding right, I first went through the exercise of building a complete connection in PHP, which for me is easier/quicker/dirtier than developing and deploying in Apex to force.com. Since my goal was to understand exactly what was being passed and when, I didn’t use the built in PHP oAuth functions, and instead encoded and sent everything manually.

The process looked something like this.

PHP Example

The first connection takes the initial CLIENT_KEY and builds the request. Note all request parameters: These are all required and must be set in an exact order. They are then combined, URL-encoded and base64-encoded to form the first request token. The order of the encoding here is very important, as any discrepancy between the key that we generate, and the key that the host application thinks we should have generated based on the list of parameters, will result in a failure.

    $url 	= OAUTH_REQUEST_URL;
    $fields = array( 'oauth_consumer_key' 	 => CLIENT_KEY,
         'oauth_nonce'  => time(),
         'oauth_signature_method' => 'HMAC-SHA1',
         'oauth_timestamp'	  => time(),
         'oauth_version'  => '1.0',
        'oauth_callback'  => CALLBACK_URL );

    ksort($fields);

    $sorted_fields = array();

    foreach ($fields as $key => $value)  {
        $sorted_fields[] = rawurlencode($key) . '=' . rawurlencode($value);
    }

    $string_fields = implode('&', $sorted_fields);
    $signature_data = strtoupper('POST') . '&' . rawurlencode(OAUTH_REQUEST_URL)  . '&' . rawurlencode($string_fields);
    $key = rawurlencode(CLIENT_SECRET) . '&';
    $signature = base64_encode(hash_hmac('SHA1', $signature_data, $key, 1));

    $fields['oauth_signature'] = $signature;

    $fields_string = '';

    foreach( $fields as $key => $value )  {
        $fields_string .= $key . '=' . $value . '&'; 
    }

    rtrim($fields_string, '&');

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_POST, count($fields));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
    $response = curl_exec($ch);
    $headerSent = curl_getinfo($ch, CURLINFO_HEADER_OUT );
    curl_close($ch);

If that works, we then redirect the user to the host application. The user is prompted to sign in (if they are not already signed in) and then asked to confirm the connection.

    $_SESSION['secret'] = $request_token['oauth_token_secret'];
    session_write_close();

    header('Location: '. OAUTH_AUTHORISE_URL .'?oauth_token='.$request_token['oauth_token']);


Once the user has authorized there, they’ll be sent back to the URL that we specified in step 1.

Then, we sign another request, using the values we were just given from the redirect:

    $url 	= OAUTH_ACCESS_URL;
    $fields = array(	'oauth_consumer_key'  => CLIENT_KEY,
        'oauth_nonce'  => time(),
        'oauth_signature_method' => 'HMAC-SHA1',
        'oauth_timestamp'	 => time(),
        'oauth_version'  => '1.0',
        'oauth_callback' => CALLBACK_URL,
        'oauth_token' => $_GET['oauth_token'],
        'oauth_verifier' => $_GET['oauth_verifier'] );

    ksort($fields);

    $sorted_fields = array();

    foreach ($fields as $key => $value)  {
        $sorted_fields[] = rawurlencode($key) . '=' . rawurlencode($value);
    }

    $string_fields = implode('&', $sorted_fields);
    $signature_data = strtoupper('POST') . '&' . rawurlencode(OAUTH_ACCESS_URL)  . '&' . rawurlencode($string_fields);
    $key = rawurlencode(CLIENT_SECRET) . '&' . rawurlencode($_SESSION['secret']);
		
    $signature = base64_encode(hash_hmac('SHA1', $signature_data, $key, 1));

    $fields['oauth_signature'] = $signature;

    $fields_string = '';

    foreach( $fields as $key => $value )  {
        $fields_string .= $key . '=' . $value . '&'; 
    }

    rtrim($fields_string, '&');

    $ch = curl_init($url);
    curl_setopt($ch,  CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_POST, count($fields));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
    $response = curl_exec($ch);
    $headerSent = curl_getinfo($ch, CURLINFO_HEADER_OUT );
    curl_close($ch);

Finally, if everything worked, we are given the user’s final access token, to be used for all subsequent API calls. I’m storing it here as a session value for testing purposes.

    $_SESSION['realmId'] = $_GET['realmId'];
    $_SESSION['dataSource'] = $_GET['dataSource'];
    
    $_SESSION['oauth_token'] 	= $request_token['oauth_token'];
    $_SESSION['oauth_token_secret'] = $request_token['oauth_token_secret'];
    session_write_close();

Now, we can test making an actual API call, such as requesting the details of an invoice by ID. First, we need to build a request signature for this specific, one-time API call by the current user:

    $url = 'https://sandbox-quickbooks.api.intuit.com/v3/company/123456/invoice/123;

    $fields = array(	'oauth_consumer_key' 	 => CLIENT_KEY,
        'oauth_nonce'  => time(),
        'oauth_signature_method' => urlencode( 'HMAC-SHA1' ),
        'oauth_timestamp'	 => time(),
        'oauth_version'  => '1.0',
        'oauth_token'  => urlencode( $_SESSION['oauth_token'] ));

    ksort($fields);

    $sorted_fields = array();

    foreach ($fields as $key => $value) 
    {
        $sorted_fields[] = rawurlencode($key) . '=' . rawurlencode($value);
    }

    $string_fields = implode('&', $sorted_fields);
    $signature_data = strtoupper('GET') . '&' . rawurlencode($url)  . '&' . rawurlencode( $string_fields );
    $key = rawurlencode(CLIENT_SECRET) . '&' . rawurlencode($_SESSION['oauth_token_secret']);
    $signature = urlencode( base64_encode(hash_hmac('SHA1', $signature_data, $key, 1)) );


We then send that signature along with the API call to authenticate it.

    $ch = curl_init($url);

    $header   = array();
    $header[] = 'Accept: application/json';
    $header[] = 'Authorization: OAuth oauth_token="' . $fields['oauth_token'] . '",oauth_nonce="' . $fields['oauth_nonce'] . '",oauth_consumer_key="' . $fields['oauth_consumer_key'] . '",oauth_signature_method="' . $fields['oauth_signature_method'] . '",oauth_timestamp="' . $fields['oauth_timestamp'] . '",oauth_version="' . $fields['oauth_version'] . '",oauth_signature="' . $signature . '"';

    curl_setopt($ch, CURLOPT_HTTPHEADER,		$header);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $response = curl_exec($ch);
    curl_close($ch);

    print_r(json_decode($response,1));
    exit;

And that’s it! Easy enough, right? The hardest part for me was getting the order of the fields and encoding correct, both as they are passed and what is encoded before what.

Salesforce Example

Once that is sorted (ha), is relatively easy to implement the same workflow in Apex.

First, we get the initial temporary token, and redirect the user to the QuickBooks login/verification screen:

/* =============================================================================
//
//    Function: OAuth_Step1_getTempTokenAndRedirectToIntuit    
//
/*=============================================================================*/
    public pagereference OAuth_Step1_getTempTokenAndRedirectToIntuit() {
        
        serviceObject = getServiceSettings();
        
		String nonce = string.valueof(dateTime.now().getTime()/1000);
        String timestamp = string.valueof(dateTime.now().getTime()/1000);
 		
        Map<String,String> parameters = new Map<String,String>();
        parameters.put('oauth_callback', EncodingUtil.urlEncode(URL.getSalesforceBaseUrl().toExternalForm() + '/apex/QuickbooksConnect2', 'UTF-8'));
        parameters.put('oauth_consumer_key', serviceObject.Consumer_Key__c);
        parameters.put('oauth_nonce', nonce);
        parameters.put('oauth_signature_method', 'HMAC-SHA1');
        parameters.put('oauth_timestamp', timestamp);
        parameters.put('oauth_version', '1.0');

        HttpRequest req = new HttpRequest();
        HttpResponse res;
         
        req.setEndpoint(serviceObject.Request_Token_URL__c);
        req.setMethod('POST');   
        		 
        String signature = generateSignature(req, serviceObject.Consumer_Secret__c, '', parameters);
		
		String body = 'oauth_callback=' + URL.getSalesforceBaseUrl().toExternalForm() + '/apex/QuickbooksConnect2' + '&';
		body += 'oauth_consumer_key=' + serviceObject.Consumer_Key__c + '&';
		body += 'oauth_nonce=' + nonce + '&';
		body += 'oauth_signature_method=HMAC-SHA1&';
		body += 'oauth_timestamp=' + timestamp + '&';
		body += 'oauth_version=1.0&';
		body += 'oauth_signature=' + signature;
       		 req.setBody(body);
		
		String authToken = '';
		
        try {
        	
        	Map<String,String> responseItems = getResponseNVP( req );

        	serviceObject.Temporary_Token_Secret__c = responseItems.get('oauth_token_secret');
        	update serviceObject;
        
        	authToken = responseItems.get('oauth_token');
        
        } catch(Exception e) {
    		System.debug(e.getMessage());        
    	}
    	
    	String redirectUrl = 'https://appcenter.intuit.com/Connect/Begin?oauth_token=' + authToken;
        	pagereference redirect = new PageReference( redirectUrl );
        	return redirect.setRedirect(true);
    }

Then, when the user returns, we get their final token and finally redirect them to our test page:

/*=============================================================================
//
//    Function: OAuth_Step2_getFinalToken    
//
/*=============================================================================*/
    public pagereference OAuth_Step2_getFinalToken() {
        
        serviceObject = getServiceSettings();
        
		String nonce = string.valueof(dateTime.now().getTime()/1000);
        String timestamp = string.valueof(dateTime.now().getTime()/1000);
 		
 		String tokenParm       = apexpages.currentpage().getparameters().get('oauth_token');
        String tokenVerifier   = apexpages.currentpage().getparameters().get('oauth_verifier');
 		
        Map<String,String> parameters = new Map<String,String>();
        parameters.put('oauth_callback', EncodingUtil.urlEncode(URL.getSalesforceBaseUrl().toExternalForm() + '/apex/QuickbooksConnect2', 'UTF-8'));
        parameters.put('oauth_consumer_key', serviceObject.Consumer_Key__c);
        parameters.put('oauth_nonce', nonce);
        parameters.put('oauth_signature_method', 'HMAC-SHA1');
        parameters.put('oauth_timestamp', timestamp);
        parameters.put('oauth_token', tokenParm);
        parameters.put('oauth_verifier', tokenVerifier);
        parameters.put('oauth_version', '1.0');
    
    	Http http       = new Http();
        HttpRequest req = new HttpRequest();
         
        req.setEndpoint(serviceObject.Access_Token_URL__c);
        req.setMethod('POST');   
        		 
        String signature = generateSignature(req, serviceObject.Consumer_Secret__c, serviceObject.Temporary_Token_Secret__c, parameters);
		
		String body = 'oauth_callback=' + URL.getSalesforceBaseUrl().toExternalForm() + '/apex/QuickbooksConnect2' + '&';
		body += 'oauth_consumer_key=' + serviceObject.Consumer_Key__c + '&';
		body += 'oauth_nonce=' + nonce + '&';
		body += 'oauth_signature_method=HMAC-SHA1&';
		body += 'oauth_timestamp=' + timestamp + '&';
		body += 'oauth_version=1.0&';
		body += 'oauth_token=' + tokenParm + '&';
		body += 'oauth_verifier=' + tokenVerifier + '&';
		body += 'oauth_signature=' + signature;		
        req.setBody(body);

        try {
	    
			Map<String,String> responseItems = getResponseNVP( req );

	        outputString += JSON.serialize( responseItems );
		
			serviceObject.OAuth_Token__c = responseItems.get('oauth_token');
	        serviceObject.OAuth_Token_Secret__c = responseItems.get('oauth_token_secret');
	        update serviceObject;
	    
	    } catch(Exception e) {
    		System.debug(e.getMessage());        
    	}
    	
	    String redirectUrl = URL.getSalesforceBaseUrl().toExternalForm() + '/apex/QuickbooksConnectTest';
	    pagereference redirect = new PageReference( redirectUrl );
	    redirect.setRedirect(true);
    	
    	return redirect;
    }

Where, finally, we can make a test API request (again, for an invoice by ID):

/*=============================================================================
//
//    Function: testAPICall    
//
/*=============================================================================*/
    public void testAPICall() {
		
	String endpoint = 'https://sandbox-quickbooks.api.intuit.com/v3/company/123456/invoice/123;

		Http http = new Http();
        HttpRequest req = new HttpRequest();
		
        req.setEndpoint(endpoint);
        req.setMethod('GET');   
        req = signRequest(req);

        HttpResponse res;
        res = http.send(req); 
        String resParams = res.getBody();

        outputString +=resParams;    
    }

In each of these examples, I’m doing the grunt work of building the signatures and headers in some utility functions, using this two-legged oAuth example as a starting point:

/*=============================================================================
//
//    Function: signRequest    
//
/*=============================================================================*/
    public static HttpRequest signRequest(HttpRequest req) {

		serviceObject = getServiceSettings();

        String nonce     = string.valueof(dateTime.now().getTime()/1000);
        String timestamp = string.valueof(dateTime.now().getTime()/1000);
 
        Map<String,String> parameters = new Map<String,String>();
        parameters.put('oauth_consumer_key', serviceObject.Consumer_Key__c);
        parameters.put('oauth_nonce', nonce);
        parameters.put('oauth_signature_method', 'HMAC-SHA1');
        parameters.put('oauth_timestamp', timestamp);
        parameters.put('oauth_token', EncodingUtil.urlEncode(serviceObject.OAuth_Token__c, 'UTF-8'));
        parameters.put('oauth_version', '1.0');
 
        String signature = generateSignature(req, serviceObject.Consumer_Secret__c, serviceObject.OAuth_Token_Secret__c, parameters);
        String header = generateHeader(signature, parameters);
        req.setHeader('Authorization', header);
 
        return req;
    }

/*=============================================================================
//
//    Function: generateHeader    
//
/*=============================================================================*/
    private static String generateHeader(String signature, Map<String,String> parameters) {
        String header = 'OAuth ';
        for (String key : parameters.keySet()) {
            header = header + key + '="'+parameters.get(key)+'", ';
        }
        return header + 'oauth_signature="' + signature + '"';
    }
 
/*=============================================================================
//
//    Function: generateSignature    
//
/*=============================================================================*/
    private static String generateSignature(HttpRequest req, String consumerSecret, String tokenSecret, Map<String,String> parameters) {
        String s 	= createBaseString(req, parameters);        
        String key  = EncodingUtil.urlEncode(consumerSecret, 'UTF-8') + '&' + EncodingUtil.urlEncode(tokenSecret, 'UTF-8');
 
        Blob sig = Crypto.generateMac(
           'HmacSHA1'
          , Blob.valueOf(s)
          , Blob.valueOf(key)
        );
        return EncodingUtil.urlEncode( EncodingUtil.base64encode(sig), 'UTF-8');
    }

Integrating this into our existing systems is an ongoing process, but so far we’ve made a few important decisions regarding how this will work.

Once the authentication has been established, the actual API calls are relatively simple to implement, provided the header signature is signed correctly. For any of this to work, a new “Remote Site” entry needs to be created for the base endpoint https://sandbox-quickbooks.api.intuit.com/.

Since not every user of our internal systems would have a QuickBooks login with which to authenticate, it made sense for us to modify the workflow so that an oAuth token is generated only once and then stored in our system for all future API calls. Generated tokens expire after 180 days, so the application needs to check the stored token and prompt for re-authentication when needed. In our case, we’re sending an alert notification to the Salesforce admin when this happens.

We’re storing the token in a custom object, and making all the QuickBooks API calls via triggers on other custom objects. Most of the users don’t need to have access to the token object at all, which in our case is good, as we’d already hit the 10-custom-objects-per-communities-user limit.

Have you run into any other issues integrating Salesforce with the QuickBooks API? Any workarounds that would be good to know? Share your experiences in the comments below.

Download The Code

More Resources



 

Related Posts