Connecting to Salesforce using OAuth 2.0 from PHP

Dancers

Introduction

Salesforce, with its rich set of APIs allows endless integration with 3rd party systems.

The key to accessing Salesforce from another system, be it a Web App, Mobile device or even a command line script, is logging on and obtaining a Session Token.

There a number of ways to do that. You could, for example, prompt for a Username, Password and Security token and use Salesforce’s Username-Password flow. However that would mean passing user credentials around and is not ideal. If you wanted to avoid the user having to repeatedly log on to Salesforce you would have to store the user credentials somewhere which would be even worse.

A better way of doing this is to use Salesforce’s Web Server Authentication Flow. This lets Salesforce handle the login process and sends back an Authentication Token from which you can request a Session Token. Better still, you can use this method to get a Refresh Token. The Refresh Token can then be stored or used later when the Session Token has expired in order to get a fresh Session Token without having to repeat the login process.

Warning: It’s very important that the Refresh Token (and to a certain extent the Session Token) is treated as if it were a password. Take steps to store it securely as anyone gaining access to this token could log on to your Salesforce org. The code in this example is for illustrative purposes, I don’t recommend spitting your tokens out to the screen.

The OAuth Dance

Sometimes referred to as the OAuth dance, this is the exchange of information between the consumer (in this case a PHP web server) and a provider (in this case Salesforce).

The following diagram outlines the steps involved in getting the all important Session Token:

OAuth Flow

Once you have you a Refresh Token you can hang on to it and use it for logging in again once your Session Token has expired. The process is then much simpler:

OAuth Refresh Token flow

PHP Example

I’ve chosen PHP, it’s fairly ubiquitous and I have it to hand but the steps involved apply to whatever language and framework you choose.

Secure Your Server!

It’s important that your PHP script is running on a secure server. Salesforce will only accept a call back to a URL over HTTPS. If you don’t have a secure server and simply want to try it out in an insecure and non-sensitive environment, I’ve described a Dropbox hack at the end. Doing so will transmit sensitive login information over an insecure connection. It is therefore not recommended, so use it at your own peril!

Create a Connected Application

Firstly you’ll need to login in to Salesforce and navigate to Setup->Create->Apps

Under Connected Apps, click New.

Fill in the mandatory fields.

Check the Enable OAuth Settings box.

In the Callback URL field, enter the URL of your PHP Script:

Eg: https://apps.myphpserver.com/TestOAUTH/sfdc_oauth_web.php

Under Available OAuth Scopes select:

Perform requests on your behalf at any time (refresh_token, offline_access)

Click Save and make a note of the generated Consumer Key and Consumer Secret.

Consumer App

The consumer app is a simple PHP web page that displays the connection state and provides buttons to step through the OAuth process:

OAuth Reset

The Reset button re-initialises the state to what you see above. passthroughState1 and 2 are simply there to demonstrate how to pass state through the authentication request and back. codeVerifier is 128 bytes of random data used to secure the authentication step.

Clicking Authenticate will redirect the user to the salesforce login page and prompt the user to Authenticate the connection before issuing a call back to the PHP script:

OAuth Authorise

OAuth Callback

You’ll see we now have a code passed back from Salesforce. This is the Authentication Code. Both passthroughState1 and 2 have also been sent back in a state parameter. You’ll see how this works in the code later.

Clicking Login via Authentication Code will take that code and attempt to login to Salesforce to get the Session Token and Refresh Token:

OAuth Code Login

You’ll see now we have a Session Token and Refresh Token. Using the Session Token we can now perform a request to get some data. Clicking the Get User button will use the REST API to get some user information:

Get User

Once the Session Token has expired we can use the Refresh Token to obtain a new Session Token without having to authenticate again. In a real world example you may cache this Refresh Token for later use. Clicking Login via Refresh Token with do this:

OAuth Refresh Login

PHP Script

Here’s the PHP Script in full. I’ll describe some of the salient points after. You will need to modify the getClientId() and getClientSecret() methods to to use the Consumer Key and Consumer Secret that belong to the Connected App you created earlier.

<?php
	// Report all errors (ignore Notices)
	error_reporting(E_ALL & ~E_NOTICE);
	ini_set('display_errors', 1);
	session_start();

	// Define our State class
	class State 
	{
		public $passthroughState1;	// Arbitary state we want to pass to the Authentication request
		public $passthroughState2;	// Arbitary state we want to pass to the Authentication request

		public $code;				// Authentication code received from Salesforce
		public $token;				// Session token
		public $refreshToken;		// Refresh token
		public $instanceURL;		// Salesforce Instance URL
		public $userId;				// Current User Id
		
		public $codeVerifier;		// 128 bytes of random data used to secure the request

		public $error;				// Error code
		public $errorDescription;	// Error description

		/**
		 * Constructor - Takes 2 pieces of optional state we want to preserve through the request
		 */
	    function __construct($state1 = "", $state2 = "")
	    {
	    	// Initialise arbitary state
	    	$this->passthroughState1 = $state1;
	    	$this->passthroughState2 = $state2;

	    	// Initialise remaining state
			$this->code = "";
			$this->token = "";
			$this->refreshToken = "";
			$this->instanceURL = "";
			$this->userId = "";
			
			$this->error = "";
			$this->errorDescription = "";

			// Generate 128 bytes of random data
			$this->codeVerifier = bin2hex(openssl_random_pseudo_bytes(128));
	    }

	    /**
	     * Helper function to populate state following a call back from Salesforce
	     */
	    function loadStateFromRequest()
	    {
	    	$stateString = "";

	    	// If we've arrived via a GET request, we can assume it's a callback from Salesforce OAUTH
	    	// so attempt to load the state from the parameters in the request
			if ($_SERVER["REQUEST_METHOD"] == "GET") 
			{
				$this->code = $this->sanitizeInput($_GET["code"]);
				$this->error = $this->sanitizeInput($_GET["error"]);
				$this->errorDescription = $this->sanitizeInput($_GET["error_description"]);
				$stateString = $this->sanitizeInput($_GET["state"]);

				// If we have a state string, then deserialize this into state as it's been passed
				// to the salesforce request and back
				if ($stateString)
				{
					$this->deserializeStateString($stateString);
				}
			}
	    }

	    /**
	     * Helper function to sanitize any input and prevent injection attacks
	     */
	    function sanitizeInput($data) 
	    {
  			$data = trim($data);
  			$data = stripslashes($data);
  			$data = htmlspecialchars($data);
  			return $data;
		}

		/**
		 * Helper function to serialize our arbitary state we want to send accross the request
		 */
		function serializeStateString()
		{
			$stateArray = array("passthroughState1" => $this->passthroughState1, 
								"passthroughState2" => $this->passthroughState2
								);

			return rawurlencode(base64_encode(serialize($stateArray)));
		}

		/**
		 * Helper function to deserialize our arbitary state passed back in the callback
		 */
		function deserializeStateString($stateString)
		{
			$stateArray = unserialize(base64_decode(rawurldecode($stateString)));

			$this->passthroughState1 = $stateArray["passthroughState1"];
			$this->passthroughState2 = $stateArray["passthroughState2"];
		}

		/**
		 * Helper function to generate the code challenge for the code verifier
		 */
		function generateCodeChallenge()
		{
			$hash = pack('H*', hash("SHA256", $this->generateCodeVerifier()));

			return $this->base64url_encode($hash);
		}

		/**
		 * Helper function to generate the code verifier
		 */
		function generateCodeVerifier()
		{
			return $this->base64url_encode(pack('H*', $this->codeVerifier));
		}

		/**
		 * Helper function to Base64URL encode as per https://tools.ietf.org/html/rfc4648#section-5
		 */
		function base64url_encode($string)
		{
			return strtr(rtrim(base64_encode($string), '='), '+/', '-_');
		}

		/**
		 * Helper function to display the current state values
		 */
		function debugState($message = NULL)
		{
			if ($message != NULL)
			{
				echo "<pre>$message</pre>";
			}

			echo "<pre>passthroughState1 = $this->passthroughState1</pre>";
			echo "<pre>passthroughState2 = $this->passthroughState2</pre>";
			echo "<pre>code = $this->code</pre>";
			echo "<pre>token = $this->token</pre>";
			echo "<pre>refreshToken = $this->refreshToken</pre>";
			echo "<pre>instanceURL = $this->instanceURL</pre>";
			echo "<pre>userId = $this->userId</pre>";
			echo "<pre>error = $this->error</pre>";
			echo "<pre>errorDescription = $this->errorDescription</pre>";
			echo "<pre>codeVerifier = $this->codeVerifier</pre>";
		}
	}

	// If we have not yet initialised state, are resetting or are Authenticating then Initialise State
	// and store in a session variable.
	if ($_SESSION['state'] == NULL || $_POST["reset"] || $_POST["authenticate"])
	{
	 	$_SESSION['state'] = new State('ippy', 'dippy');
	}

	$state = $_SESSION['state'];

	// Attempt to load the state from the page request
	$state->loadStateFromRequest();

	// if an error is present, render the error
	if ($state->error != NULL)
	{
		renderError();		
	}

	// Determine the form action
	if ($_POST["authenticate"])	// Authenticate button clicked
	{
		doOAUTH();	
	}
	else if ($_POST["login_via_code"])	// Login via Authentication Code button clicked
	{
		if (!loginViaAuthenticationCode())
		{
			renderError();
			return;
		}

		renderPage();
	}
	else if ($_POST["login_via_refresh_token"])	// Login via Refresh Token button clicked
	{
		if (!loginViaRefreshToken())
		{
			renderError();
			return;
		}

		renderPage();
	}
	else if ($_POST["get_user"])	// Get User button clicked
	{
		// Get the user data from Salesforce
		$userDataHTML = getUserData();

		// Render the page passing in the user data
		renderPage($userDataHTML);
	}
	else	// Otherwise render the page
	{
		renderPage();
	}

	// Render the Page
	function renderPage($userDataHTML = NULL)
	{
		$state = $_SESSION['state'];

		echo "<!DOCTYPE html>";
?>
		<html>
			<head>
			  	<title>SFDC - OAuth 2.0 Web Server Authentication Flow</title>
			  	<meta charset="UTF-8">
			</head>

			<body>
				<h1>SFDC - OAuth 2.0 Web Server Authentication Flow</h1>
<?php
				// Show the current state values
				$state->debugState();

				// If we have some user data to display then do so
				if ($userDataHTML)
				{
					echo $userDataHTML;
				}
?>
				<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">
					<input type="submit" name="reset" value="Reset" />
					<input type="submit" name="authenticate" value="Authenticate" />
					<input type="submit" name="login_via_code" value="Login via Authentication Code" />
					<input type="submit" name="login_via_refresh_token" value="Login via Refresh Token" />
					<input type="submit" name="get_user" value="Get User" />
				</form>

			</body>
		</html>
<?php
	}

	/**
	 * Redirect page to Salesforce to authenticate
	 */
	function doOAUTH()
	{
		$state = $_SESSION['state'];

		// Set the Authentication URL
		// Note we pass in the code challenge
		$href = "https://login.salesforce.com/services/oauth2/authorize?response_type=code" . 
				"&client_id=" . getClientId() . 
				"&redirect_uri=" . getCallBackURL() . 
				"&scope=api refresh_token" . 
				"&prompt=consent" . 
				"&code_challenge=" . $state->generateCodeChallenge() .
				"&state=" . $state->serializeStateString();

		// Wipe out arbitary state values to demonstrate passing additional state to salesforce and back
		$state->passthroughState1 = NULL;
		$state->passthroughState2 = NULL;

		// Perform the redirect
		header("location: $href");
	}

	/**
	 * Login via an Authentication Code
	 */
	function loginViaAuthenticationCode()
	{
		$state = $_SESSION['state'];

		// Create the Field array to pass to the post request
		// Note we pass in the code verifier and the authentication code
		$fields = array('grant_type' => 'authorization_code', 
						'client_id' => getClientId(),
						'client_secret' => getClientSecret(),
						'redirect_uri' => getCallBackURL(),
						'code_verifier' => $state->generateCodeVerifier(),
						'code' => $state->code,
						);
		
		// perform the login to Salesforce
		return doLogin($fields, false);
	}

	/**
	 * Login via a Refresh Token
	 */
	function loginViaRefreshToken()
	{
		$state = $_SESSION['state'];

		// Create the Field array to pass to the post request
		// Note we pass in the refresh token
		$fields = array('grant_type' => 'refresh_token', 
						'client_id' => getClientId(),
						'client_secret' => getClientSecret(),
						'redirect_uri' => getCallBackURL(),
						'refresh_token' => $state->refreshToken,
						);

		// perform the login to Salesforce
		return doLogin($fields, true);
	}

	/**
	 * Login to Salesforce to get a Session Token using CURL
	 */
	function doLogin($fields, $isViaRefreshToken)
	{
		$state = $_SESSION['state'];

		// Set the POST url to call
		$postURL = 'https://login.salesforce.com/services/oauth2/token';

		// Header options
		$headerOpts = array('Content-type: application/x-www-form-urlencoded');

		// Create the params for the POST request from the supplied fields	
		$params = "";
		
		foreach($fields as $key=>$value) 
		{ 
			$params .= $key . '=' . $value . '&';
		}

		$params = rtrim($params, '&');

		// Open the connection
		$ch = curl_init();

		// Set the url, number of POST vars, POST data etc
		curl_setopt($ch, CURLOPT_URL, $postURL);
		curl_setopt($ch, CURLOPT_POST, count($fields));
		curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headerOpts);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

		// Execute POST
		$result = curl_exec($ch);

		// Close the connection
		curl_close($ch);

		//record the results into state
		$typeString = gettype($result);
		$resultArray = json_decode($result, true);

		$state->error = $resultArray["error"];
		$state->errorDescription = $resultArray["error_description"];

		// If there are any errors return false
		if ($state->error != null)
		{
			return false;
		}

		$state->instanceURL = $resultArray["instance_url"];
		$state->token = $resultArray["access_token"];

		// If we are logging in via an Authentication Code, we want to store the 
		// resulting Refresh Token
		if (!$isViaRefreshToken)
		{
			$state->refreshToken = $resultArray["refresh_token"];
		}

		// Extract the user Id
		if ($resultArray["id"] != null)
		{
			$trailingSlashPos = strrpos($resultArray["id"], '/');

			$state->userId = substr($resultArray["id"], $trailingSlashPos + 1);
		}

		// verify the signature
		$baseString = $resultArray["id"] . $resultArray["issued_at"];
		$signature = base64_encode(hash_hmac('SHA256', $baseString, getClientSecret(), true));

		if ($signature != $resultArray["signature"])
		{
			$state->error = 'Invalid Signature';
			$state->errorDescription = 'Failed to verify OAUTH signature.';

			return false;
		}

		// Debug that we've logged in via the appropriate method
		echo "<pre>Logged in " . ($isViaRefreshToken ? "via refresh token" : "via authorisation code") . "</pre>";

		return true;
	}

	/**
	 * Get User Data from Salesforce using CURL
	 */
	function getUserData()
	{
		$state = $_SESSION['state'];

		// Set our GET request URL
		$getURL = $state->instanceURL . '/services/data/v20.0/sobjects/User/' . $state->userId . '?fields=Name';

		// Header options
		$headerOpts = array('Authorization: Bearer ' . $state->token);

		// Open connection
		$ch = curl_init();

		// Set the url and header options
		curl_setopt($ch, CURLOPT_URL, $getURL);
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headerOpts);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

		// Execute GET
		$result = curl_exec($ch);

		// Close connection
		curl_close($ch);

		// Get the results
		$typeString = gettype($result);
		$resultArray = json_decode($result, true);

		// Return them as an html String
		$rtnString = '<hr><h2>User Data</h2>';

		foreach($resultArray as $key=>$value) 
		{ 
			$rtnString .= "<pre>$key=$value</pre>";
		}

		return $rtnString;
	}

	/**
	 * Helper function to render an Error
	 */
	function renderError()
	{
		$state = $_SESSION['state'];

		echo '<div class="error"><span class="error_msg">' . $state->error . '</span> <span class="error_desc">' . $state->errorDescription . '</span></div>';
	}

	/**
	 * Get the hard coded Client Id for the Conected Application
	 */
	function getClientId()
	{
		return "xxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyzzzzzzzzzzzzzzzzzzzzzzzzzz";
	}

	/**
	 * Get the hard coded Client Secret for the Conected Application
	 */
	function getClientSecret()
	{
		return "1234567890123456789";
	}

	/**
	 * Get the Call back URL (the current php script)
	 */
	function getCallBackURL()
	{
		$callbackURL = ($_SERVER['HTTPS'] == NULL || $_SERVER['HTTPS'] == false ? "http://" : "https://") .
			$_SERVER['SERVER_NAME'] . "/" . $_SERVER['PHP_SELF'];

		return $callbackURL;
	}
?>

I’m using a class called State to represent the state and storing an instance of it in a SESSION variable.

You’ll notice some helper functions for popuating the state from a GET request following the Salesforce call back.

You’ll also notice some helper functions for serialising and deserialising the state we want to pass through with the authorisation request. While it’s not that useful in this example as we are using PHP’s SESSION variables to manage state it demonstrates how you can preserve data across the request.

In addition there are two helper functions that generate the Code Challenge and Code Verifier used across the authorisation request. While not strictly necessary this adds an additional layer of security. Salesforce will compare the two values when attempting to login using the authentication code. I had a bit of mare getting this to work so I’d like to give a quick nod to Ian Ratcliffe’s forum post that used the PHP bin2hex and pack functions to clinch this. You’ll see these codes used when constructing the authorisation and login requests in the doOAUTH() and loginViaAuthenticationCode() functions.

In the doLogin() function you’ll also notice another security check after the login has succeeded. This verifies the signature passed back in the login response with a hash of the User Id and Issued At values from the response using the Client Secret.

Dropbox Hack

As I mentioned earlier, it’s important that your web server is secured since Salesforce will only authenticate a callback over HTTPS.

I don’t recommend this at all and take no responsibility for using this hack … but if, like me, you’re just hacking around in a developer org, have nothing that you’d want falling into the wrong hands and are just trying stuff out you can use Dropbox to take care of the HTTPS callback.

To do this, in your Public Dropbox folder create a script called redirect.htm that looks like this (change the path to match the URL of your PHP script:

<script>
var href = window.location.href;
var queryParams = href.substring(href.indexOf('?'));
window.location = 'http://apps.myphpserver.com/TestOAUTH/sfdc_oauth_web.php' + queryParams;
</script>

Now copy the Dropbox public link and paste it into the Callback URL of your Connected App definition on Salesforce.

In the function getCallBackURL() of your PHP script assign the same link path to the $callbackURL variable.

Now Salesforce will use call back to the Dropbox script which will then strip the parameters and attach them to a redirect to your PHP Script (all very insecurely, of course!)

Advertisements

Author, brainstormer, coder, dad, explorer, four chord trickster, gig goer, home worker, inquisitor, joker, knowledge seeker, likes: marmite, note scribbler, opinionator, poet, quite likes converse, roller skater, six music listener, tea drinker, urban dweller, vinyl spinner, word wrangler, x-factor hater, Yorkshireman (honorary), zombie slayer (lie).

Tagged with: , , , , , ,
Posted in apex, code, force.com, PHP, salesforce
One comment on “Connecting to Salesforce using OAuth 2.0 from PHP
  1. […] Source: Connecting to Salesforce using OAuth 2.0 from PHP […]

Comments are closed.

About Me
Product Services Developer at:
FinancialForce.com
All views expressed here are my own. More about me and contact details here.

Enter your email address to follow this blog and receive notifications of new posts by email.

Copyright (there isn’t any, feel free to reuse!)

CC0
To the extent possible under law, Tony Scott has waived all copyright and related or neighboring rights to MeltedWires.com Examples and Code Samples. This work is published from: United Kingdom.

%d bloggers like this: