• Wilcox Jones
  • NEWBIE
  • 0 Points
  • Member since 2020

  • Chatter
    Feed
  • 0
    Best Answers
  • 0
    Likes Received
  • 0
    Likes Given
  • 0
    Questions
  • 1
    Replies
Hi there,

I just thought I'll write the results of my journey through the Salesforce APEX REST world. This is not a question, rather a documentation. Please note that I'm more of a newbie rather than an experienced programmer. I'll try to be precise but sometimes I might simply don't know better. In the end it did work for me - after several hours of looking, testing and reading.
What do I want to do?
The primary intention is to offer a form on my web server (NOT Salesforce) for people to register. In our case we already have Accounts and Contacts in our Salesforce system and we want to ask (some) of the contacts to go to our web-site and apply for the restriceted area. They would be supplying their email address, their account number and a password.
Later - after their request has been approved - they gain access to the web server's restricted site (again - NOT Salesforce) by checking the Salesforce data.
What was my first thought?
Easy, I thought. Create some Javascript. The customer loads the JS into his browser. Upon the button pressed event it takes the data from the form and does post it against a REST endpoint at Salesforce. There I would have a REST service - simply a very basic APEX class with some special "comments" - and this class would be doing the thing.
What I did forget...
was that stuff with the security.
First trap was the so-called CORS stuff (Cross-Origin Resource Sharing). The customer's browser is loading code from my web server. And the JS code loaded from this server, running on the customer's client, would be calling something on a different domain - Salesforce. So browsers do not allow this. Unless - well - unless the second server allows it --> you have to whitelist the web server.
I tried this - but it did not work. Basically the message is: you are not allowed to call an APEX REST service if you didn't authenticate yourself. So - how would we be doing this?
Use OAUTH...
Salesforce offers several so-called flows to authenticate a user. It is really worth spending a few minutes to read the help here - https://help.salesforce.com/articleView?id=remoteaccess_authenticate.htm&type=5 . The thing is that - at least to the extend I understood the documentation - you either prompt the user for a SALESFORCE UserId and Password throught the standard login screen (which isn't really nice, and which requires every user to have corresponding SALESFORCE license), or you do use some kind of pre-shared secret (my understanding is based upon docs like this: https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/extend_code_cors.htm where it says at the bootom: „CORS does not support requests for unauthenticated resources, including OAuth endpoints. You must pass an OAuth token with requests that require it.“)
I decided to go with the so-called Java Web Token JWT. Well - nice - but then my Javascript would have the pre-shared secret somewhere on the browser. And if it's on the browser it's no secret anymore.... So I did
Get rid of CORS
and decided to implement the connectivity to Salesforce not with Javascript in the browser on the customer's client but using PHP on web server.
The JWT scenario works like this:
- you set up a so-called "Connected app" in Salesforce (Set-up -> Build --> Create --> Apps
- I did specify to use OAuth
- the OAuth policy included:
* Admin approved users are pre-authorized
* Scope includes "Perform requests on your behalf at any time" and "Access and manage your data"
* a digital certificate - I created a selfsigned certificate using openssl (openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365) - I did upload the cert.pem to Salesforce - and the key.pem on my webserver - surely not into the webroot - you know – this file should be kept REALLY confidentially...
- I then did create a permission set to include the connected app and assigned the permission set to a user (AFAIK the APEX REST class to do the work for the service runs at system level - so who-ever this user is - he/she can do a LOT...
So now - what happens?
The PHP code on the server creates a request. It addresses a well-know endpoint and fills in some data. It signs the data with the private key that we did generate earlier. Then it calls the endpoint. Salesforce checks the signature using the certificate, finds the connected app based upon the data in the request and provides a token - a "newly generated password" back to the PHP code.
The PHP code uses this token to authenticate the request that it sends to the Salesforce APEX REST endpoint.
The code
Let me give you some example.
The HTML file on the web server
<!DOCTYPE html> 
<html lang="en"> 
<head> 
<meta charset="utf-8"> 
<title>Integration to Salesforce - Test</title> 
</head>
<body>
<!-- Form -->
<form name="applicant" id="applicant" autocomplete="off">
<p>
<label>Email</label><input type="text" id="email" size="64" autofocus>
</p>
<p>
<label>Ext.Account No.</label><input type="text" id="kdnr" size="64">
</p>
<p>
<label>Password</label><input type="password" id="password" size="64">
</p>
<p><button class="btn btn-success" id="btnSubmit" type="button" onclick="btnSubmitOnClick()">Submit</button></p>


<p><span id="insertresult"></span></p>
</form>
<script type="text/javascript" src="md5.min.js"></script>

<script type="text/javascript" src="testsf.js"></script>

</body>
</html>
The testsf.js Javascript on the webserver (md5.min.js is a tool library that also resides on the server - get it in the internet..)
function btnSubmitOnClick() {
	var email = document.getElementById( "email").value;
	var kdnr = document.getElementById( "kdnr").value;
// we won't be storing the password anywhere in clear text - use the md5 coded value - and yes I know - this is NOT secure - but this is a demonstration only
	var password = md5( document.getElementById( "password").value);

// and yes - we should be doing some checks here to make sure we do not get malformed data --> https://stackoverflow.com/questions/295566/sanitize-rewrite-html-on-the-client-side/430240#430240
    
	var xhr;
	var data = 'email=' + email + '&kdnr=' + kdnr + '&password=' + password;

	xhr = new XMLHttpRequest();
	xhr.open( "POST", "jwtbearer.php", true);
	xhr.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded");
	xhr.addEventListener( "load", display_insert);
	xhr.send( data);

	function display_insert() {
		document.getElementById( "insertresult").innerHTML = xhr.responseText;
	}
}
Now for the PHP code in jwtbearer.php on the web server:
<?php
// ini_set('display_errors', 'On');
// error_reporting(E_ALL | E_STRICT);
$email = $_POST ['email'];
$kdnr = $_POST ['kdnr'];
$password = $_POST ['password'];

// documentation in https://help.salesforce.com/articleView?id=remoteaccess_oauth_jwt_flow.htm&type=5
// code based upon https://developer.salesforce.com/forums/?id=906F00000005HTiIAM

// the following values ought to be defined via $_SERVER ['variables']
// this is the client ID in the connected app
define('CONSUMER_KEY', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXX');
// and this is the secret of the connected app - actually we do not use it
define('CONSUMER_SECRET', '99999999999999');
// the subject is a name of a user. Make sure that
// - the connected application is set to "Admin approved users are pre-authorized"
// - there exists a permission set that has the right to access the connected application
// - this user is assigned to the permission set
// - the connected application has at least the following two scopes: "Access and manage your data (api)", "Perform requests on your behalf at any time (refresh_token, offline_access)"
// - the connected app has attached the certificate for the private key further down
define('SUBJECT', 'user@domain.test');

define('LOGIN_BASE_URL', 'https://test.salesforce.com');

// First step: get an authorization token from Salesforce that allows us to later call the APEX REST service
// we do this using the so-called JWT bearer flow --> https://help.salesforce.com/articleView?id=remoteaccess_oauth_jwt_flow.htm&type=5

// this is where we'll getting the token from
$token_url = LOGIN_BASE_URL.'/services/oauth2/token';

// JSon Header
$h = array(
	"alg" => "RS256"	
);

$jsonH = json_encode(($h));	

// ATTENTION: it has to be base64URL encoded!!!
$header = rtrim(strtr(base64_encode($jsonH), '+/', '-_'), '=');

// Create JSon Claim/Payload
$c = array(
	"iss" => CONSUMER_KEY, 
	"sub" => SUBJECT, 
	"aud" => LOGIN_BASE_URL, 
	"exp" => strval(time() + (5 * 60))
);

$jsonC = (json_encode($c));	

$payload = rtrim(strtr(base64_encode($jsonC), '+/', '-_'), '='); 

$jwtToBeSigned = $header.'.'.$payload;

// This is where openssl_sign will put the signature
$signature = "";

// get the private key from a file (and supply the password for this key - ought to be stored in a S_SERVER variable as well
$pkeyid = openssl_pkey_get_private("file:///etc/php5/apache2/mykey.pem", "9999999999");

// Sign the header and payload
openssl_sign($jwtToBeSigned, $signature, $pkeyid, OPENSSL_ALGO_SHA256);
openssl_free_key($pkeyid);

// Base64URL encode the signature
$secret = rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');

// construct the "assertion" that Salesforce will use to test
$assertion = $header.'.'.$payload.'.'.$secret;

// and from this construct the array of fields to post
$post_fields = array(
	'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
	'assertion' => $assertion
);

// now prepare the cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $token_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

// Make the API call, and then extract the information from the response
$token_request_body = curl_exec($ch);
if ( $token_request_body === FALSE) {
    echo "<pre>";        
    echo "Error from curl_exec during JWT bearer token request: " . curl_error( $ch);
    echo "</pre>";   
    return;
}

// so now we got the token - let's extract the relevant info
$token_request_array = json_decode( $token_request_body, true);

$theToken = $token_request_array[ 'access_token'];
$theInstance = $token_request_array[ 'instance_url'];

curl_close($ch);

// now start the real Service call
// salesforce will tell us where EXACTLY we have to ring...
$token_url = $theInstance . '/services/apexrest/hpp/v1';

// remember: the APEX class in salesforce defines the path; the methods in this class correspond to the various types like POSt, GET....
// Content-type has to be correct
$headers = array( 
            "POST " . "/services/apexrest/test/v1". " HTTP/1.1",
            "Content-type: application/json", 
            "Accept: */*", 
            "Cache-Control: no-cache", 
            "Pragma: no-cache",
            "Authorization: Bearer " . $theToken
        ); 

// these are the fields we want to submit to the APEX REST service
$post_fields = array(
	'email' => $email,
	'kdnr' => $kdnr,
	'password' => $password
);


$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $token_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_fields));
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 

$service_request_body = curl_exec($ch) ;
   
if ( $service_request_body === FALSE) {
    echo "<pre>";        
    echo "Error from curl_exec during APEX REST call: " . curl_error( $ch);
    echo "</pre>";    
    return;
}
    
curl_close($ch);
echo "Success!";
Now to Salesforce. I addedd two checkbox fields to the Contact standard object - Applied_for_web_access__c and Granted_web_access__c. I also added a text(32) field to store the hashed password: Web_Hash__c.
Here is the sample code for the APEX class serving as the REST service:
@RestResource(urlMapping='/test/*')
global with sharing class testRestInterface {
    @HttpPost
    global static String createApplicationForAccess( String email, String kdnr, String password) {
        Contact[] cList = [Select Id From Contact Where Email = :email and Account.ExtAccountNo__c = :kdnr];
        if ( !cList.isEmpty()) {
            Contact aContact = cList[0];
            aContact.Applied_for_web_access__c = true;
            aContact.Web_Hash__c = password;
            update aContact;
        } else {
/* here I decided to create a lead... */
            Lead aLead = new Lead( Lastname='Unknown web access requestor', Company='unkonwn company', Applied_for_web_access__c = true, Web_Hash__c = password);
            insert aLead;
        }
        return 'SUCCESS';
    }

}
Later in the project I did use Email Actions with Email Templates that get send upon Changes to the Checkboxes caught in Process Builder - but that's another story....

Lessons learned:
- no APEX REST access without authentication
- authentication done on the web server - not on the client
- base64URL encoded values in the JWT bearer section
- pre-authorized users, with permission set

So - hopefully I did not state something totally wrong here and hopefully too this will help somebody someday.